//+------------------------------------------------------------------+ //| qubitrino_bridge.mq5 | //| Мост MT5 → Qubitrino, советник потока B | //| Контракт v2 (ADR-0118/0119, D-087) | //+------------------------------------------------------------------+ #property copyright "Qubitrino" #property link "https://qubitrino.ai" #property version "1.00" #property description "Шлёт торговые события счёта на Qubitrino. Не торгует." #property description "Streams account trade events to Qubitrino. No trading." #property description "Токен выдаётся на платформе / Token is issued by the platform." #define EA_VERSION "0.3.2" #define CONTRACT_VERSION 2 #define QUEUE_MAX 1000 #define BACKOFF_MAX_SEC 30 #define RETRY_AFTER_SEC 3 #define HTTP_TIMEOUT_MS 5000 //--- входные параметры input string InpBridgeUrl = "https://qubitrino.ai/api/mt5/v1/ingest/"; // URL приёмного API / Ingest API URL input string InpToken = ""; // Токен моста (выдаёт платформа) / Bridge token (from the platform) input int InpSnapshotSec = 60; // Период снимков и heartbeat, сек / Snapshot & heartbeat period, sec input int InpActivityIdleMin = 10; // Без тиков дольше — heartbeat / No ticks longer — heartbeat mode input bool InpLogOnly = false; // Только писать JSON в журнал / Log JSON only, do not send //--- состояние отправки enum ESEND { SEND_OK = 0, // принято (200, в т.ч. duplicate) SEND_RETRY = 1, // временный сбой: сеть, 429, 5xx SEND_DROP = 2, // 400: наше сообщение браковано — не повторять SEND_FATAL = 3 // 401/403/426: стоп до действия пользователя }; string g_session_id = ""; long g_seq = 0; string g_queue[]; int g_qn = 0; datetime g_next_attempt = 0; int g_fail_streak = 0; bool g_frozen = false; bool g_need_recovered = true; // первый снимок после OnInit — recovered bool g_force_snapshot = false; // сервер ответил gap — снимок вне очереди кадров datetime g_next_snapshot = 0; datetime g_started_at = 0; datetime g_last_deposit_at = 0; bool g_last_deposit_known = false; //--- кэш SL/TP открытых позиций (для диффа sltp_set/move/remove) ulong g_cticket[]; double g_csl[]; double g_ctp[]; int g_cn = 0; //+------------------------------------------------------------------+ //| Утилиты | //+------------------------------------------------------------------+ string JsonEscape(const string s) { string r = s; StringReplace(r, "\\", "\\\\"); StringReplace(r, "\"", "\\\""); StringReplace(r, "\r", ""); StringReplace(r, "\n", " "); return r; } bool IsRu() { return StringFind(TerminalInfoString(TERMINAL_LANGUAGE), "Russian") == 0; } string L(const string ru, const string en) { return IsRu() ? ru : en; } string Num(const double v) { string s = DoubleToString(v, 8); while(StringGetCharacter(s, StringLen(s) - 1) == '0') s = StringSubstr(s, 0, StringLen(s) - 1); if(StringGetCharacter(s, StringLen(s) - 1) == '.') s = StringSubstr(s, 0, StringLen(s) - 1); return s; } string Money(const double v) { return DoubleToString(v, 2); } string IsoTime(const datetime t) { MqlDateTime st; TimeToStruct(t, st); return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d", st.year, st.mon, st.day, st.hour, st.min, st.sec); } string DirectionFromDealType(const long dtype) { if(dtype == DEAL_TYPE_BUY) return "buy"; if(dtype == DEAL_TYPE_SELL) return "sell"; return ""; } string DirectionFromPositionType(const long ptype) { if(ptype == POSITION_TYPE_BUY) return "buy"; if(ptype == POSITION_TYPE_SELL) return "sell"; return ""; } ulong FirstEntryDeal(const ulong position_id) { if(!HistorySelectByPosition(position_id)) return 0; ulong found = 0; datetime found_time = 0; int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong deal = HistoryDealGetTicket(i); if(deal == 0 || HistoryDealGetInteger(deal, DEAL_ENTRY) != DEAL_ENTRY_IN) continue; datetime at = (datetime)HistoryDealGetInteger(deal, DEAL_TIME); if(found == 0 || at < found_time) { found = deal; found_time = at; } } return found; } string DirectionFromPosition(const ulong position_id) { if(PositionSelectByTicket(position_id)) return DirectionFromPositionType(PositionGetInteger(POSITION_TYPE)); ulong entry = FirstEntryDeal(position_id); if(entry == 0) return ""; return DirectionFromDealType(HistoryDealGetInteger(entry, DEAL_TYPE)); } string OpenFactsFromPosition(const ulong position_id) { string out = ""; datetime opened_at = 0; double open_price = 0.0; if(PositionSelectByTicket(position_id)) { opened_at = (datetime)PositionGetInteger(POSITION_TIME); open_price = PositionGetDouble(POSITION_PRICE_OPEN); } else { ulong entry = FirstEntryDeal(position_id); if(entry > 0) { opened_at = (datetime)HistoryDealGetInteger(entry, DEAL_TIME); open_price = HistoryDealGetDouble(entry, DEAL_PRICE); } } if(opened_at > 0) out = out + ",\"open_time\":\"" + IsoTime(opened_at) + "\""; if(open_price > 0.0) out = out + ",\"open_price\":" + Num(open_price); return out; } string OccurredField(const ulong deal_ticket) { datetime at = (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); if(at <= 0) return ""; return ",\"occurred_at\":\"" + IsoTime(at) + "\""; } string PointField(const string symbol) { double point = SymbolInfoDouble(symbol, SYMBOL_POINT); if(point <= 0.0) return ""; return ",\"point\":" + Num(point); } string NullablePriceField(const string name, const double value) { if(value > 0.0) return ",\"" + name + "\":" + Num(value); return ",\"" + name + "\":null"; } string LastDepositField() { if(!g_last_deposit_known) return ""; if(g_last_deposit_at == 0) return ",\"last_deposit_at\":null"; return ",\"last_deposit_at\":\"" + IsoTime(g_last_deposit_at) + "\""; } void SeedLastDeposit() { g_last_deposit_known = false; g_last_deposit_at = 0; if(!HistorySelect(0, TimeTradeServer() + 60)) return; int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong deal = HistoryDealGetTicket(i); if(deal == 0 || HistoryDealGetInteger(deal, DEAL_TYPE) != DEAL_TYPE_BALANCE) continue; if(HistoryDealGetDouble(deal, DEAL_PROFIT) <= 0.0) continue; datetime at = (datetime)HistoryDealGetInteger(deal, DEAL_TIME); if(at > g_last_deposit_at) g_last_deposit_at = at; } g_last_deposit_known = true; } int TzOffsetMin() { // Пояс сервера брокера в минутах от UTC; сервер контракта не угадывает. return (int)((TimeTradeServer() - TimeGMT()) / 60); } string NewUuid() { // UUIDv4: 8-4-4-4-12 hex; версия и вариант по RFC 4122. string out = ""; int groups[5] = {8, 4, 4, 4, 12}; int pos = 0; for(int g = 0; g < 5; g++) { for(int i = 0; i < groups[g]; i++) { int nibble = MathRand() % 16; if(pos == 12) nibble = 4; // версия else if(pos == 16) nibble = 8 + MathRand() % 4; // вариант 10xx out = out + StringFormat("%x", nibble); pos++; } if(g < 4) out = out + "-"; } return out; } string CommonFieldsWithId(const string msg_type, const string event_id) { string s = "\"v\":" + IntegerToString(CONTRACT_VERSION); s = s + ",\"event_id\":\"" + event_id + "\""; s = s + ",\"session_id\":\"" + g_session_id + "\""; s = s + ",\"seq\":" + IntegerToString(g_seq); s = s + ",\"sent_at\":\"" + IsoTime(TimeTradeServer()) + "\""; s = s + ",\"tz_offset\":" + IntegerToString(TzOffsetMin()); s = s + ",\"type\":\"" + msg_type + "\""; s = s + ",\"ea_version\":\"" + EA_VERSION + "\""; g_seq++; return s; } string CommonFields(const string msg_type) { return CommonFieldsWithId(msg_type, NewUuid()); } string StableEventId(const int kind, const ulong ticket, const datetime at) { // Повторный backfill/перезапуск создаёт те же UUID, а не дубликаты. return StringFormat("%08x-%04x-4000-8000-%012x", kind, (uint)(at & 0xFFFF), ticket); } //+------------------------------------------------------------------+ //| Очередь исходящих сообщений | //+------------------------------------------------------------------+ void Enqueue(const string json) { if(g_qn >= QUEUE_MAX) { // Переполнение = события теряются: честно помечаем следующий снимок recovered. ArrayRemove(g_queue, 0, 1); g_qn--; g_need_recovered = true; Print(L("bridge: очередь переполнена — старейшее сообщение сброшено", "bridge: queue overflow — oldest message dropped")); } ArrayResize(g_queue, g_qn + 1); g_queue[g_qn] = json; g_qn++; } //+------------------------------------------------------------------+ //| Отправка одного сообщения | //+------------------------------------------------------------------+ ESEND SendRaw(const string json) { if(InpLogOnly) { Print("bridge [log-only]: ", json); return SEND_OK; } char data[]; char result[]; StringToCharArray(json, data, 0, StringLen(json), CP_UTF8); string headers = "Content-Type: application/json\r\n" "Authorization: Bearer " + InpToken + "\r\n"; string result_headers = ""; ResetLastError(); int code = WebRequest("POST", InpBridgeUrl, headers, HTTP_TIMEOUT_MS, data, result, result_headers); if(code == -1) { Print(L("bridge: WebRequest ошибка ", "bridge: WebRequest error "), GetLastError(), L(" (URL должен быть в списке разрешённых: Сервис → Настройки → Советники)", " (URL must be allowed: Tools → Options → Expert Advisors)")); return SEND_RETRY; } string body = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); if(code == 200) { if(StringFind(body, "\"gap\":{") >= 0) { g_force_snapshot = true; Print(L("bridge: сервер зафиксировал пропуск seq — шлю снимок состояния", "bridge: server recorded a seq gap — sending state snapshot")); } return SEND_OK; } if(code == 429) { Print(L("bridge: 429 rate_limited — пауза ", "bridge: 429 rate_limited — pause "), RETRY_AFTER_SEC, " s"); return SEND_RETRY; } if(code == 400) { // 400 = наше сообщение не прошло схему: повторять бессмысленно. Print(L("bridge: 400 отказ: ", "bridge: 400 rejected: "), body, L(" — сообщение сброшено (дефект советника)", " — message dropped (EA defect)")); return SEND_DROP; } if(code == 401 || code == 403 || code == 426) { Print("bridge: ", code, " ", body, L(" — мост остановлен: проверьте токен (401), доступ (403), версию (426)", " — bridge stopped: check token (401), access (403), version (426)")); return SEND_FATAL; } Print("bridge: HTTP ", code, L(" — повтор позже", " — will retry")); return SEND_RETRY; } void FlushQueue() { if(g_qn == 0 || g_frozen) return; if(TimeCurrent() < g_next_attempt) return; ESEND res = SendRaw(g_queue[0]); if(res == SEND_OK) { ArrayRemove(g_queue, 0, 1); g_qn--; g_fail_streak = 0; g_next_attempt = 0; Comment(L("Qubitrino bridge: доставлено, очередь ", "Qubitrino bridge: delivered, queue ") + IntegerToString(g_qn) + " | " + TimeToString(TimeCurrent(), TIME_SECONDS)); } else if(res == SEND_DROP) { ArrayRemove(g_queue, 0, 1); g_qn--; g_need_recovered = true; } else if(res == SEND_RETRY) { g_fail_streak++; int shift = g_fail_streak; if(shift > 5) shift = 5; int delay = 1 << shift; // 2,4,8,16,32… → ограничим if(delay > BACKOFF_MAX_SEC) delay = BACKOFF_MAX_SEC; g_next_attempt = TimeCurrent() + delay; Comment(L("Qubitrino bridge: нет связи, повтор через ", "Qubitrino bridge: no link, retry in ") + IntegerToString(delay) + L(" с, очередь ", " s, queue ") + IntegerToString(g_qn)); } else // SEND_FATAL { g_frozen = true; Comment(L("Qubitrino bridge: ОСТАНОВЛЕН — смотрите журнал «Эксперты»", "Qubitrino bridge: STOPPED — see the Experts log")); EventKillTimer(); } } //+------------------------------------------------------------------+ //| Кэш SL/TP | //+------------------------------------------------------------------+ int CacheFind(const ulong ticket) { for(int i = 0; i < g_cn; i++) if(g_cticket[i] == ticket) return i; return -1; } void CachePut(const ulong ticket, const double sl, const double tp) { int idx = CacheFind(ticket); if(idx < 0) { idx = g_cn; ArrayResize(g_cticket, g_cn + 1); ArrayResize(g_csl, g_cn + 1); ArrayResize(g_ctp, g_cn + 1); g_cn++; } g_cticket[idx] = ticket; g_csl[idx] = sl; g_ctp[idx] = tp; } void CacheRemove(const ulong ticket) { int idx = CacheFind(ticket); if(idx < 0) return; g_cticket[idx] = g_cticket[g_cn - 1]; g_csl[idx] = g_csl[g_cn - 1]; g_ctp[idx] = g_ctp[g_cn - 1]; ArrayResize(g_cticket, g_cn - 1); ArrayResize(g_csl, g_cn - 1); ArrayResize(g_ctp, g_cn - 1); g_cn--; } void SeedCache() { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; CachePut(ticket, PositionGetDouble(POSITION_SL), PositionGetDouble(POSITION_TP)); } } //+------------------------------------------------------------------+ //| День/неделя по времени брокера | //+------------------------------------------------------------------+ datetime DayStart() { MqlDateTime st; TimeToStruct(TimeTradeServer(), st); st.hour = 0; st.min = 0; st.sec = 0; return StructToTime(st); } datetime WeekStart() { MqlDateTime st; TimeToStruct(TimeTradeServer(), st); st.hour = 0; st.min = 0; st.sec = 0; datetime today0 = StructToTime(st); int back = (st.day_of_week == 0) ? 6 : st.day_of_week - 1; // понедельник return today0 - back * 86400; } //+------------------------------------------------------------------+ //| P/L | //+------------------------------------------------------------------+ bool RealizedParts(const datetime from, double &gross, double &commission, double &swap, double &fee) { gross = 0.0; commission = 0.0; swap = 0.0; fee = 0.0; if(!HistorySelect(from, TimeTradeServer() + 60)) return false; int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong deal = HistoryDealGetTicket(i); long dtype = HistoryDealGetInteger(deal, DEAL_TYPE); if(dtype != DEAL_TYPE_BUY && dtype != DEAL_TYPE_SELL) continue; // баланс/коррекции — не торговый результат gross += HistoryDealGetDouble(deal, DEAL_PROFIT); commission += HistoryDealGetDouble(deal, DEAL_COMMISSION); swap += HistoryDealGetDouble(deal, DEAL_SWAP); fee += HistoryDealGetDouble(deal, DEAL_FEE); } return true; } void FloatingParts(double &gross, double &swap) { gross = 0.0; swap = 0.0; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetTicket(i) == 0) continue; gross += PositionGetDouble(POSITION_PROFIT); swap += PositionGetDouble(POSITION_SWAP); } } bool PositionParts(const ulong position_id, const bool include_current, double &gross, double &commission, double &swap, double &fee) { gross = 0.0; commission = 0.0; swap = 0.0; fee = 0.0; if(!HistorySelectByPosition(position_id)) return false; int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong deal = HistoryDealGetTicket(i); gross += HistoryDealGetDouble(deal, DEAL_PROFIT); commission += HistoryDealGetDouble(deal, DEAL_COMMISSION); swap += HistoryDealGetDouble(deal, DEAL_SWAP); fee += HistoryDealGetDouble(deal, DEAL_FEE); } if(include_current && PositionSelectByTicket(position_id)) { gross += PositionGetDouble(POSITION_PROFIT); swap = PositionGetDouble(POSITION_SWAP); } return true; } double RealizedPnl(const datetime from) { double gross, commission, swap, fee; if(!RealizedParts(from, gross, commission, swap, fee)) return 0.0; return gross + swap + commission; // прежний net, fee отдельно } double FloatingPnl() { double gross, swap; FloatingParts(gross, swap); return gross + swap; // прежний floating_pnl } double PositionNetPnl(const ulong position_id) { // Итог всей позиции (все её сделки) — как строка «Позиции» вечернего отчёта. double gross, commission, swap, fee; if(!PositionParts(position_id, false, gross, commission, swap, fee)) return 0.0; return gross + swap + commission; // прежний net, fee отдельно } string PeriodFields(const string prefix, const datetime from) { double gross, commission, swap, fee; if(!RealizedParts(from, gross, commission, swap, fee)) return ""; double floating_gross, floating_swap; FloatingParts(floating_gross, floating_swap); string out = ",\"" + prefix + "_gross_pnl\":" + Money(gross + floating_gross); out = out + ",\"" + prefix + "_commission\":" + Money(commission); out = out + ",\"" + prefix + "_swap\":" + Money(swap + floating_swap); out = out + ",\"" + prefix + "_fee\":" + Money(fee); return out; } //+------------------------------------------------------------------+ //| События потока B | //+------------------------------------------------------------------+ void SendDealOpen(const ulong deal_ticket, const ulong position_id, const string symbol, const double volume, const double price) { string s = "{" + CommonFields("deal_open"); s = s + ",\"symbol\":\"" + JsonEscape(symbol) + "\""; s = s + ",\"volume\":" + Num(volume); s = s + ",\"price\":" + Num(price); s = s + ",\"ticket\":\"" + IntegerToString((long)position_id) + "\""; s = s + OccurredField(deal_ticket); string direction = DirectionFromDealType(HistoryDealGetInteger(deal_ticket, DEAL_TYPE)); if(direction != "") s = s + ",\"direction\":\"" + direction + "\""; s = s + PointField(symbol); if(PositionSelectByTicket(position_id)) s = s + ",\"position_volume\":" + Num(PositionGetDouble(POSITION_VOLUME)); s = s + ",\"positions_total\":" + IntegerToString(PositionsTotal()); s = s + NullablePriceField("open_sl", HistoryDealGetDouble(deal_ticket, DEAL_SL)); s = s + NullablePriceField("open_tp", HistoryDealGetDouble(deal_ticket, DEAL_TP)); s = s + LastDepositField(); Enqueue(s + "}"); } void SendDealClose(const ulong deal_ticket, const ulong position_id, const double volume, const double price) { string s = "{" + CommonFields("deal_close"); s = s + ",\"ticket\":\"" + IntegerToString((long)position_id) + "\""; s = s + ",\"volume\":" + Num(volume); s = s + ",\"price\":" + Num(price); s = s + ",\"net_pnl\":" + Money(PositionNetPnl(position_id)); s = s + OccurredField(deal_ticket); string direction = DirectionFromPosition(position_id); if(direction != "") s = s + ",\"direction\":\"" + direction + "\""; s = s + PointField(HistoryDealGetString(deal_ticket, DEAL_SYMBOL)); s = s + OpenFactsFromPosition(position_id); bool exists = PositionSelectByTicket(position_id); double remaining = exists ? PositionGetDouble(POSITION_VOLUME) : 0.0; s = s + ",\"remaining_volume\":" + Num(remaining); s = s + ",\"position_closed\":" + (exists ? "false" : "true"); double gross, commission, swap, fee; if(PositionParts(position_id, false, gross, commission, swap, fee)) { s = s + ",\"gross_pnl\":" + Money(gross); s = s + ",\"commission\":" + Money(commission); s = s + ",\"swap\":" + Money(swap); s = s + ",\"fee\":" + Money(fee); } Enqueue(s + "}"); } void SendSltp(const string msg_type, const ulong position_id, const double sl, const double tp, const double old_sl, const double old_tp) { string s = "{" + CommonFields(msg_type); s = s + ",\"ticket\":\"" + IntegerToString((long)position_id) + "\""; s = s + ",\"occurred_at\":\"" + IsoTime(TimeTradeServer()) + "\""; string direction = DirectionFromPosition(position_id); if(direction != "") s = s + ",\"direction\":\"" + direction + "\""; if(msg_type == "sltp_remove") { string removed = "both"; if(old_tp == 0.0) removed = "sl"; else if(old_sl == 0.0) removed = "tp"; s = s + ",\"removed\":\"" + removed + "\""; } else { s = s + ",\"sl\":" + (sl > 0.0 ? Num(sl) : "null"); s = s + ",\"tp\":" + (tp > 0.0 ? Num(tp) : "null"); s = s + ",\"prev_sl\":" + (old_sl > 0.0 ? Num(old_sl) : "null"); s = s + ",\"prev_tp\":" + (old_tp > 0.0 ? Num(old_tp) : "null"); } Enqueue(s + "}"); } void SendSnapshot(const bool recovered) { string arr = ""; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; string p = "{"; p = p + "\"ticket\":\"" + IntegerToString((long)ticket) + "\""; p = p + ",\"symbol\":\"" + JsonEscape(PositionGetString(POSITION_SYMBOL)) + "\""; p = p + ",\"volume\":" + Num(PositionGetDouble(POSITION_VOLUME)); p = p + ",\"open_price\":" + Num(PositionGetDouble(POSITION_PRICE_OPEN)); double sl = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); p = p + ",\"sl\":" + (sl > 0.0 ? Num(sl) : "null"); p = p + ",\"tp\":" + (tp > 0.0 ? Num(tp) : "null"); p = p + ",\"floating_pnl\":" + Money(PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP)); datetime opened_at = (datetime)PositionGetInteger(POSITION_TIME); if(opened_at > 0) p = p + ",\"open_time\":\"" + IsoTime(opened_at) + "\""; string direction = DirectionFromPositionType(PositionGetInteger(POSITION_TYPE)); if(direction != "") p = p + ",\"direction\":\"" + direction + "\""; p = p + PointField(PositionGetString(POSITION_SYMBOL)); double gross, commission, swap, fee; if(PositionParts(ticket, true, gross, commission, swap, fee)) { p = p + ",\"gross_pnl\":" + Money(gross); p = p + ",\"commission\":" + Money(commission); p = p + ",\"swap\":" + Money(swap); p = p + ",\"fee\":" + Money(fee); } p = p + "}"; if(arr != "") arr = arr + ","; arr = arr + p; } string s = "{" + CommonFields("positions_snapshot"); s = s + ",\"positions\":[" + arr + "]"; s = s + LastDepositField(); if(recovered) s = s + ",\"recovered\":true"; Enqueue(s + "}"); } void SendDayWeekPnl(const bool recovered) { double floating = FloatingPnl(); string s = "{" + CommonFields("day_week_pnl"); s = s + ",\"day_pnl\":" + Money(floating + RealizedPnl(DayStart())); s = s + ",\"week_pnl\":" + Money(floating + RealizedPnl(WeekStart())); s = s + PeriodFields("day", DayStart()); s = s + PeriodFields("week", WeekStart()); s = s + ",\"currency\":\"" + JsonEscape(AccountInfoString(ACCOUNT_CURRENCY)) + "\""; if(recovered) s = s + ",\"recovered\":true"; Enqueue(s + "}"); } void SendHeartbeat() { Enqueue("{" + CommonFields("heartbeat") + "}"); } //+------------------------------------------------------------------+ //| Контракт v2: ордера, cash и снимки счёта | //+------------------------------------------------------------------+ string OrderTypeName(const long order_type) { switch((int)order_type) { case ORDER_TYPE_BUY_LIMIT: return "buy_limit"; case ORDER_TYPE_SELL_LIMIT: return "sell_limit"; case ORDER_TYPE_BUY_STOP: return "buy_stop"; case ORDER_TYPE_SELL_STOP: return "sell_stop"; case ORDER_TYPE_BUY_STOP_LIMIT: return "buy_stop_limit"; case ORDER_TYPE_SELL_STOP_LIMIT: return "sell_stop_limit"; } return ""; } bool IsPendingOrderType(const long order_type) { return order_type == ORDER_TYPE_BUY_LIMIT || order_type == ORDER_TYPE_SELL_LIMIT || order_type == ORDER_TYPE_BUY_STOP || order_type == ORDER_TYPE_SELL_STOP || order_type == ORDER_TYPE_BUY_STOP_LIMIT || order_type == ORDER_TYPE_SELL_STOP_LIMIT; } string OrderEventFields(const ulong ticket, const string symbol, const long order_type, const double volume, const double price, const datetime occurred_at, const string position_ticket) { string s = ",\"ticket\":\"" + IntegerToString((long)ticket) + "\""; if(StringLen(symbol) > 0) s = s + ",\"symbol\":\"" + JsonEscape(symbol) + "\""; string type_name = OrderTypeName(order_type); if(StringLen(type_name) > 0) s = s + ",\"order_type\":\"" + type_name + "\""; if(volume > 0.0) s = s + ",\"volume\":" + Num(volume); if(price > 0.0) s = s + ",\"price\":" + Num(price); s = s + ",\"occurred_at\":\"" + IsoTime(occurred_at) + "\""; if(StringLen(position_ticket) > 0) s = s + ",\"position_ticket\":\"" + position_ticket + "\""; return s; } void SendOrderEvent(const string msg_type, const ulong ticket, const string symbol, const long order_type, const double volume, const double price, const datetime occurred_at, const string position_ticket) { int stable_kind = msg_type == "order_place" ? 1 : (msg_type == "order_update" ? 2 : (msg_type == "order_cancel" ? 3 : 4)); string event_id = StableEventId(stable_kind, ticket, occurred_at); Enqueue("{" + CommonFieldsWithId(msg_type, event_id) + OrderEventFields(ticket, symbol, order_type, volume, price, occurred_at, position_ticket) + "}"); } void SendCashOperation(const ulong deal_ticket) { if(!HistoryDealSelect(deal_ticket)) return; if(HistoryDealGetInteger(deal_ticket, DEAL_TYPE) != DEAL_TYPE_BALANCE) return; double amount = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); datetime at = (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); string operation = amount >= 0.0 ? "deposit" : "withdrawal"; string event_id = StableEventId(5, deal_ticket, at); string s = "{" + CommonFieldsWithId("cash_operation", event_id); s = s + ",\"ticket\":\"" + IntegerToString((long)deal_ticket) + "\""; s = s + ",\"operation\":\"" + operation + "\""; s = s + ",\"amount\":" + Money(amount); s = s + ",\"currency\":\"" + JsonEscape(AccountInfoString(ACCOUNT_CURRENCY)) + "\""; s = s + ",\"occurred_at\":\"" + IsoTime(at) + "\""; Enqueue(s + "}"); } void SendAccountSnapshot(const bool recovered) { string s = "{" + CommonFields("account_snapshot"); s = s + ",\"balance\":" + Money(AccountInfoDouble(ACCOUNT_BALANCE)); s = s + ",\"equity\":" + Money(AccountInfoDouble(ACCOUNT_EQUITY)); s = s + ",\"currency\":\"" + JsonEscape(AccountInfoString(ACCOUNT_CURRENCY)) + "\""; if(recovered) s = s + ",\"recovered\":true"; Enqueue(s + "}"); } void SendPendingOrdersSnapshot(const bool recovered) { string s = "{" + CommonFields("pending_orders_snapshot") + ",\"orders\":["; int count = 0; for(int i = 0; i < OrdersTotal(); i++) { ulong ticket = OrderGetTicket(i); if(ticket == 0) continue; long order_type = OrderGetInteger(ORDER_TYPE); if(!IsPendingOrderType(order_type)) continue; datetime at = (datetime)OrderGetInteger(ORDER_TIME_SETUP); if(count > 0) s = s + ","; s = s + "{" + StringSubstr(OrderEventFields(ticket, OrderGetString(ORDER_SYMBOL), order_type, OrderGetDouble(ORDER_VOLUME_CURRENT), OrderGetDouble(ORDER_PRICE_OPEN), at, ""), 1); s = s + ",\"time\":\"" + IsoTime(at) + "\"}"; count++; } s = s + "]"; if(recovered) s = s + ",\"recovered\":true"; Enqueue(s + "}"); } void BackfillHistoryV2() { if(!HistorySelect(0, TimeTradeServer() + 60)) return; int deals = HistoryDealsTotal(); for(int i = 0; i < deals; i++) SendCashOperation(HistoryDealGetTicket(i)); int orders = HistoryOrdersTotal(); for(int i = 0; i < orders; i++) { ulong ticket = HistoryOrderGetTicket(i); if(ticket == 0) continue; long order_type = HistoryOrderGetInteger(ticket, ORDER_TYPE); if(!IsPendingOrderType(order_type)) continue; long state = HistoryOrderGetInteger(ticket, ORDER_STATE); datetime at = (datetime)HistoryOrderGetInteger(ticket, ORDER_TIME_DONE); string kind = state == ORDER_STATE_FILLED ? "order_execute" : (state == ORDER_STATE_CANCELED || state == ORDER_STATE_EXPIRED ? "order_cancel" : "order_place"); string position = kind == "order_execute" ? IntegerToString(HistoryOrderGetInteger(ticket, ORDER_POSITION_ID)) : ""; SendOrderEvent(kind, ticket, HistoryOrderGetString(ticket, ORDER_SYMBOL), order_type, HistoryOrderGetDouble(ticket, ORDER_VOLUME_CURRENT), HistoryOrderGetDouble(ticket, ORDER_PRICE_OPEN), at, position); } } void OnOrderTrans(const ulong ticket) { if(!OrderSelect(ticket)) return; long order_type = OrderGetInteger(ORDER_TYPE); if(!IsPendingOrderType(order_type)) return; datetime setup = (datetime)OrderGetInteger(ORDER_TIME_SETUP); bool is_new = setup >= g_started_at; datetime at = is_new ? setup : TimeTradeServer(); SendOrderEvent(is_new ? "order_place" : "order_update", ticket, OrderGetString(ORDER_SYMBOL), order_type, OrderGetDouble(ORDER_VOLUME_CURRENT), OrderGetDouble(ORDER_PRICE_OPEN), at, ""); } void OnOrderHistory(const ulong ticket) { if(!HistoryOrderSelect(ticket)) return; long order_type = HistoryOrderGetInteger(ticket, ORDER_TYPE); if(!IsPendingOrderType(order_type)) return; long state = HistoryOrderGetInteger(ticket, ORDER_STATE); if(state != ORDER_STATE_FILLED && state != ORDER_STATE_CANCELED && state != ORDER_STATE_EXPIRED) return; datetime at = (datetime)HistoryOrderGetInteger(ticket, ORDER_TIME_DONE); string kind = state == ORDER_STATE_FILLED ? "order_execute" : "order_cancel"; string position = kind == "order_execute" ? IntegerToString(HistoryOrderGetInteger(ticket, ORDER_POSITION_ID)) : ""; SendOrderEvent(kind, ticket, HistoryOrderGetString(ticket, ORDER_SYMBOL), order_type, HistoryOrderGetDouble(ticket, ORDER_VOLUME_CURRENT), HistoryOrderGetDouble(ticket, ORDER_PRICE_OPEN), at, position); } //+------------------------------------------------------------------+ //| Разбор транзакций | //+------------------------------------------------------------------+ void OnDealAdded(const ulong deal_ticket) { if(!HistoryDealSelect(deal_ticket)) { Print(L("bridge: сделка ", "bridge: deal "), deal_ticket, L(" ещё не в истории — пропуск разбора", " not in history yet — skipped")); return; } long dtype = HistoryDealGetInteger(deal_ticket, DEAL_TYPE); if(dtype == DEAL_TYPE_BALANCE) { SendCashOperation(deal_ticket); if(HistoryDealGetDouble(deal_ticket, DEAL_PROFIT) > 0.0) { g_last_deposit_at = (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); g_last_deposit_known = true; } return; } if(dtype != DEAL_TYPE_BUY && dtype != DEAL_TYPE_SELL) return; // кредит, коррекции — вне контракта long entry = HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); ulong pos_id = (ulong)HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); if(pos_id == 0) return; string symbol = HistoryDealGetString(deal_ticket, DEAL_SYMBOL); double volume = HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); double price = HistoryDealGetDouble(deal_ticket, DEAL_PRICE); if(entry == DEAL_ENTRY_IN) { SendDealOpen(deal_ticket, pos_id, symbol, volume, price); double sl = 0.0, tp = 0.0; if(PositionSelectByTicket(pos_id)) { sl = PositionGetDouble(POSITION_SL); tp = PositionGetDouble(POSITION_TP); } CachePut(pos_id, sl, tp); if(sl > 0.0 || tp > 0.0) SendSltp("sltp_set", pos_id, sl, tp, 0.0, 0.0); } else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_OUT_BY || entry == DEAL_ENTRY_INOUT) { // INOUT (разворот на netting): шлём только закрытие старой позиции, // открытие новой не отслеживается — ограничение ADR-0108. SendDealClose(deal_ticket, pos_id, volume, price); CacheRemove(pos_id); } } void OnPositionTrans(const ulong pos_id) { if(!PositionSelectByTicket(pos_id)) { CacheRemove(pos_id); // позиция закрылась — закрытие пришло DEAL_ADD return; } double sl = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); int idx = CacheFind(pos_id); if(idx < 0) { CachePut(pos_id, sl, tp); // позиция старше советника: молча в кэш return; } double old_sl = g_csl[idx]; double old_tp = g_ctp[idx]; if(sl == old_sl && tp == old_tp) return; // транзакция позиции без смены SL/TP (объём и т.п.) bool sl_set = (old_sl == 0.0 && sl > 0.0); bool tp_set = (old_tp == 0.0 && tp > 0.0); bool sl_gone = (old_sl > 0.0 && sl == 0.0); bool tp_gone = (old_tp > 0.0 && tp == 0.0); if(sl_gone || tp_gone) SendSltp("sltp_remove", pos_id, 0.0, 0.0, old_sl, old_tp); else if(sl_set || tp_set) SendSltp("sltp_set", pos_id, sl, tp, old_sl, old_tp); else SendSltp("sltp_move", pos_id, sl, tp, old_sl, old_tp); CachePut(pos_id, sl, tp); } //+------------------------------------------------------------------+ //| Активность рынка | //+------------------------------------------------------------------+ bool MarketActive() { if(!TerminalInfoInteger(TERMINAL_CONNECTED)) return false; MqlTick tick; if(!SymbolInfoTick(Symbol(), tick) || tick.time == 0) return false; return (TimeCurrent() - tick.time) < InpActivityIdleMin * 60; } //+------------------------------------------------------------------+ //| Обработчики | //+------------------------------------------------------------------+ int OnInit() { if(MQLInfoInteger(MQL_TESTER)) { Print(L("bridge: WebRequest не работает в тестере стратегий — советник не запущен", "bridge: WebRequest is unavailable in the Strategy Tester — EA not started")); return INIT_FAILED; } if(StringLen(InpToken) < 10) { Print(L("bridge: не задан токен (параметр Token) — советник не запущен", "bridge: token is not set (Token input) — EA not started")); return INIT_PARAMETERS_INCORRECT; } if(StringFind(InpBridgeUrl, "http://") != 0 && StringFind(InpBridgeUrl, "https://") != 0) { Print(L("bridge: URL должен начинаться с http:// или https://", "bridge: URL must start with http:// or https://")); return INIT_PARAMETERS_INCORRECT; } if(InpSnapshotSec < 10) { Print(L("bridge: период снимков слишком мал (минимум 10 с)", "bridge: snapshot period too small (min 10 s)")); return INIT_PARAMETERS_INCORRECT; } MathSrand((uint)(GetTickCount64() ^ (ulong)GetMicrosecondCount() ^ (ulong)AccountInfoInteger(ACCOUNT_LOGIN))); g_session_id = NewUuid(); g_seq = 0; g_qn = 0; ArrayResize(g_queue, 0); g_frozen = false; g_fail_streak = 0; g_next_attempt = 0; g_need_recovered = true; g_force_snapshot = false; SeedCache(); SeedLastDeposit(); g_started_at = TimeTradeServer(); BackfillHistoryV2(); EventSetTimer(1); g_next_snapshot = 0; // первый кадр — сразу (проверка связи + recovered) Print(L("bridge: старт, ea_version ", "bridge: start, ea_version "), EA_VERSION, L(", сессия ", ", session "), g_session_id, ", url ", InpBridgeUrl); Comment(L("Qubitrino bridge: запущен, сессия ", "Qubitrino bridge: started, session ") + g_session_id); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); Comment(""); Print(L("bridge: стоп, причина ", "bridge: stop, reason "), reason); } void OnTimer() { if(g_frozen) return; if(g_force_snapshot) { SendSnapshot(true); SendDayWeekPnl(true); SendAccountSnapshot(true); SendPendingOrdersSnapshot(true); g_force_snapshot = false; } datetime now = TimeCurrent(); if(now >= g_next_snapshot) { if(MarketActive()) { SendSnapshot(g_need_recovered); SendDayWeekPnl(g_need_recovered); SendAccountSnapshot(g_need_recovered); SendPendingOrdersSnapshot(g_need_recovered); g_need_recovered = false; } else { SendHeartbeat(); } g_next_snapshot = now + InpSnapshotSec; } FlushQueue(); } void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { if(g_frozen) return; if(trans.type == TRADE_TRANSACTION_DEAL_ADD) OnDealAdded(trans.deal); else if(trans.type == TRADE_TRANSACTION_POSITION) OnPositionTrans(trans.position); else if(trans.type == TRADE_TRANSACTION_ORDER_ADD || trans.type == TRADE_TRANSACTION_ORDER_UPDATE) OnOrderTrans(trans.order); else if(trans.type == TRADE_TRANSACTION_HISTORY_ADD) OnOrderHistory(trans.order); } //+------------------------------------------------------------------+