Appearance
Dynamic Stop Loss / Take Profit
Эта страница описывает общий контракт между MainApp, Backend и торговыми терминалами в части динамических Stop Loss и Take Profit.
Где разрешены динамические источники
Динамические источники доступны только для сигналов категории Price / Indicator. Для сигналов Screen / OCR они не применяются, и отправлять их не следует.
В качестве источника можно выбрать:
Open,High,LowилиClose;- значение индикаторной колонки, например
IndHighилиIndLow.
Volume и его краткое обозначение V запрещены в качестве источников динамической защиты. Поле DOHLCV.V может присутствовать в диагностическом OHLC-пакете, однако это не означает, что Volume разрешено использовать для Stop Loss или Take Profit.
Три варианта для каждой стороны защиты
Для Stop Loss и Take Profit доступны три взаимоисключающих варианта:
- абсолютная цена:
Stop LossилиTake Profit; - расстояние в тиках:
Stop Loss TicksилиTake Profit Ticks; - динамический источник:
Stop Loss SourceилиTake Profit Source.
Для каждой стороны допускается не более одного варианта. Stop Loss и Take Profit задаются независимо друг от друга: можно указать только Stop Loss, только Take Profit или обе стороны сразу.
Динамический источник допустим только для order.submit и не используется в position.protect: защита уже открытой позиции через position.protect должна передаваться в виде уже вычисленного числа или количества тиков.
Что делает MainApp
MainApp вычисляет значения источников на основе текущего снимка данных Price / Indicator. Терминальный плагин получает уже готовые числовые значения и не выполняет повторный расчет индикаторов.
Для динамического входа MainApp отправляет один первоначальный order.submit:
json
{
"signalId": "delivery-entry-001",
"type": "order",
"cmd": "submit",
"payload": {
"instrument": "ES 09-26",
"side": "buy",
"executionType": "market",
"quantity": 1,
"stopLoss": 6280.25,
"lifecycleKey": "life-001",
"entrySignalId": "delivery-entry-001"
}
}Если изменился только источник Take Profit, MainApp отправляет отдельное обновление с новым signalId, но с сохранением тех же lifecycleKey и entrySignalId:
json
{
"signalId": "delivery-protection-002",
"type": "order",
"cmd": "submit",
"payload": {
"instrument": "ES 09-26",
"side": "buy",
"executionType": "market",
"quantity": 1,
"takeProfit": 6310.25,
"lifecycleKey": "life-001",
"entrySignalId": "delivery-entry-001",
"protectionUpdate": true
}
}Правила обновлений:
- каждое новое обновление получает новый delivery
signalId; lifecycleKeyостается неизменным на протяжении всей операции входа;entrySignalIdпо-прежнему указывает на первоначальный вход;- передается только та сторона защиты, которая изменилась;
- отсутствие
Stop LossилиTake Profitв обновлении не означает удаления второй стороны; - после получения
position.closed,order.cancelledилиorder.expiredMainApp прекращает отправку обновлений по этойlifecycle-сессии.
Lifecycle-корреляция
Первоначальный динамический order.submit регистрирует в Backend корреляцию:
LifecycleKey + EntrySignalId
Dynamic-сессия создаётся только для entry-команды, в которой включена dynamic protection. Для такой сессии lifecycleKey и entrySignalId остаются неизменными: событие position.opened активирует соответствующую dynamic-сессию, а последующие dynamic updates используют тот же lifecycleKey.
Lifecycle-события position.opened могут приходить и для обычных ордеров без dynamic protection. MainApp игнорирует такие события в контексте dynamic protection — это не ошибка. Ошибкой считается только невозможность сопоставить событие с ожидаемой dynamic-сессией, если такая сессия была создана для entry-команды.
Для entry-команды с dynamic protection terminal не должен отправлять lifecycle-событие до регистрации соответствующей корреляции. Если Backend не находит ожидаемую запись, он возвращает:
json
{
"success": false,
"status": "NotFound",
"message": "No registered lifecycle was found."
}Для передачи lifecycle-сообщений используется метод SignalR Hub — PublishPositionLifecycle. Метод возвращает ACK-объект с полями success, status, state и message.
Значение platform в payload должно соответствовать терминалу:
| Terminal | platform | Hub client type |
|---|---|---|
| MetaTrader 5 | mt5 | MetaTrader |
| NinjaTrader 8 | ninjatrader | NinjaTrader |
terminalInstanceId должен точно совпадать с зарегистрированным Instance ID, а accountName и instrument — с account и symbol/instrument, для которых была создана корреляция.
Минимальный lifecycle payload:
json
{
"eventId": "event-001",
"eventType": "position.opened",
"platform": "mt5",
"terminalInstanceId": "MT5-DEMO-01",
"accountName": "Demo",
"instrument": "EURUSD",
"positionRef": "position-001",
"entrySignalId": "delivery-entry-001",
"lifecycleKey": "life-001",
"sequence": 1,
"timestamp": 1785060000000
}Backend дополнительно проверяет, что lifecycle принадлежит нужному пользователю, terminal instance, платформе, account и instrument.
Для события position.opened Backend возвращает state: "active" — это внутреннее состояние Backend, и терминалу достаточно проверять success и status. Для полного закрытия lifecycle используется событие position.closed.
Пример успешного ACK:
json
{
"success": true,
"status": "OK",
"state": "active",
"message": null
}Правила обработки ACK:
OK— событие принято;Duplicate,StaleSequence,TerminalState— безопасный результат для повторной или устаревшей доставки; повторно отправлять событие не нужно;NotFound,EntrySignalMismatch,TerminalInstanceMismatch,PlatformMismatch,AccountMismatch,InstrumentMismatch,UnauthorizedTerminal,InvalidPayload,Disabled— свидетельствуют об ошибке контракта или регистрации, а не о поводе повторить тот жеpayload.
Для MT5-коннектора необходим экспорт DLL PublishPositionLifecycle и вызов Hub-метода с нормализованным lifecycle payload. Если экспорт отсутствует, outbox в MQL5 не сможет доставить событие в Backend.
Для NinjaTrader 8 текущий indicator package уже реализует lifecycle outbox: он обрабатывает PositionUpdate/ExecutionUpdate и вызывает PublishPositionLifecycle с capability position-lifecycle-v1. Об успешной доставке можно судить по записи feedback sent в логе. В payload используется каноническое значение platform: "ninjatrader"; Backend также принимает nt8.
Поведение при закрытии
MainApp получает от Backend событие PositionStateChanged и сопоставляет его сначала по lifecycleKey, а затем — как совместимый fallback — по entrySignalId или positionRef. Это сопоставление применяется к ожидаемой dynamic-сессии; события position.opened от обычных ордеров без dynamic protection игнорируются. При наступлении position.closed, order.cancelled или order.expired динамическая сессия удаляется, поэтому последующие изменения индикаторов больше не порождают команд.
Backend deployment
Production Backend работает из:
/var/www/signalr-app2
В этой же папке находятся:
SSNew.dllи runtime-файлы;ssnew.db;ssnew-backend.env;GeoLite2-Country.mmdb.
Скрипт deploy.ps1 сохраняет файлы *.db, *.db-shm, *.db-wal и *.env при синхронизации. Режим -Fast передает только основные файлы приложения (SSNew.dll, apphost, deps/runtime JSON и production-конфигурацию), не копируя DLL-зависимости и не удаляя прочие файлы на сервере.