Table of Contents

Getting started

This page takes you from an empty project to a program that connects to a ViiSync account, reads it, streams quotes, trades and downloads history. Every snippet is compiled as part of the documentation build.

1. Reference the library

ViiSync.AccountApi ships as one file, ViiSync.AccountApi.dll, built for net8.0 and net10.0. It is not published on NuGet. You get the DLL for your target framework together with its XML documentation file (ViiSync.AccountApi.xml, which gives you IntelliSense) and a .sha256 checksum.

Copy both files into your repository, for example under lib/, and reference the DLL:

<ItemGroup>
  <Reference Include="ViiSync.AccountApi">
    <HintPath>lib\ViiSync.AccountApi.dll</HintPath>
  </Reference>
</ItemGroup>

The DLL depends on nothing but the .NET runtime. Its only public namespace is ViiSync.AccountApi:

using ViiSync.AccountApi;

2. Connect and log in

Create the API with the account login, password, server host and port, then call Connect(). Connect opens the WebSocket, logs in and loads the account state (symbols, positions, pending orders and account figures) before it returns.

// Port 443 connects over TLS to wss://<host>/ws; any other port uses ws://<host>:<port>/ws.
var api = new ViiSyncAccountApi(login, password, "server.example.com", 443);
api.OnConnectProgress += (_, e) => Console.WriteLine($"connect: {e.Progress}");
try
{
    api.Connect();   // connect + log in + load symbols, positions, orders and account figures
}
catch (ConnectException ex) when (ex.Code == Msg.INVALID_ACCOUNT)
{
    Console.WriteLine("login or password refused");
    throw;
}

Port 443 connects over TLS to wss://<host>/ws. You can also pass the endpoint URL and set the timeouts explicitly:

var api = new ViiSyncAccountApi(login, password, new Uri("wss://server.example.com/ws"))
{
    ConnectTimeout = 20_000,     // connect + login + initial state, ms
    ExecutionTimeout = 30_000,   // how long a trade call waits for the server, ms
    AutoReconnect = true,        // reconnect after a dropped connection (the default)
};
api.Connect();

If the connection drops, the API reconnects by itself (AutoReconnect), logs in again and reloads the state. OnConnectProgress reports each stage.

A login with the account's investor password opens a read-only session: IsInvestor is true, and every trade or password call throws InvestorModeException.

3. Read the account

The account figures are kept current from the server's pushes, so reading them costs no round trip.

// Kept current from the server's pushes: reading these costs no round trip.
Console.WriteLine($"#{api.Account.Login} {api.AccountCurrency} 1:{api.AccountLeverage} {api.AccountMethod}");
Console.WriteLine($"balance {api.AccountBalance:N2} equity {api.AccountEquity:N2}");
Console.WriteLine($"margin {api.AccountMargin:N2} free {api.AccountFreeMargin:N2} level {api.MarginLevel:N2}%");
api.OnAccountUpdate += a => Console.WriteLine($"equity now {a.AccountEquity:N2}");

4. Symbols

Symbols holds the account's symbol catalog: the specification of each symbol (SymbolInfo), its trade settings in MT5's SymGroup shape and its weekly sessions.

foreach (var name in api.Symbols.Names)
{
    SymbolInfo info = api.Symbols.GetInfo(name);    // specification
    SymGroup trade = api.Symbols.GetGroup(name);    // trade settings in MT5's SymGroup shape
    Console.WriteLine($"{name}: {info.Digits} digits, contract {info.ContractSize}, " +
                      $"lots {trade.MinLots}..{trade.MaxLots} step {trade.LotsStep}");
}

5. Quotes

Subscribe to a symbol to stream its quotes to OnQuote. GetQuote returns the latest quote.

api.OnQuote += (_, q) => Console.WriteLine($"{q.Symbol} {q.Bid}/{q.Ask} at {q.Time:HH:mm:ss.fff} UTC");
api.Subscribe("EURUSD");

// The latest quote: the streamed one when subscribed, otherwise one round trip to the server.
Quote? quote = api.GetQuote("EURUSD");
Console.WriteLine(quote is null ? "no price" : $"spread {quote.Ask - quote.Bid}");

api.Unsubscribe("EURUSD");

6. Trade

OrderSend takes MT5's parameter list. A market order returns the opened position; a pending order returns the working order. It returns after the server has answered, and throws ServerException with an MT5 return code when the server refuses the request (see Error codes).

var q = api.GetQuote("EURUSD") ?? throw new InvalidOperationException("no price");
// Same parameter list as MT5's OrderSend. With deviation > 0 and a price, a fill outside price ± deviation
// points is refused with REQUOTE; with deviation 0 the order fills at the live quote.
Order position = api.OrderSend("EURUSD", 0.10, q.Ask, OrderType.Buy,
    sl: 0, tp: 0, deviation: 10, comment: "docs", expertID: 42);
Console.WriteLine($"opened position #{position.Ticket} at {position.OpenPrice}");

Modify the SL/TP of a position:

// SL/TP are absolute prices; 0 removes a level.
var info = api.Symbols.GetInfo("EURUSD");
api.OrderModify(position.Ticket, "EURUSD", position.Lots, 0, position.OrderType,
    sl: Math.Round(q.Bid - 200 * info.Points, info.Digits), tp: 0);

Close part of it, then the rest:

// Lots below the position's volume close part of it; the rest stays open.
Order part = api.OrderClose(position.Ticket, "EURUSD", 0, 0.04, position.OrderType);
Console.WriteLine($"closed {part.CloseLots} lots, P/L {part.Profit:N2}");
Order closed = api.OrderClose(position.Ticket, "EURUSD", 0, 0, position.OrderType);   // the rest
Console.WriteLine($"closed #{closed.Ticket}: P/L {closed.Profit:N2}, commission {closed.Commission:N2}");

Pending orders are placed, moved and cancelled the same way:

var q = api.GetQuote("EURUSD") ?? throw new InvalidOperationException("no price");
Order limit = api.OrderSend("EURUSD", 0.10, Math.Round(q.Bid - 0.0050, 5), OrderType.BuyLimit,
    expiration: new Expiration { Type = ExpirationType.Specified, DateTime = DateTime.UtcNow.AddHours(4) });
Console.WriteLine($"pending order #{limit.Ticket} {limit.State}");

// Move it, then cancel it.
api.OrderModify(limit.Ticket, "EURUSD", limit.Lots, Math.Round(q.Bid - 0.0060, 5), limit.OrderType, sl: 0, tp: 0);
api.OrderDelete(limit.Ticket, limit.OrderType, "EURUSD", limit.Lots, 0);

On a hedging account, a position can be closed by an opposite one:

// Hedging accounts: close a position by an opposite one of the same symbol (no spread paid on the pair).
Order closed = api.OrderCloseBy(buyTicket, sellTicket);

Open positions and working orders are always at hand:

foreach (Order o in api.GetOpenedOrders())
    Console.WriteLine(o.IsPending
        ? $"pending #{o.Ticket} {o.OrderType} {o.Lots} {o.Symbol} @ {o.OpenPrice}"
        : $"position #{o.Ticket} {o.OrderType} {o.Lots} {o.Symbol} P/L {o.Profit:N2}");

Trading through a dropped connection

A trade call that loses its connection fails with code NO_CONNECTION (a ConnectException or ServerException), and one whose answer never comes fails with REQUEST_TIMEOUT. In both cases the request may or may not have executed. OrderClientSafe wraps the API and settles the outcome for you:

// Survives a dropped connection mid-call: waits for the reconnect, then settles the outcome from the server's
// state. An open is never re-sent (it is looked up instead), so it cannot double the exposure.
var safe = new OrderClientSafe(api) { TradeTimeoutSafe = 60_000 };
Order o = safe.OrderSend("EURUSD", 0.10, 0, OrderType.Buy);
safe.OrderClose(o.Ticket, "EURUSD", 0, 0, o.OrderType);

7. History

DownloadOrderHistory returns the trades closed in a window (one Order per closed position, built from its deals as the MT5 terminal does), every deal of the window and every order placed in it.

var to = DateTime.UtcNow;
var from = to.AddDays(-7);
OrderHistoryEventArgs h = api.DownloadOrderHistory(from, to);
Console.WriteLine($"{h.Orders.Count} closed trades, {h.InternalDeals.Count} deals, {h.InternalOrders.Count} orders");
foreach (Order trade in h.Orders)
    Console.WriteLine($"#{trade.Ticket} {trade.Symbol} {trade.OpenTime:u} -> {trade.CloseTime:u} P/L {trade.Profit:N2}");

Bars come in any timeframe, and raw ticks are available too:

// Any timeframe in minutes: 1, 5, 15, 30, 60, 240, 1440, 10080 (W1), 43200 (MN1) ...
Bar[] h1 = api.DownloadQuoteHistory("EURUSD", DateTime.UtcNow.AddDays(-2), DateTime.UtcNow, 60);
TickBar[] ticks = api.DownloadTickHistory("EURUSD", DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow);
Console.WriteLine($"{h1.Length} H1 bars, {ticks.Length} ticks");

8. Events

The API raises MT5's events with MT5's delegate shapes, plus a few ViiSync extras (OnAccountUpdate, OnLog).

// Events are raised on the API's receive loop: keep handlers short and never block in them.
api.OnOrderUpdate += (_, u) => Console.WriteLine($"{u.Type} {u.TradeType} {u.Order}");
api.OnOrderProgress += (_, p) => Console.WriteLine($"request {p.TradeRequest.RequestId}: {p.Type} {p.TradeResult.Status}");
api.OnConnectProgress += (_, e) => Console.WriteLine($"connection: {e.Progress}");
api.OnLog += (_, line) => Console.WriteLine($"log: {line}");
Important

Events are raised on the API's receive loop. A slow handler delays every later push, quotes included. Handlers that run longer than ProcessEventTimeoutMs are reported on OnLog and counted in SlowEventHandlers. Hand long work to another thread.

9. Async and disconnect

Every blocking call has an …Async twin that takes a CancellationToken:

await api.ConnectAsync(ct);
Order o = await api.OrderSendAsync("EURUSD", 0.10, 0, OrderType.Buy, cancellation: ct);
await api.OrderCloseAsync(o.Ticket, "EURUSD", 0, 0, o.OrderType, cancellation: ct);
await api.DisconnectAsync();
api.Disconnect();   // or: using var api = new ViiSyncAccountApi(...); Dispose disconnects

Next