Skip to content

События торговых терминалов через SSE ​

Terminal Events API позволяет внешнему клиенту читать диагностические события выбранного NinjaTrader 8 или MetaTrader 5 account в реальном времени:

  • order_update — изменение состояния ордера;
  • execution_update — исполнение, включая partial fill;
  • position_update — изменение позиции.

Поток односторонний: terminal передаёт события backend через существующее SignalR-соединение, а Public API client читает их через Server-Sent Events (SSE). Обычная отправка сигналов и ReportApiSignalStatus работают независимо от этого потока.

Оба endpoint требуют scope events:read.

GET /api/v1/terminal-events/sources ​

Возвращает доступные сейчас NinjaTrader и MetaTrader sources текущего пользователя.

Source включается в список, если:

  • terminal подключён к аккаунту этого пользователя;
  • terminal объявил capability terminal-events-v1;
  • последний heartbeat получен не более 120 секунд назад.

Один source — это точная комбинация platform + instanceId + accountName. Несколько accounts или компьютеров отображаются отдельными строками, если каждое terminal connection зарегистрировано со своим уникальным instanceId. Повторное подключение с тем же instanceId, включая пустой, заменяет прежнее соединение того же типа terminal для этого пользователя.

Примеры ​

bash
curl 'https://tradingmonitor.pro/api/v1/terminal-events/sources' \
  --header 'Authorization: Bearer tmpro_api_REPLACE_ME'
csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

public sealed record TerminalSource(
    string Platform,
    string InstanceId,
    string AccountName,
    string? ClientVersion,
    DateTimeOffset LastHeartbeat);

using var http = new HttpClient
{
    BaseAddress = new Uri("https://tradingmonitor.pro")
};
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "tmpro_api_REPLACE_ME");

var sources = await http.GetFromJsonAsync<List<TerminalSource>>(
    "/api/v1/terminal-events/sources");

foreach (var source in sources ?? new List<TerminalSource>())
{
    var instance = source.InstanceId.Length == 0
        ? "(default / empty)"
        : source.InstanceId;
    Console.WriteLine(
        $"{source.Platform} | {instance} | {source.AccountName} | {source.LastHeartbeat:O}");
}
python
import requests

response = requests.get(
    "https://tradingmonitor.pro/api/v1/terminal-events/sources",
    headers={"Authorization": "Bearer tmpro_api_REPLACE_ME"},
    timeout=30,
)
response.raise_for_status()

for source in response.json():
    instance = source["instanceId"] or "(default / empty)"
    print(
        source["platform"],
        instance,
        source["accountName"],
        source["lastHeartbeat"],
    )

Пример ответа:

json
[
  {
    "platform": "ninjatrader",
    "instanceId": "",
    "accountName": "Sim101",
    "clientVersion": "1.4.0",
    "lastHeartbeat": "2026-07-16T12:00:00Z"
  },
  {
    "platform": "mt5",
    "instanceId": "",
    "accountName": "12345678@Broker-Demo",
    "clientVersion": "2.11.0.0",
    "lastHeartbeat": "2026-07-16T12:00:03Z"
  }
]
ПолеОписание
platformninjatrader или mt5
instanceIdточное значение из настроек terminal plugin; пустая строка является допустимым default instance
accountNameдля NinjaTrader — выбранный account, например Sim101; для MT5 — автоматически сформированное <ACCOUNT_LOGIN>@<ACCOUNT_SERVER>
clientVersionверсия terminal client или null
lastHeartbeatвремя последнего подтверждённого heartbeat

Список является моментальным снимком. Source может отключиться после получения ответа, поэтому stream endpoint повторно проверяет его доступность.

GET /api/v1/terminal-events/stream ​

Открывает SSE-подписку для одной точной комбинации platform + instanceId + accountName.

Query parameters ​

ПараметрОбязательныйОграничения
platformдаровно одно значение: ninjatrader или mt5
instanceIdдаровно одно значение; пустая строка разрешена; до 128 символов
accountNameдаровно одно непустое значение; до 128 символов

Совпадение platform, instanceId и accountName точное; instanceId и accountName чувствительны к регистру. Не вводите их вручную, если можно выбрать значения из ответа /sources.

Пустой instanceId

Для default terminal query parameter всё равно должен присутствовать: instanceId=. Отсутствующий параметр и пустое значение — разные случаи.

Дополнительный request header:

HeaderОбязательныйНазначение
Last-Event-IDнетпродолжить поток после последнего полностью обработанного eventId; не более 80 символов

curl ​

-N отключает output buffering в curl. Вариант с пустым default instance:

bash
curl --no-buffer --get \
  'https://tradingmonitor.pro/api/v1/terminal-events/stream' \
  --header 'Authorization: Bearer tmpro_api_REPLACE_ME' \
  --data-urlencode 'platform=ninjatrader' \
  --data-urlencode 'instanceId=' \
  --data-urlencode 'accountName=Sim101'

Вариант для MetaTrader 5 default instance:

bash
curl --no-buffer --get \
  'https://tradingmonitor.pro/api/v1/terminal-events/stream' \
  --header 'Authorization: Bearer tmpro_api_REPLACE_ME' \
  --data-urlencode 'platform=mt5' \
  --data-urlencode 'instanceId=' \
  --data-urlencode 'accountName=12345678@Broker-Demo'

Повторное подключение после события evt_...:

bash
curl --no-buffer --get \
  'https://tradingmonitor.pro/api/v1/terminal-events/stream' \
  --header 'Authorization: Bearer tmpro_api_REPLACE_ME' \
  --header 'Last-Event-ID: evt_REPLACE_WITH_LAST_PROCESSED_ID' \
  --data-urlencode 'platform=ninjatrader' \
  --data-urlencode 'instanceId=VPS-NT8-01' \
  --data-urlencode 'accountName=Live01'

C# ​

Пример читает SSE records последовательно. Для production-клиента оберните ReadStreamAsync в reconnect loop и передавайте последний полностью обработанный eventId.

csharp
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

static async Task ReadStreamAsync(
    string apiKey,
    string platform,
    string instanceId,
    string accountName,
    string? lastEventId,
    CancellationToken cancellationToken)
{
    using var http = new HttpClient
    {
        BaseAddress = new Uri("https://tradingmonitor.pro"),
        Timeout = Timeout.InfiniteTimeSpan
    };

    var query =
        $"?platform={Uri.EscapeDataString(platform)}" +
        $"&instanceId={Uri.EscapeDataString(instanceId)}" +
        $"&accountName={Uri.EscapeDataString(accountName)}";

    using var request = new HttpRequestMessage(
        HttpMethod.Get,
        "/api/v1/terminal-events/stream" + query);
    request.Headers.Authorization =
        new AuthenticationHeaderValue("Bearer", apiKey);
    request.Headers.Accept.Add(
        new MediaTypeWithQualityHeaderValue("text/event-stream"));
    if (!string.IsNullOrEmpty(lastEventId))
    {
        request.Headers.TryAddWithoutValidation("Last-Event-ID", lastEventId);
    }

    using var response = await http.SendAsync(
        request,
        HttpCompletionOption.ResponseHeadersRead,
        cancellationToken);
    response.EnsureSuccessStatusCode();

    await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
    using var reader = new StreamReader(stream, Encoding.UTF8);

    string? id = null;
    string? eventType = null;
    var data = new StringBuilder();

    while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
    {
        var line = await reader.ReadLineAsync(cancellationToken);
        if (line is null)
        {
            break;
        }

        if (line.Length == 0)
        {
            if (eventType is not null)
            {
                var json = data.ToString();
                Console.WriteLine($"{eventType} id={id ?? "-"} data={json}");

                // Persist id only after the event has been handled successfully.
                if (id is not null)
                {
                    lastEventId = id;
                }
            }

            id = null;
            eventType = null;
            data.Clear();
            continue;
        }

        if (line.StartsWith(':'))
        {
            continue; // keepalive comment
        }
        if (line.StartsWith("id:", StringComparison.Ordinal))
        {
            id = line[3..].TrimStart();
        }
        else if (line.StartsWith("event:", StringComparison.Ordinal))
        {
            eventType = line[6..].TrimStart();
        }
        else if (line.StartsWith("data:", StringComparison.Ordinal))
        {
            if (data.Length > 0)
            {
                data.Append('\n');
            }
            data.Append(line[5..].TrimStart());
        }
    }
}

using var stop = new CancellationTokenSource();
Console.CancelKeyPress += (_, args) =>
{
    args.Cancel = true;
    stop.Cancel();
};

await ReadStreamAsync(
    "tmpro_api_REPLACE_ME",
    platform: "ninjatrader",
    instanceId: "",       // default / empty instance
    accountName: "Sim101",
    lastEventId: null,
    stop.Token);

Python ​

python
import json
import requests


def read_sse(api_key, platform, instance_id, account_name, last_event_id=None):
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Accept": "text/event-stream",
    }
    if last_event_id:
        headers["Last-Event-ID"] = last_event_id

    with requests.get(
        "https://tradingmonitor.pro/api/v1/terminal-events/stream",
        headers=headers,
        params={
            "platform": platform,
            "instanceId": instance_id,  # "" is a valid exact value
            "accountName": account_name,
        },
        stream=True,
        timeout=(10, None),
    ) as response:
        response.raise_for_status()

        event_id = None
        event_type = None
        data_lines = []

        for line in response.iter_lines(decode_unicode=True):
            if line == "":
                if event_type:
                    payload_text = "\n".join(data_lines)
                    payload = json.loads(payload_text)
                    print(event_type, event_id, payload)

                    # Save only after successful processing.
                    if event_id:
                        last_event_id = event_id

                event_id = None
                event_type = None
                data_lines = []
                continue

            if line.startswith(":"):
                continue  # keepalive comment
            if line.startswith("id:"):
                event_id = line[3:].lstrip()
            elif line.startswith("event:"):
                event_type = line[6:].lstrip()
            elif line.startswith("data:"):
                data_lines.append(line[5:].lstrip())

    return last_event_id


last_id = read_sse(
    "tmpro_api_REPLACE_ME",
    platform="ninjatrader",
    instance_id="",
    account_name="Sim101",
)

Формат событий ​

Пример SSE record:

text
id: evt_16338ff754da40f5a32e1e986b98e8b5
event: order_update
data: {"eventId":"evt_16338ff754da40f5a32e1e986b98e8b5","eventType":"order_update","platform":"ninjatrader","instanceId":"","accountName":"Sim101","occurredAt":"2026-07-16T12:00:00.200Z","receivedAt":"2026-07-16T12:00:00.240Z","rawText":"..."}

JSON в data:

ПолеОписание
eventIdуникальный cursor события
eventTypeorder_update, execution_update или position_update
platformфактическая зарегистрированная platform: ninjatrader или mt5
instanceIdфактический instance зарегистрированного соединения
accountNametrusted account identity зарегистрированного terminal connection
occurredAtвремя события на terminal
receivedAtвремя приёма backend
rawTextдиагностический текст с фактическими terminal-specific fields

Backend не доверяет значениям platform/instance/account, переданным в event report: source identity берётся из зарегистрированного SignalR connection.

Correlation в rawText ​

  • API order/execution классифицируется как api_tracked и содержит requestId, executionId, orderRef, когда correlation известна;
  • ручные, ATM, сторонние strategy/EA и legacy-ордера классифицируются как untracked;
  • position events классифицируются как unknown, поскольку terminal position update не всегда позволяет достоверно определить происхождение изменения;
  • MT5 rawText содержит тип MqlTradeTransaction, tickets, symbol, order/deal state, volume, prices, SL/TP, request ID, broker retcode и comment;
  • MT5 account identity формируется автоматически как <ACCOUNT_LOGIN>@<ACCOUNT_SERVER>.

Пример MT5 order_update:

text
callback=OnTradeTransaction
 | tracking=api_tracked
 | transaction=TRADE_TRANSACTION_ORDER_ADD
 | order=501234567
 | deal=0
 | position=0
 | positionBy=0
 | symbol=EURUSD
 | orderType=ORDER_TYPE_BUY_LIMIT
 | orderState=ORDER_STATE_PLACED
 | dealType=DEAL_TYPE_BUY
 | volume=0.10000000
 | price=1.0812000000
 | priceTrigger=0.0000000000
 | sl=1.0792000000
 | tp=1.0842000000
 | requestAction=TRADE_ACTION_PENDING
 | requestType=ORDER_TYPE_BUY_LIMIT
 | requestOrder=0
 | requestPosition=0
 | requestId=73
 | retcode=10009
 | brokerComment=Request completed
 | requestId=3b080d01b59448d59c3e1d9b93725378
 | executionId=exec_3ef5c67ee80d4638b5139b83b331d5a3
 | orderRef=ord_c05a69ac94f948c0997c661d83d2a2b1

В MT5 имя requestId встречается в двух разных контекстах: числовой trade request ID терминала и строковый Public API requestId в correlation-хвосте. rawText остаётся диагностическим текстом, поэтому при автоматическом анализе не полагайтесь на уникальность имён внутри строки.

Пример MT5 position snapshot после DEAL_ADD:

text
callback=OnTradeTransaction
 | tracking=unknown
 | transaction=position_snapshot
 | symbol=EURUSD
 | position=501234568
 | state=open
 | selectedPosition=501234568
 | positionType=POSITION_TYPE_BUY
 | volume=0.10000000
 | priceOpen=1.0812500000
 | priceCurrent=1.0812700000
 | sl=1.0792500000
 | tp=1.0842500000

NinjaTrader rawText отражает фактические callbacks NinjaTrader и обычно содержит account, instrument, order/execution/position identifiers, state, quantity и prices. Набор и порядок фрагментов может меняться между версиями indicator.

rawText предназначен для диагностики и отображения. Не используйте его как стабильный машинный контракт; для надёжного статуса API-команды используйте GET /api/v1/signals/{requestId}.

Доставка и дедупликация ​

MT5 connector ставит terminal events в неблокирующую FIFO-очередь:

  • до 2000 записей;
  • не более 10 минут;
  • повторная отправка выполняется с тем же terminal reportId, пока backend не подтвердит событие;
  • backend дедуплицирует одинаковый reportId и возвращает тот же eventId.

Если backend не поддерживает ReportTerminalEvent, connector отключает только отправку событий до следующего reconnect. Получение сигналов и отправка execution statuses продолжаются.

SSE-клиент также должен быть идемпотентным. Сохраняйте eventId только после успешной обработки и не применяйте повторно уже сохранённый eventId.

Keepalive, replay и переподключение ​

Backend отправляет comment : keepalive примерно каждые 15 секунд при отсутствии событий. Клиент должен игнорировать comments, но считать их признаком живого соединения.

События хранятся в process-local memory:

  • не более 1000 событий на пользователя;
  • не более 10 минут;
  • буфер очищается при перезапуске backend;
  • replay выполняется только при наличии Last-Event-ID.

Если cursor отсутствует в текущем буфере, backend сначала отправляет:

text
event: replay_gap
data: {"reason":"cursor_not_available"}

После replay_gap backend передаёт доступные buffered events для выбранного source, затем продолжает live stream. Это означает, что между сохранённым cursor и первым replayed event могли быть потеряны события.

Рекомендуемый reconnect backoff: 1, 2, 5 и затем 10 секунд. На каждом reconnect передавайте последний eventId, обработка которого завершилась успешно.

Лимиты и изоляция ​

  • одновременно разрешено не более 3 SSE streams на пользователя;
  • лимит общий для всех API keys пользователя и всех instanceId + accountName;
  • SSE subscriptions не уменьшают и не изменяют лимит устройств тарифного плана;
  • события разных пользователей изолированы;
  • stream фильтруется по точной комбинации platform + instanceId + accountName;
  • каждый HTTP-запрос к Public API, включая открытие stream, проходит общий API-key rate limiter;
  • очередь live events одного subscriber ограничена 256 элементами; слишком медленный клиент отключается.

Каждая SSE subscription удерживает один HTTP connection. Всегда закрывайте response/stream при остановке приложения, смене source или revoke credentials.

Ошибки ​

EndpointHTTPCodeПричина
оба401invalid_api_keyключ отсутствует или недействителен
оба403insufficient_scopeнет events:read
stream400invalid_terminal_sourceневерные, отсутствующие или повторённые query parameters
stream400invalid_event_cursorповторённый или слишком длинный Last-Event-ID
stream404terminal_source_not_foundexact source сейчас не активен
stream429too_many_event_subscriptionsуже открыты 3 streams пользователя; Retry-After: 5
оба429api_rate_limitedпревышен API-key rate limit

Если ошибка возникает до начала SSE response, backend возвращает обычный JSON error envelope. После 200 text/event-stream разрыв соединения обнаруживается как EOF/network exception и обрабатывается reconnect logic.

TradingMonitor.Pro documentation.