Skip to content

Сигналы и статус исполнения ​

POST /api/v1/signals ​

Отправляет одну торговую или управляющую команду. Требует scope signals:write.

Обязательные заголовки ​

http
Authorization: Bearer tmpro_api_<secret>
Content-Type: application/json
Idempotency-Key: <unique-value>

Idempotency-Key должен иметь длину 1–128 символов и не содержать управляющих символов. Для новой логической операции используйте новый UUID.

Envelope сигнала ​

ПолеТипОбязательноеОграничения и смысл
vintegerдатолько 1
signalIdstringдауникальный в аккаунте, 1–128 символов
tsintegerдаUnix milliseconds; не позже допустимого clock skew
ttlMsintegerда1..300000; ts + ttlMs должен быть в будущем
platformstringдаодин или несколько comma-separated tokens
instanceIdstringнетточный terminal instance, 1–128 символов
typestringданепустая строка до 64 символов
cmdstringданепустая строка до 64 символов
payloadobjectдапараметры, которые проверяет целевой terminal/integration

Поддерживаемые platform tokens:

TokenНазначение
ninjatraderNinjaTrader tracked terminal
mt5MetaTrader tracked terminal
anyNinjaTrader и MetaTrader
telegramTelegram notification
discordDiscord notification
n8nn8n webhook

Tokens можно объединять, например "ninjatrader,telegram".

Если instanceId отсутствует, backend направляет tracked-команду всем совместимым активным terminals выбранного типа. Если instanceId указан, совпадение точное и чувствительное к регистру. Чтобы адресовать несколько компьютеров независимо, задайте уникальный InstanceId в каждом индикаторе.

Пустой instanceId

В signal body нельзя передавать "instanceId": "": validator отклонит его. Для маршрутизации без точного instance просто не добавляйте поле instanceId.

Lifecycle correlation ​

Для lifecycle-коррелированного order.submit в payload передаются стабильные поля:

json
{
  "lifecycleKey": "life-001",
  "entrySignalId": "entry-001"
}

Первоначальный order.submit регистрирует в Backend correlation record. Последующие обновления защиты могут использовать новый delivery signalId, но обязаны сохранять ту же пару lifecycleKey + entrySignalId; при обновлении также передаётся protectionUpdate: true.

После регистрации correlation terminal connector публикует lifecycle-события через SignalR Hub method PublishPositionLifecycle. Поддерживаются position.opened, position.closed, order.cancelled и order.expired. Для entry-команды с dynamic protection, если событие пришло до предварительного order.submit и регистрации ожидаемой корреляции, Backend вернёт success=false, status=NotFound. События position.opened для обычных ордеров без dynamic protection не образуют dynamic-сессию и игнорируются MainApp в контексте dynamic protection.

HTTP-ответ 202 Accepted означает только прием сигнала Public API. ACK lifecycle приходит отдельно от terminal connector и содержит success, status, state и message. При повторной доставке с тем же eventId Backend возвращает success=true, status=Duplicate.

Пример market order ​

bash
export TMPRO_API_KEY='tmpro_api_REPLACE_ME'
TS=$(date +%s%3N)
IDEMPOTENCY_KEY=$(uuidgen)

curl --request POST 'https://tradingmonitor.pro/api/v1/signals' \
  --header "Authorization: Bearer $TMPRO_API_KEY" \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: $IDEMPOTENCY_KEY" \
  --data "{
    \"v\": 1,
    \"signalId\": \"entry-$IDEMPOTENCY_KEY\",
    \"ts\": $TS,
    \"ttlMs\": 15000,
    \"platform\": \"ninjatrader\",
    \"instanceId\": \"VPS-NT8-01\",
    \"type\": \"order\",
    \"cmd\": \"submit\",
    \"payload\": {
      \"instrument\": \"ES 09-26\",
      \"side\": \"buy\",
      \"executionType\": \"market\",
      \"quantity\": 1,
      \"stopLoss\": 6280.25,
      \"takeProfit\": 6310.25
    }
  }"
csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

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

var signal = new
{
    v = 1,
    signalId = $"entry-{Guid.NewGuid():N}",
    ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
    ttlMs = 15_000,
    platform = "ninjatrader",
    instanceId = "VPS-NT8-01",
    type = "order",
    cmd = "submit",
    payload = new
    {
        instrument = "ES 09-26",
        side = "buy",
        executionType = "market",
        quantity = 1,
        stopLoss = 6280.25,
        takeProfit = 6310.25
    }
};

using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/signals")
{
    Content = JsonContent.Create(signal)
};
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());

using var response = await http.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine($"{(int)response.StatusCode} {body}");
Console.WriteLine($"Location: {response.Headers.Location}");
Console.WriteLine(
    $"Replayed: {response.Headers.TryGetValues("Idempotency-Replayed", out var values) && values.Single() == "true"}");
python
import time
import uuid
import requests

api_key = "tmpro_api_REPLACE_ME"
signal = {
    "v": 1,
    "signalId": f"entry-{uuid.uuid4().hex}",
    "ts": int(time.time() * 1000),
    "ttlMs": 15_000,
    "platform": "ninjatrader",
    "instanceId": "VPS-NT8-01",
    "type": "order",
    "cmd": "submit",
    "payload": {
        "instrument": "ES 09-26",
        "side": "buy",
        "executionType": "market",
        "quantity": 1,
        "stopLoss": 6280.25,
        "takeProfit": 6310.25,
    },
}

response = requests.post(
    "https://tradingmonitor.pro/api/v1/signals",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json=signal,
    timeout=30,
)
print(response.status_code, response.text)
print("Location:", response.headers.get("Location"))
print("Replayed:", response.headers.get("Idempotency-Replayed"))

Успешный ответ 202 Accepted:

json
{
  "requestId": "req_or_32_hex_characters",
  "signalId": "entry-0fc57b2cd72c4791a55c623c523c88c2",
  "status": "accepted",
  "acceptedAt": "2026-07-16T12:00:00Z"
}

Также возвращаются:

http
Location: /api/v1/signals/<requestId>
Idempotency-Replayed: false

accepted означает, что backend выполнил account/quota checks, создал запись запроса и передал сигнал в доступный pipeline. Проверяйте terminalRouting и executions через status endpoint.

Безопасный retry ​

При network timeout отправьте byte-identical JSON с тем же Idempotency-Key.

  • тот же key + то же тело: повторный 202, тот же requestId, Idempotency-Replayed: true;
  • тот же key + другое тело: 409 idempotency_conflict;
  • исходная операция ещё обрабатывается: 409 request_in_progress, обычно Retry-After: 2;
  • outcome исходной операции неизвестен: 409 idempotency_outcome_unknown;
  • новый idempotency key с уже использованным signalId: 409 duplicate_signal_id.

При idempotency_outcome_unknown не создавайте новый key для той же торговой команды. Сначала вручную проверьте terminal и broker account.

GET /api/v1/signals/{requestId} ​

Возвращает backend status, routing и executions. Требует scope signals:read.

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

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

using var response = await http.GetAsync($"/api/v1/signals/{requestId}");
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
python
import requests

request_id = "REPLACE_WITH_REQUEST_ID"
response = requests.get(
    f"https://tradingmonitor.pro/api/v1/signals/{request_id}",
    headers={"Authorization": "Bearer tmpro_api_REPLACE_ME"},
    timeout=30,
)
print(response.status_code, response.text)

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

json
{
  "requestId": "3b080d01b59448d59c3e1d9b93725378",
  "signalId": "entry-f06d4948101a42df9a6e0555ad691810",
  "status": "accepted",
  "createdAt": "2026-07-16T06:39:57.0420248Z",
  "completedAt": "2026-07-16T06:39:57.0507129Z",
  "terminalRouting": {
    "status": "dispatched",
    "targetCount": 1
  },
  "executions": [
    {
      "executionId": "exec_3ef5c67ee80d4638b5139b83b331d5a3",
      "orderRef": "ord_c05a69ac94f948c0997c661d83d2a2b1",
      "platform": "ninjatrader",
      "instanceId": "VPS-NT8-01",
      "status": "broker_submitted",
      "filledQuantity": null,
      "averageFillPrice": null,
      "reasonCode": null,
      "createdAt": "2026-07-16T06:39:57.0465139Z",
      "updatedAt": "2026-07-16T06:39:57.3957111Z",
      "completedAt": null,
      "cancel": null
    }
  ]
}

Top-level status ​

StatusЗначение
processingbackend ещё не зафиксировал outcome
acceptedbackend принял запрос
unknownbackend не смог надёжно подтвердить outcome; автоматически не повторять

terminalRouting.status ​

StatusЗначение
not_applicablesignal не содержит terminal route, например только messenger route
no_compatible_terminalнет активного terminal с необходимыми capabilities и подходящим instance
dispatchedtracked envelope отправлен одному или нескольким terminals

targetCount — число созданных terminal executions.

executions[].status ​

Обычная последовательность entry order:

text
dispatched
  -> terminal_accepted
  -> broker_submitted
  -> partially_filled
  -> filled | rejected | cancelled

Все возможные execution statuses:

StatusФинальныйЗначение
dispatchedнетbackend отправил tracked command
terminal_acceptedнетterminal принял команду в обработку
broker_submittedнетордер отправлен broker/adapter
partially_filledнетчастичное исполнение
filledдаордер исполнен
rejectedдаterminal или broker отклонил команду
cancelledдаtracked order отменён
not_executedдакоманда не была выполнена
terminal_completedдаnon-order команда подтверждена terminal state updates
not_confirmedусловно финальныйподтверждение не получено до timeout

После reconnect terminal может восстановить correlation и заменить not_confirmed на достоверный broker_submitted, partially_filled или финальный status.

Значение полей execution ​

ПолеОписание
executionIdbackend correlation конкретного terminal execution
orderRefopaque reference entry order; используется для exact cancel
platformninjatrader или mt5
instanceIdterminal instance, которому отправлена команда
filledQuantityподтверждённое terminal количество исполнения
averageFillPriceподтверждённая средняя цена
reasonCodeстабильный machine-readable reason, если доступен
cancelstatus последнего cancel request для этого execution

orderRef создаётся только для type=order, cmd=submit. Не пытайтесь заменить его NinjaTrader OrderId.

Противоположные сигналы и итоговая позиция ​

Public API и обычные сигналы являются независимыми командами. Backend и terminal plugins не выполняют общий автоматический арбитраж противоположных сигналов по одной паре account + instrument. Команды с разными signalId могут быть приняты и отправлены в терминал, даже если их направления конфликтуют.

В одном remote-плагине обычные и Public API-сигналы обрабатываются последовательно, но очередь не превращает несколько команд в атомарную операцию разворота. Ранее выставленный встречный pending-ордер автоматически не отменяется. Ручные заявки, TMProLocal, ATM, другие EA, стратегии и копировщики являются отдельными источниками и с этой очередью не координируются.

Статус execution filled подтверждает исполнение конкретного API-tracked ордера. Он не гарантирует, что текущая позиция счёта сохранилась в направлении этого ордера:

  • на NinjaTrader или MT5 netting account встречный объём может уменьшить, закрыть или развернуть общую позицию;
  • на MT5 hedging account встречный вход может создать отдельную противоположную позицию вместо закрытия существующей;
  • partial fills, разные объёмы, pending-ордера, ручные операции и другие торговые системы могут изменить итоговое состояние.

Exact cancel отменяет только выбранный orderRef. Он не отменяет другие встречные, ручные, ATM/EA или защитные ордера и не закрывает уже открытую позицию. Перенос Stop Loss/Take Profit между пересекающимися противоположными сигналами не является атомарным или гарантированным.

Один управляющий источник

Для одной пары account + instrument рекомендуется использовать либо Public API, либо обычные сигналы, но не оба источника одновременно. Это относится также к TMProLocal, ручным заявкам, ATM, другим стратегиям, EA и копировщикам.

Использование только одного источника уменьшает межисточниковые гонки, но не защищает от двух перекрывающихся противоположных сигналов внутри самого этого источника.

Рекомендуемый порядок безопасного разворота:

  1. Не отправляйте встречную команду до окончательного terminal-confirmed результата предыдущей.
  2. Сверьте текущую позицию и все working entry/protection orders.
  3. Отмените каждый конфликтующий pending-ордер отдельно и дождитесь финального результата отмены.
  4. Закройте существующую позицию и подтвердите отсутствие позиции в фактическом состоянии терминала.
  5. Только после этого отправьте новый entry и установите новую защиту.
  6. После ручного вмешательства приостановите автоматические сигналы и полностью сверяйте состояние перед возобновлением.

Проверяйте этот сценарий, включая partial fills и защитные ордера, на отдельном demo/simulation account. Для MT5 netting и hedging accounts выполняйте отдельные проверки.

Polling до финального результата ​

Не опрашивайте endpoint в tight loop. Практический интервал — 500–2000 ms, затем backoff.

bash
REQUEST_ID='REPLACE_WITH_REQUEST_ID'

for attempt in $(seq 1 60); do
  body=$(
    curl --silent --show-error \
      "https://tradingmonitor.pro/api/v1/signals/$REQUEST_ID" \
      --header 'Authorization: Bearer tmpro_api_REPLACE_ME'
  )

  echo "$body" | jq .

  routing=$(echo "$body" | jq -r '.terminalRouting.status')
  execution_count=$(echo "$body" | jq '.executions | length')
  pending_count=$(
    echo "$body" |
      jq '[.executions[] | select(
        .status != "filled" and
        .status != "rejected" and
        .status != "cancelled" and
        .status != "not_executed" and
        .status != "terminal_completed" and
        .status != "not_confirmed"
      )] | length'
  )

  if [ "$routing" != 'dispatched' ] ||
     { [ "$execution_count" -gt 0 ] && [ "$pending_count" -eq 0 ]; }; then
    break
  fi

  sleep 1
done
csharp
using System.Linq;
using System.Net.Http.Headers;
using System.Text.Json;

var finalStatuses = new HashSet<string>(StringComparer.Ordinal)
{
    "filled",
    "rejected",
    "cancelled",
    "not_executed",
    "terminal_completed",
    "not_confirmed"
};

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

for (var attempt = 0; attempt < 60; attempt++)
{
    using var response = await http.GetAsync($"/api/v1/signals/{requestId}");
    response.EnsureSuccessStatusCode();

    var text = await response.Content.ReadAsStringAsync();
    using var json = JsonDocument.Parse(text);
    var root = json.RootElement;
    var routing = root.GetProperty("terminalRouting").GetProperty("status").GetString();
    var executions = root.GetProperty("executions").EnumerateArray().ToArray();

    Console.WriteLine(text);

    if (routing != "dispatched" ||
        (executions.Length > 0 &&
         executions.All(item =>
             finalStatuses.Contains(item.GetProperty("status").GetString() ?? ""))))
    {
        break;
    }

    await Task.Delay(TimeSpan.FromSeconds(1));
}
python
import time
import requests

FINAL = {
    "filled",
    "rejected",
    "cancelled",
    "not_executed",
    "terminal_completed",
    "not_confirmed",
}

url = "https://tradingmonitor.pro/api/v1/signals/REPLACE_WITH_REQUEST_ID"
headers = {"Authorization": "Bearer tmpro_api_REPLACE_ME"}

for _ in range(60):
    response = requests.get(url, headers=headers, timeout=30)
    response.raise_for_status()
    body = response.json()
    routing = body["terminalRouting"]["status"]
    executions = body.get("executions", [])

    print(body)
    if routing != "dispatched" or (
        executions and all(item["status"] in FINAL for item in executions)
    ):
        break

    time.sleep(1)

terminalRouting.status=no_compatible_terminal является завершённым routing result: terminal execution не будет создан. not_applicable означает, что signal не содержал route в NinjaTrader/MT5, например был направлен только в messenger.

Каталог команд ​

Backend проверяет общий envelope, а фактические type, cmd и поля payload проверяет целевой terminal plugin. Поэтому HTTP 202 Accepted возможен и для команды, которую terminal затем завершит статусом rejected или not_executed.

Полный каталог всех поддерживаемых команд, матрица NinjaTrader 8/MetaTrader 5, обязательные поля и примеры curl, C# и Python для каждого варианта находятся на странице Справочник команд.

Кратко:

ГруппаКоманды
systempause, resume, info
ordersubmit с market, limit, stop; NT8 также stop_limit
positionclose, reduce, protect, close_all, flatten_now
riskblock_instrument, allow_instrument, flatten_now

Ошибки signal endpoints ​

Полный справочник находится на странице Ошибки, retries и лимиты.

Частые ответы:

HTTPCode
400invalid_idempotency_key, malformed_json
401invalid_api_key
403insufficient_scope, account_frozen
409idempotency_conflict, request_in_progress, idempotency_outcome_unknown, duplicate_signal_id
413request_body_too_large
415unsupported_media_type
422invalid_signal
429api_rate_limited, signal_rate_limit_exceeded, daily_signal_limit_exceeded
500signal_processing_failed

TradingMonitor.Pro documentation.