//+------------------------------------------------------------------+ //| 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" //+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ //--- 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; //--- торговые константы #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(sUI_SCALE_MAX) s=UI_SCALE_MAX; 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) { //--- шрифт не задался — грубая оценка по длине строки if(!TextSetFont("Arial",font_size,0)) return(StringLen(text)*font_size/2+20); uint tw=0; uint th=0; //--- размер метки не получен — грубая оценка if(!TextGetSize(text,tw,th)) return(StringLen(text)*font_size/2+20); return((int)tw); } //+------------------------------------------------------------------+ //| Нормализация цены по SYMBOL_DIGITS | //+------------------------------------------------------------------+ double NormalizePrice(const string symbol,const double price) { int digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); 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(vmax_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); } 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)); 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); 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); } return(ok); } //+------------------------------------------------------------------+ //| FindFirstPendingOrder — первый активный отложенный ордер | //+------------------------------------------------------------------+ ulong FindFirstPendingOrder() { for(int i=0;i0.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 сделка: 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 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()); } //+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+