2026-09-10 16:43:28 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //| OrderLifecycleRecorder.mq5 |
|
| | | //| Copyright 2000-2026, MetaQuotes Ltd. |
|
| | | //| www.mql5.com |
|
| | | //+------------------------------------------------------------------+
|
| | | #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
|
| | | #property link "https://www.mql5.com"
|
| | | #property version "1.00"
|
| | | #property description "Вывод полного жизненного цикла заявки:"
|
| | | #property description "запрос, ордер, сделка, позиция."
|
| | | #property description "Накопление событий, reconciliation, CSV, timeline."
|
| | |
|
| | | #include "TradeLifecycle.mqh"
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Входные параметры |
|
| | | //+------------------------------------------------------------------+
|
2026-09-16 15:37:55 +03:00 | | | //--- Magic Number
|
| | | input ulong InpMagic=20260806;
|
| | | //--- Объем тестовой заявки,лот
|
| | | input double InpVolume=0.01;
|
| | | //--- символ (пусто=текущий график)
|
| | | input string InpSymbol="";
|
| | | //--- OrderSendAsync() вместо OrderSend()
|
| | | input bool InpUseAsync=true;
|
| | | //--- таймаут ожидания события,мс
|
| | | input int InpReconcileTimeoutMs=5000;
|
| | | //--- число повторов запроса при отказе (0=без ограничений)
|
| | | input int InpRetries=0;
|
| | | //--- пауза между повторами,мс
|
| | | input int InpRetryDelayMs=5000;
|
| | | //--- entry/reason на всю цепочку в CSV (не только DEAL_ADD)
|
| | | input bool InpCsvChainEntry=true;
|
| | | //--- размер шрифта кнопок (базовый,масштабируется по ширине окна)
|
| | | input int InpBtnFontSize=10;
|
| | | //--- доп. множитель шрифта поверх DPI (1.0=только авто)
|
| | | input double InpFontScale=1.24;
|
2026-09-10 16:43:28 +03:00 | | |
|
| | | //--- торговые константы
|
| | | #define TRADE_DEVIATION_POINTS 20 // допустимое отклонение для рыночного ордера, пункты
|
| | | #define PENDING_DISTANCE_POINTS 100 // расстояние до отложенного ордера от рынка, пункты
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Имена кнопок |
|
| | | //+------------------------------------------------------------------+
|
| | | const string BUTTON_NAMES[]=
|
| | | {
|
| | | "Market Buy", // 0 — покупка по рынку
|
| | | "Market Sell", // 1 — продажа по рынку
|
| | | "Pending Buy Limit", // 2 — отложенный ордер Buy Limit
|
| | | "Cancel", // 3 — отмена первого отложенного ордера
|
| | | "Modify", // 4 — модификация отложенного ордера
|
| | | "Export CSV", // 5 — экспорт событий в CSV
|
| | | "Draw Timeline", // 6 — построение timeline на графике
|
| | | "Close" // 7 — закрытие позиций по одной за нажатие
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Базовые размеры панели (при макете окна ~900px) |
|
| | | //+------------------------------------------------------------------+
|
| | | #define UI_BTN_BASE_WIDTH 70 // минимальная ширина кнопки
|
| | | #define UI_BTN_BASE_HEIGHT 36 // высота кнопки
|
| | | #define UI_BTN_BASE_Y 44 // отступ сверху
|
| | | #define UI_BTN_PADDING 16 // внутреннее поле кнопки слева/справа
|
| | | #define UI_BTN_GAP 6 // зазор между кнопками
|
| | | #define UI_PANEL_BASE_W 720 // эталонная ширина панели при scale=1.0
|
| | | #define UI_SCALE_MIN 0.75 // нижняя граница масштаба
|
| | | #define UI_SCALE_MAX 1.50 // верхняя граница масштаба
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Глобальные переменные |
|
| | | //+------------------------------------------------------------------+
|
| | | CTradeLifecycle ExtLifecycle; // накопитель событий+реестр запросов
|
| | | string ExtSymbol; // рабочий символ
|
| | | ulong ExtStartUs; // GetMicrosecondCount() для стартовой метки
|
| | | bool ExtHasEvents; // появились ли события
|
| | | ulong ExtLastEventUs; // GetMicrosecondCount() последнего события
|
| | | bool ExtWatchdogFired; // watchdog уже сработал в этом «тихом» периоде
|
| | | double ExtUiScale; // текущий масштаб панели
|
| | | uint ExtLocalSeq; // локальный порядковый номер отправленных запросов
|
| | | //+------------------------------------------------------------------+
|
| | | //| Состояние повторов запроса (InpRetries) |
|
| | | //+------------------------------------------------------------------+
|
| | | ENUM_ORDER_TYPE ExtRetryType; // тип ордера для повтора
|
| | | bool ExtRetryPending; // запланирован ли повтор
|
| | | ulong ExtRetryAtUs; // момент повтора (GetMicrosecondCount)
|
| | | int ExtRetriesLeft; // осталось повторов (-1=без ограничений)
|
| | | //+------------------------------------------------------------------+
|
| | | //| Масштаб UI по ширине окна графика: панель не должна выходить |
|
| | | //| За пределы окна (UI_PANEL_BASE_W — ширина панели при scale=1.0),|
|
| | | //| С границами UI_SCALE_MIN..UI_SCALE_MAX. |
|
| | | //+------------------------------------------------------------------+
|
| | | double GetUIScale()
|
| | | {
|
| | | long chart_w=ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
|
| | |
|
| | | double s=(chart_w>0?(double)chart_w/UI_PANEL_BASE_W:1.0);
|
| | |
|
| | | if(s<UI_SCALE_MIN)
|
| | | s=UI_SCALE_MIN;
|
| | | if(s>UI_SCALE_MAX)
|
| | | s=UI_SCALE_MAX;
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(s);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Итоговый коэффициент масштабирования шрифта |
|
| | | //+------------------------------------------------------------------+
|
| | | double GetFontScale()
|
| | | {
|
| | | double base=(InpFontScale>0.1?InpFontScale:1.0);
|
| | |
|
| | | int dpi=(int)TerminalInfoInteger(TERMINAL_SCREEN_DPI);
|
| | | if(dpi<=0)
|
| | | return(base);
|
| | |
|
| | | return(base*(double)dpi/96.0);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Измерение ширины надписи по метрикам шрифта MQL5 |
|
| | | //+------------------------------------------------------------------+
|
| | | int TextWidthPx(const string text,const int font_size)
|
| | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- шрифт не задался — грубая оценка по длине строки
|
2026-09-10 16:43:28 +03:00 | | | if(!TextSetFont("Arial",font_size,0))
|
2026-09-16 15:37:55 +03:00 | | | return(StringLen(text)*font_size/2+20);
|
2026-09-10 16:43:28 +03:00 | | |
|
| | | uint tw=0;
|
| | | uint th=0;
|
2026-09-16 15:37:55 +03:00 | | | //--- размер метки не получен — грубая оценка
|
2026-09-10 16:43:28 +03:00 | | | if(!TextGetSize(text,tw,th))
|
2026-09-16 15:37:55 +03:00 | | | return(StringLen(text)*font_size/2+20);
|
2026-09-10 16:43:28 +03:00 | | |
|
| | | return((int)tw);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Нормализация цены по SYMBOL_DIGITS |
|
| | | //+------------------------------------------------------------------+
|
| | | double NormalizePrice(const string symbol,const double price)
|
| | | {
|
| | | int digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(NormalizeDouble(price,digits));
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Нормализация объема: диапазон SYMBOL_VOLUME_MIN..MAX и шаг STEP |
|
| | | //+------------------------------------------------------------------+
|
| | | double NormalizeVolume(const string symbol,const double volume)
|
| | | {
|
| | | double min_v=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN);
|
| | | double max_v=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MAX);
|
| | | double step_v=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP);
|
| | |
|
| | | double v=volume;
|
| | | if(v<min_v)
|
| | | v=min_v;
|
| | | if(v>max_v)
|
| | | v=max_v;
|
| | | if(step_v>0)
|
| | | {
|
| | | v=MathRound(v/step_v)*step_v;
|
| | | int vdigits=(int)MathCeil(-MathLog10(step_v));
|
| | | if(vdigits<0)
|
| | | vdigits=0;
|
| | | v=NormalizeDouble(v,vdigits);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(v);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Выбор режима исполнения из SYMBOL_FILLING_MODE |
|
| | | //+------------------------------------------------------------------+
|
| | | ENUM_ORDER_TYPE_FILLING SelectFilling(const string symbol)
|
| | | {
|
| | | long mode=SymbolInfoInteger(symbol,SYMBOL_FILLING_MODE);
|
| | |
|
| | | if((mode&SYMBOL_FILLING_FOK)!=0)
|
| | | return(ORDER_FILLING_FOK);
|
| | | if((mode&SYMBOL_FILLING_IOC)!=0)
|
| | | return(ORDER_FILLING_IOC);
|
| | | return(ORDER_FILLING_RETURN);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| BuildRequest — заполнение MqlTradeRequest |
|
| | | //+------------------------------------------------------------------+
|
| | | bool BuildRequest(const string symbol,const ENUM_ORDER_TYPE type,
|
| | | const double volume,MqlTradeRequest &request)
|
| | | {
|
| | | ZeroMemory(request);
|
| | |
|
| | | bool is_market=(type==ORDER_TYPE_BUY||type==ORDER_TYPE_SELL);
|
| | | request.action=(is_market?TRADE_ACTION_DEAL:TRADE_ACTION_PENDING);
|
| | | request.magic=InpMagic;
|
| | | request.symbol=symbol;
|
| | | request.type=type;
|
| | | request.volume=NormalizeVolume(symbol,volume);
|
| | | request.deviation=TRADE_DEVIATION_POINTS;
|
| | | request.type_time=ORDER_TIME_GTC;
|
| | | request.type_filling=SelectFilling(symbol);
|
| | | request.comment=StringFormat("LC_%u",ExtLocalSeq+1);
|
| | |
|
| | | //--- цена: рыночная — Ask/Bid; отложенная — с отступом от рынка
|
| | | double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);
|
| | | double bid=SymbolInfoDouble(symbol,SYMBOL_BID);
|
| | | double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
|
| | | if(point<=0)
|
| | | point=1e-6;
|
| | |
|
| | | switch(type)
|
| | | {
|
| | | case ORDER_TYPE_BUY:
|
| | | request.price=ask;
|
| | | break;
|
| | | case ORDER_TYPE_SELL:
|
| | | request.price=bid;
|
| | | break;
|
| | | case ORDER_TYPE_BUY_LIMIT:
|
| | | request.price=ask-PENDING_DISTANCE_POINTS*point;
|
| | | break;
|
| | | case ORDER_TYPE_SELL_LIMIT:
|
| | | request.price=bid+PENDING_DISTANCE_POINTS*point;
|
| | | break;
|
| | | default:
|
| | | PrintFormat("BuildRequest: тип '%s' пока не поддерживается",EnumToString(type));
|
| | | return(false);
|
| | | }
|
| | |
|
| | | request.price=NormalizePrice(symbol,request.price);
|
| | |
|
| | | PrintFormat("BuildRequest: %s %s vol=%.2f price=%.5f filling=%s",
|
| | | EnumToString(type),symbol,request.volume,request.price,
|
| | | EnumToString(request.type_filling));
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(true);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| PreCheck — предварительная проверка OrderCheck |
|
| | | //+------------------------------------------------------------------+
|
| | | bool PreCheck(const MqlTradeRequest &request,MqlTradeCheckResult &check_result)
|
| | | {
|
| | | ZeroMemory(check_result);
|
| | |
|
| | | if(!OrderCheck(request,check_result))
|
| | | {
|
| | | PrintFormat("PreCheck: отказ | retcode=%u '%s'",check_result.retcode,check_result.comment);
|
| | | return(false);
|
| | | }
|
| | |
|
| | | PrintFormat("PreCheck: ок | retcode=%u margin_free=%.2f",check_result.retcode,check_result.margin_free);
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(true);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| ScheduleRetry — планирование повтора запроса (InpRetries) |
|
| | | //+------------------------------------------------------------------+
|
| | | void ScheduleRetry(const ENUM_ORDER_TYPE type)
|
| | | {
|
| | | if(InpRetries==0||ExtRetriesLeft>0)
|
| | | {
|
| | | if(ExtRetriesLeft>0)
|
| | | ExtRetriesLeft--;
|
| | |
|
| | | ExtRetryType=type;
|
| | | ExtRetryPending=true;
|
| | | ExtRetryAtUs=GetMicrosecondCount()+(ulong)InpRetryDelayMs*1000;
|
| | |
|
| | | PrintFormat("ScheduleRetry: %s через %d мс | осталось повторов: %s",
|
| | | EnumToString(type),InpRetryDelayMs,
|
| | | (InpRetries==0?"без ограничений":(string)ExtRetriesLeft));
|
| | | }
|
| | | else
|
| | | {
|
| | | PrintFormat("ScheduleRetry: лимит повторов исчерпан (%d), %s не повторяем",
|
| | | InpRetries,EnumToString(type));
|
| | | }
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| SendTradeRequest — BuildRequest -> PreCheck -> OrderSend/Async |
|
| | | //+------------------------------------------------------------------+
|
| | | bool SendTradeRequest(const ENUM_ORDER_TYPE type,const double volume)
|
| | | {
|
| | | MqlTradeRequest request;
|
| | | if(!BuildRequest(ExtSymbol,type,volume,request))
|
| | | return(false);
|
| | |
|
| | | MqlTradeCheckResult check_result;
|
| | | if(!PreCheck(request,check_result))
|
| | | {
|
| | | ScheduleRetry(type);
|
| | | return(false);
|
| | | }
|
| | |
|
| | | MqlTradeResult result;
|
| | | ulong t0=GetMicrosecondCount();
|
| | | bool ok=(InpUseAsync?OrderSendAsync(request,result):OrderSend(request,result));
|
| | |
|
| | | PrintFormat("SendTradeRequest: %s | ok=%s retcode=%u '%s' request_id=%u dt=%I64u мкс",
|
| | | EnumToString(type),
|
| | | (ok?"true":"false"),
|
| | | result.retcode,result.comment,result.request_id,
|
| | | GetMicrosecondCount()-t0);
|
| | |
|
| | | if(ok)
|
| | | {
|
| | | ExtLocalSeq++;
|
| | | ExtLifecycle.RegisterRequest(ExtLocalSeq,result.request_id,InpMagic,ExtSymbol,
|
| | | request.volume,request.comment,t0,type);
|
| | | }
|
| | | else
|
| | | {
|
| | | //--- отправка не удалась (нет соединения и т.п.) — планируем повтор
|
| | | ScheduleRetry(type);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(ok);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| FindFirstPendingOrder — первый активный отложенный ордер |
|
| | | //+------------------------------------------------------------------+
|
| | | ulong FindFirstPendingOrder()
|
| | | {
|
| | | for(int i=0;i<OrdersTotal();i++)
|
| | | {
|
| | | ulong ticket=OrderGetTicket(i);
|
| | | if(ticket==0)
|
| | | continue;
|
| | | if(OrderGetString(ORDER_SYMBOL)!=ExtSymbol)
|
| | | continue;
|
| | | if((ulong)OrderGetInteger(ORDER_MAGIC)!=InpMagic)
|
| | | continue;
|
| | |
|
| | | long type=OrderGetInteger(ORDER_TYPE);
|
| | | if(type==ORDER_TYPE_BUY_LIMIT||type==ORDER_TYPE_SELL_LIMIT||
|
| | | type==ORDER_TYPE_BUY_STOP||type==ORDER_TYPE_SELL_STOP||
|
| | | type==ORDER_TYPE_BUY_STOP_LIMIT||type==ORDER_TYPE_SELL_STOP_LIMIT)
|
| | | return(ticket);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(0);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| SendManageRequest — отправка управляющего запроса (REMOVE/MODIFY)|
|
| | | //+------------------------------------------------------------------+
|
| | | bool SendManageRequest(MqlTradeRequest &request,const double volume,
|
| | | const ENUM_ORDER_TYPE order_type)
|
| | | {
|
| | | MqlTradeCheckResult check_result;
|
| | | if(!PreCheck(request,check_result))
|
| | | return(false);
|
| | |
|
| | | MqlTradeResult result;
|
| | | ulong t0=GetMicrosecondCount();
|
| | | bool ok=(InpUseAsync?OrderSendAsync(request,result):OrderSend(request,result));
|
| | |
|
| | | PrintFormat("SendManageRequest: %s order=%I64u | ok=%s retcode=%u '%s' request_id=%u dt=%I64u мкс",
|
| | | EnumToString(request.action),request.order,
|
| | | (ok?"true":"false"),
|
| | | result.retcode,result.comment,result.request_id,
|
| | | GetMicrosecondCount()-t0);
|
| | |
|
| | | if(ok)
|
| | | {
|
| | | ExtLocalSeq++;
|
| | | ExtLifecycle.RegisterRequest(ExtLocalSeq,result.request_id,InpMagic,ExtSymbol,
|
| | | volume,request.comment,t0,order_type);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(ok);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnInit |
|
| | | //+------------------------------------------------------------------+
|
| | | int OnInit()
|
| | | {
|
| | | ExtSymbol=(InpSymbol==""?_Symbol:InpSymbol);
|
| | | ExtStartUs=GetMicrosecondCount();
|
| | | ExtHasEvents=false;
|
| | | ExtLastEventUs=0;
|
| | | ExtWatchdogFired=false;
|
| | | ExtUiScale=0.0;
|
| | | ExtLocalSeq=0;
|
| | |
|
| | | //--- состояние повторов
|
| | | ExtRetryPending=false;
|
| | | ExtRetryAtUs=0;
|
| | | ExtRetriesLeft=(InpRetries==0?-1:InpRetries);
|
| | |
|
| | | //--- подписка на символ
|
| | | if(!SymbolSelect(ExtSymbol,true))
|
| | | PrintFormat("OnInit: не удалось выбрать символ %s (error=%d)",ExtSymbol,GetLastError());
|
| | |
|
| | | //--- контекст для reconciliation (символ+magic)
|
| | | ExtLifecycle.SetContext(ExtSymbol,InpMagic);
|
| | |
|
| | | //--- создание кнопок
|
| | | CreateButtons();
|
| | | ExtUiScale=GetUIScale();
|
| | | LogButtonWidths();
|
| | |
|
| | | //--- таймер для запуска reconciliation
|
| | | EventSetTimer(1);
|
| | |
|
| | | PrintFormat("OrderLifecycleRecorder: init | symbol=%s magic=%I64u volume=%.2f async=%s ui_scale=%.2f font_scale=%.2f (dpi=%d, mult=%.2f)",
|
| | | ExtSymbol,InpMagic,InpVolume,(InpUseAsync?"true":"false"),ExtUiScale,
|
| | | GetFontScale(),(int)TerminalInfoInteger(TERMINAL_SCREEN_DPI),
|
| | | (InpFontScale>0.1?InpFontScale:1.0));
|
| | |
|
| | | //--- модель учета позиций
|
| | | PrintFormat("Модель учета позиций: %s",
|
| | | EnumToString((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)));
|
| | |
|
| | | //--- сводка для проверки доступности
|
| | | PrintEntityInfo(ExtSymbol);
|
| | |
|
| | | return(INIT_SUCCEEDED);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnDeinit |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnDeinit(const int reason)
|
| | | {
|
| | | //--- экспорт журнала при завершении
|
| | | if(reason!=REASON_CHARTCHANGE&&reason!=REASON_RECOMPILE)
|
| | | {
|
| | | string file_name=MakeExportFileName();
|
| | | ExtLifecycle.ExportCsv(file_name,InpCsvChainEntry);
|
| | | }
|
| | |
|
| | | EventKillTimer();
|
| | |
|
| | | //--- удаление графических объектов (кнопки+timeline)
|
| | | int total=ArraySize(BUTTON_NAMES);
|
| | | for(int i=0;i<total;i++)
|
| | | ObjectDelete(0,"LC_BTN_"+(string)i);
|
| | | ObjectsDeleteAll(0,"LC_TL_");
|
| | |
|
| | | Print("OrderLifecycleRecorder: завершение");
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnTradeTransaction — регистрация событий жизненного цикла заявки |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnTradeTransaction(const MqlTradeTransaction &trans,
|
| | | const MqlTradeRequest &request,
|
| | | const MqlTradeResult &result)
|
| | | {
|
| | | //--- фильтр по символу (если транзакция не глобальная)
|
| | | if(trans.symbol!=""&&trans.symbol!=ExtSymbol)
|
| | | return;
|
| | |
|
| | | //--- глобальные события (symbol='') — принимаем только запросы и локального реестра по request_id,иначе это чужие запросы
|
| | | if(trans.symbol=="")
|
| | | {
|
| | | PendingRequest _pr;
|
| | | if(result.request_id==0||!ExtLifecycle.FindRequest(result.request_id,_pr))
|
| | | return;
|
| | | }
|
| | |
|
| | | //--- формирование события
|
| | | TradeEvent e;
|
| | | ZeroMemory(e);
|
| | | e.request_id=result.request_id;
|
| | | e.order_ticket=trans.order;
|
| | | e.deal_ticket=trans.deal;
|
| | | e.position_id=trans.position;
|
| | | e.type=trans.type;
|
| | | e.local_time_us=GetMicrosecondCount();
|
| | | //--- берем время сервера на момент обработки
|
| | | e.server_time=TimeTradeServer();
|
| | | e.price=trans.price;
|
| | | e.volume=trans.volume;
|
| | | e.retcode=result.retcode;
|
| | |
|
| | | //--- для сделок: направление и причина
|
| | | if(trans.type==TRADE_TRANSACTION_DEAL_ADD&&trans.deal!=0)
|
| | | {
|
| | | HistorySelect(0,TimeCurrent());
|
| | | if(HistoryDealSelect(trans.deal))
|
| | | {
|
| | | e.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal,DEAL_ENTRY);
|
| | | e.reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(trans.deal,DEAL_REASON);
|
| | | }
|
| | | }
|
| | |
|
| | | //--- добавление в буфер
|
| | | ExtLifecycle.Add(e);
|
| | | ExtHasEvents=true;
|
| | | ExtLastEventUs=e.local_time_us;
|
| | | ExtWatchdogFired=false;
|
| | |
|
| | | PrintFormat("OnTradeTransaction: %s | order=%I64u deal=%I64u pos=%I64u price=%.5f vol=%.2f retcode=%u dt=%I64u мкс",
|
| | | EnumToString(trans.type),
|
| | | trans.order,
|
| | | trans.deal,
|
| | | trans.position,
|
| | | trans.price,
|
| | | trans.volume,
|
| | | result.retcode,
|
| | | e.local_time_us-ExtStartUs);
|
| | |
|
| | | //--- для сделок: вывод в лог
|
| | | if(e.deal_ticket!=0)
|
| | | PrintFormat(" -> сделка: entry=%s reason=%s",
|
| | | EnumToString(e.entry),EnumToString(e.reason));
|
| | |
|
| | | //--- привязка события к локальному реестру по request_id
|
| | | if(result.request_id!=0)
|
| | | {
|
| | | PendingRequest pr;
|
| | | if(ExtLifecycle.FindRequest(result.request_id,pr))
|
| | | {
|
| | | PrintFormat(" -> локальный запрос #%u | %s vol=%.2f '%s' | с момента отправки %I64u мкс",
|
| | | pr.local_seq,pr.symbol,pr.volume,pr.comment,
|
| | | e.local_time_us-pr.send_time_us);
|
| | |
|
| | | //--- при отказе сервера (REQUEST с ошибкой) — планируем повтор
|
| | | if(trans.type==TRADE_TRANSACTION_REQUEST)
|
| | | {
|
| | | uint rc=result.retcode;
|
| | | if(rc!=0&&rc!=TRADE_RETCODE_DONE&&rc!=TRADE_RETCODE_PLACED&&rc!=TRADE_RETCODE_DONE_PARTIAL)
|
| | | {
|
| | | //--- бессмысленные повторы не планируем: рынок закрыт или торговля отключена
|
| | | if(rc==TRADE_RETCODE_MARKET_CLOSED||rc==TRADE_RETCODE_TRADE_DISABLED)
|
| | | PrintFormat(" -> повтор пропущен: %s (бессмысленно до изменения условий)",
|
| | | RetcodeName(rc));
|
| | | else
|
| | | ScheduleRetry(pr.order_type);
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnTimer — повторы запросов+reconciliation по таймауту |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnTimer()
|
| | | {
|
| | | ulong now_us=GetMicrosecondCount();
|
| | |
|
| | | //--- повтор запроса по расписанию (InpRetries)
|
| | | if(ExtRetryPending&&now_us>=ExtRetryAtUs)
|
| | | {
|
| | | ExtRetryPending=false;
|
| | | PrintFormat("OnTimer: повтор запроса %s",EnumToString(ExtRetryType));
|
| | | SendTradeRequest(ExtRetryType,InpVolume);
|
| | | }
|
| | |
|
| | | //--- запуск reconciliation по таймауту только при появлении новых событий
|
| | | if(!ExtHasEvents)
|
| | | return;
|
| | |
|
| | | static ulong last_reconcile_us=0;
|
| | | static int last_reconcile_cnt=-1;
|
| | |
|
| | | if(now_us-last_reconcile_us>=(ulong)InpReconcileTimeoutMs*1000)
|
| | | {
|
| | | int cnt=ExtLifecycle.EventCount();
|
| | | if(cnt!=last_reconcile_cnt)
|
| | | {
|
| | | ExtLifecycle.Reconcile();
|
| | | last_reconcile_cnt=cnt;
|
| | | }
|
| | | last_reconcile_us=now_us;
|
| | | }
|
| | | //--- watchdog: если события не приходили InpReconcileTimeoutMs,а цепочка не завершена — принудительная сверка (обнаружение потери последнего события)
|
| | | if(ExtHasEvents&&ExtLastEventUs!=0&&
|
| | | now_us-ExtLastEventUs>=(ulong)InpReconcileTimeoutMs*1000&&
|
| | | !ExtLifecycle.IsComplete())
|
| | | {
|
| | | if(!ExtWatchdogFired)
|
| | | {
|
| | | ExtWatchdogFired=true;
|
| | | PrintFormat("OnTimer: watchdog | событий не было %d мс, цепочка не завершена — принудительная сверка",
|
| | | InpReconcileTimeoutMs);
|
| | | ExtLifecycle.Reconcile();
|
| | | }
|
| | | }
|
| | |
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnChartEvent — обработка событий графика |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
|
| | | {
|
| | | //--- при изменении размеров окна обновляется панель,если изменился масштаб
|
| | | if(id==CHARTEVENT_CHART_CHANGE)
|
| | | {
|
| | | double s=GetUIScale();
|
| | | if(MathAbs(s-ExtUiScale)>0.02)
|
| | | {
|
| | | ExtUiScale=s;
|
| | | CreateButtons();
|
| | | }
|
| | | return;
|
| | | }
|
| | |
|
| | | if(id!=CHARTEVENT_OBJECT_CLICK)
|
| | | return;
|
| | |
|
| | | int total=ArraySize(BUTTON_NAMES);
|
| | | for(int i=0;i<total;i++)
|
| | | {
|
| | | if(sparam!="LC_BTN_"+(string)i)
|
| | | continue;
|
| | |
|
| | | PrintFormat("OnChartEvent: нажата кнопка '%s'",BUTTON_NAMES[i]);
|
| | |
|
| | | //--- сброс нажатого состояния кнопки
|
| | | ObjectSetInteger(0,sparam,OBJPROP_STATE,false);
|
| | | ChartRedraw(0);
|
| | |
|
| | | switch(i)
|
| | | {
|
| | | case 0:
|
| | | OnButtonMarketBuy();
|
| | | break;
|
| | | case 1:
|
| | | OnButtonMarketSell();
|
| | | break;
|
| | | case 2:
|
| | | OnButtonPendingBuyLimit();
|
| | | break;
|
| | | case 3:
|
| | | OnButtonCancel();
|
| | | break;
|
| | | case 4:
|
| | | OnButtonModify();
|
| | | break;
|
| | | case 5:
|
| | | OnButtonExportCsv();
|
| | | break;
|
| | | case 6:
|
| | | OnButtonDrawTimeline();
|
| | | break;
|
| | | case 7:
|
| | | OnButtonClose();
|
| | | break;
|
| | | }
|
| | | break;
|
| | | }
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Вывод фактической ширины каждой кнопки в лог |
|
| | | //+------------------------------------------------------------------+
|
| | | void LogButtonWidths()
|
| | | {
|
| | | int total=ArraySize(BUTTON_NAMES);
|
| | | Print("Фактическая ширина кнопок (px):");
|
| | | for(int i=0;i<total;i++)
|
| | | {
|
| | | string name="LC_BTN_"+(string)i;
|
| | | long w=ObjectGetInteger(0,name,OBJPROP_XSIZE);
|
| | | PrintFormat(" [%d] %-20s width=%d",i,BUTTON_NAMES[i],(int)w);
|
| | | }
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Размеры и шрифт масштабируются по ширине окна |
|
| | | //+------------------------------------------------------------------+
|
| | | void CreateButtons()
|
| | | {
|
| | | double scale=GetUIScale();
|
| | | double fscale=GetFontScale();
|
| | |
|
| | | int btn_h=(int)MathRound(UI_BTN_BASE_HEIGHT*scale);
|
| | | int btn_y=(int)MathRound(UI_BTN_BASE_Y*scale);
|
| | | int btn_fs=(int)MathRound(InpBtnFontSize*scale);
|
| | | int gap=(int)MathRound(UI_BTN_GAP*scale);
|
| | | int padding=(int)MathRound(UI_BTN_PADDING*scale);
|
| | |
|
| | | if(btn_fs<7)
|
| | | btn_fs=7;
|
| | | if(btn_h<24)
|
| | | btn_h=24;
|
| | | if(gap<4)
|
| | | gap=4;
|
| | | if(padding<8)
|
| | | padding=8;
|
| | |
|
| | | int x_pos=(int)MathRound(10*scale);
|
| | | int total=ArraySize(BUTTON_NAMES);
|
| | |
|
| | | for(int i=0;i<total;i++)
|
| | | {
|
| | | string name="LC_BTN_"+(string)i;
|
| | |
|
| | | //--- Создать объект,если его еще нет
|
| | | if(ObjectFind(0,name)<0)
|
| | | {
|
| | | if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0))
|
| | | continue;
|
| | | }
|
| | |
|
| | | int text_w=TextWidthPx(BUTTON_NAMES[i],btn_fs);
|
| | | int btn_w=(int)MathRound((text_w+2*padding)*fscale);
|
| | | int min_w=(int)MathRound(UI_BTN_BASE_WIDTH*scale*fscale);
|
| | | if(btn_w<min_w)
|
| | | btn_w=min_w;
|
| | |
|
| | | ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x_pos);
|
| | | ObjectSetInteger(0,name,OBJPROP_YDISTANCE,btn_y);
|
| | | ObjectSetInteger(0,name,OBJPROP_XSIZE,btn_w);
|
| | | ObjectSetInteger(0,name,OBJPROP_YSIZE,btn_h);
|
| | | ObjectSetString(0,name,OBJPROP_FONT,"Arial");
|
| | | ObjectSetString(0,name,OBJPROP_TEXT,BUTTON_NAMES[i]);
|
| | | ObjectSetInteger(0,name,OBJPROP_FONTSIZE,btn_fs);
|
| | | ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| | | ObjectSetInteger(0,name,OBJPROP_STATE,false);
|
| | | ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| | | ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| | |
|
| | | //--- Следующая кнопка сдвигается с учетом фактической ширины
|
| | | x_pos+=btn_w+gap;
|
| | | }
|
| | |
|
| | | //--- Принудительная перерисовка,чтобы панель появилась сразу
|
| | | ChartRedraw(0);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Имя файла экспорта: lifecycle_export_YYYYMMDD_HHMMSS.csv |
|
| | | //+------------------------------------------------------------------+
|
| | | string MakeExportFileName()
|
| | | {
|
| | | MqlDateTime dt;
|
| | | TimeToStruct(TimeCurrent(),dt);
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return(StringFormat("lifecycle_export_%04d%02d%02d_%02d%02d%02d.csv",
|
| | | dt.year,dt.mon,dt.day,dt.hour,dt.min,dt.sec));
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Обработчики кнопок |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnButtonMarketBuy()
|
| | | {
|
| | | Print("Market Buy: BuildRequest -> PreCheck -> OrderSend");
|
| | | SendTradeRequest(ORDER_TYPE_BUY,InpVolume);
|
| | | }
|
| | |
|
| | | void OnButtonMarketSell()
|
| | | {
|
| | | Print("Market Sell: BuildRequest -> PreCheck -> OrderSend");
|
| | | SendTradeRequest(ORDER_TYPE_SELL,InpVolume);
|
| | | }
|
| | |
|
| | | void OnButtonPendingBuyLimit()
|
| | | {
|
| | | Print("Pending Buy Limit: BuildRequest -> PreCheck -> OrderSend");
|
| | | SendTradeRequest(ORDER_TYPE_BUY_LIMIT,InpVolume);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Cancel — отмена отложенного ордера |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnButtonCancel()
|
| | | {
|
| | | ulong ticket=FindFirstPendingOrder();
|
| | | if(ticket==0||!OrderSelect(ticket))
|
| | | {
|
| | | Print("Cancel: нет активных отложенных ордеров");
|
| | | return;
|
| | | }
|
| | |
|
| | | ENUM_ORDER_TYPE otype=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
| | | double vol=OrderGetDouble(ORDER_VOLUME_CURRENT);
|
| | |
|
| | | MqlTradeRequest request;
|
| | | ZeroMemory(request);
|
| | | request.action=TRADE_ACTION_REMOVE;
|
| | | request.order=ticket;
|
| | | request.symbol=ExtSymbol;
|
| | | request.magic=InpMagic;
|
| | | request.comment=StringFormat("LC_%u",ExtLocalSeq+1);
|
| | |
|
| | | PrintFormat("Cancel: отмена ордера %I64u (%s, vol=%.2f)",ticket,EnumToString(otype),vol);
|
| | | SendManageRequest(request,vol,otype);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Modify — модификация отложенного ордера |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnButtonModify()
|
| | | {
|
| | | ulong ticket=FindFirstPendingOrder();
|
| | | if(ticket==0||!OrderSelect(ticket))
|
| | | {
|
| | | Print("Modify: нет активных отложенных ордеров");
|
| | | return;
|
| | | }
|
| | |
|
| | | ENUM_ORDER_TYPE otype=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
| | | double vol=OrderGetDouble(ORDER_VOLUME_CURRENT);
|
| | | double old_price=OrderGetDouble(ORDER_PRICE_OPEN);
|
| | | double point=SymbolInfoDouble(ExtSymbol,SYMBOL_POINT);
|
| | | if(point<=0)
|
| | | point=1e-6;
|
| | |
|
| | | //--- сдвиг в сторону исполнения: buy-лимиты ниже,sell-лимиты выше
|
| | | bool buy_like=(otype==ORDER_TYPE_BUY_LIMIT||otype==ORDER_TYPE_BUY_STOP);
|
| | | double new_price=old_price+(buy_like?-PENDING_DISTANCE_POINTS:PENDING_DISTANCE_POINTS)*point;
|
| | | new_price=NormalizePrice(ExtSymbol,new_price);
|
| | |
|
| | | MqlTradeRequest request;
|
| | | ZeroMemory(request);
|
| | | request.action=TRADE_ACTION_MODIFY;
|
| | | request.order=ticket;
|
| | | request.symbol=ExtSymbol;
|
| | | request.price=new_price;
|
| | | request.magic=InpMagic;
|
| | | request.comment=StringFormat("LC_%u",ExtLocalSeq+1);
|
| | |
|
| | | PrintFormat("Modify: ордер %I64u (%s) цена %.5f -> %.5f",
|
| | | ticket,EnumToString(otype),old_price,new_price);
|
| | | SendManageRequest(request,vol,otype);
|
| | | }
|
| | |
|
| | | void OnButtonExportCsv()
|
| | | {
|
| | | string file_name=MakeExportFileName();
|
| | | ExtLifecycle.ExportCsv(file_name,InpCsvChainEntry);
|
| | | }
|
| | |
|
| | | void OnButtonDrawTimeline()
|
| | | {
|
| | | //--- режим OBJ_LABEL (метки)
|
| | | ExtLifecycle.DrawTimeline(ChartID());
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //| OnButtonClose - закрытие одной позиции за нажатие |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnButtonClose()
|
| | | {
|
| | | int total=PositionsTotal();
|
| | | if(total==0)
|
| | | {
|
| | | Print("OnButtonClose: no open positions");
|
| | | return;
|
| | | }
|
| | |
|
| | | //--- close position index 0; next press closes the next one
|
| | | ulong ticket=PositionGetTicket(0);
|
| | | if(ticket==0)
|
| | | {
|
| | | Print("OnButtonClose: failed to get position");
|
| | | return;
|
| | | }
|
| | |
|
| | | string symbol=PositionGetString(POSITION_SYMBOL);
|
| | | double volume=PositionGetDouble(POSITION_VOLUME);
|
| | | long type=PositionGetInteger(POSITION_TYPE);
|
| | | double price=(type==POSITION_TYPE_BUY?SymbolInfoDouble(symbol,SYMBOL_BID):SymbolInfoDouble(symbol,SYMBOL_ASK));
|
| | |
|
| | | MqlTradeRequest req={};
|
| | | MqlTradeResult res={};
|
| | | req.action=TRADE_ACTION_DEAL;
|
| | | req.symbol=symbol;
|
| | | req.volume=volume;
|
| | | req.type=(type==POSITION_TYPE_BUY?ORDER_TYPE_SELL:ORDER_TYPE_BUY);
|
| | | req.price=price;
|
| | | req.deviation=30;
|
| | | req.magic=InpMagic;
|
| | | req.position=ticket;
|
| | | req.comment="LC_CLOSE";
|
| | |
|
| | | bool ok=(InpUseAsync?OrderSendAsync(req,res):OrderSend(req,res));
|
| | | PrintFormat("OnButtonClose: %s pos=%I64u vol=%.2f | ok=%s retcode=%u '%s'",
|
| | | symbol,ticket,volume,(ok?"true":"false"),res.retcode,res.comment);
|
| | |
|
| | | if(ok)
|
| | | {
|
| | | ExtLocalSeq++;
|
| | | ExtLifecycle.RegisterRequest(ExtLocalSeq,res.request_id,InpMagic,symbol,
|
| | | volume,"LC_CLOSE",GetMicrosecondCount(),req.type);
|
| | | }
|
| | | }
|
| | | //+------------------------------------------------------------------+
|