//+------------------------------------------------------------------+ //| LifecycleBenchmark.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 "Серия замеров задержки исполнения заявок OrderSend() и OrderSendAsync(): send -> REQUEST -> DEAL_ADD" //--- число корзин (столбцов) гистограммы распределения задержек #define HIST_BUCKETS 10 // число корзин (столбцов) гистограммы распределения задержек #define SEG 7 // число сегментов в одной строке статистики (заголовок + 6 полей) //+------------------------------------------------------------------+ //| Режим измерений | //+------------------------------------------------------------------+ enum ENUM_BENCH_MODE { MODE_SYNC_ONLY=0, // только OrderSend() MODE_ASYNC_ONLY=1, // только OrderSendAsync() MODE_BOTH=2 // Sync и Async через один }; //+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ input int InpCount=200; // количество замеров input double InpVolume=0.01; // объем ордера, лот input int InpMagic=20260807; // Magic Number замеров input int InpDeviationPts=20; // допустимое отклонение, пункты input int InpTimeoutMs=5000; // таймаут ожидания сделки, мс input ENUM_BENCH_MODE InpMode=MODE_BOTH; // режим замеров input bool InpExportCsv=true; // экспорт замеров в CSV input double InpFontScale=1.24; // множитель шрифта поверх DPI input int InpBtnFontSize=8; // базовый размер шрифта подписей //+------------------------------------------------------------------+ //| Структура одного замера | //| Хранит временные метки в микросекундах и флаги того, какие | //| События уже получены для этого замера. | //+------------------------------------------------------------------+ struct BenchSample { ulong send_us; // метка перед вызовом OrderSend/OrderSendAsync ulong ack_us; // метка сразу после возврата вызова ulong request_us; // момент TRADE_TRANSACTION_REQUEST ulong deal_us; // момент TRADE_TRANSACTION_DEAL_ADD uint result_retcode; // retcode из события REQUEST (для async-заявок) bool has_request; // флаг: REQUEST получен bool has_deal; // флаг: DEAL_ADD получен bool is_async; // true = замер через OrderSendAsync }; //+------------------------------------------------------------------+ //| Глобальные переменные | //+------------------------------------------------------------------+ BenchSample ExtSamples[]; // массив замеров (динамический, размер = InpCount) string ExtSymbol=""; // символ, на котором идут замеры int ExtRunId=0; // уникальный id прогона (для comment ордеров) int ExtIndex=0; // индекс следующего замера для отправки int ExtWaitIndex=-1; // индекс замера, события которого ждем string ExtWaitComment=""; // comment сопоставляемого замера bool ExtWaiting=false; // флаг: ожидание сделки по активному замеру ulong ExtDeadlineUs=0; // время таймаута активного замера ulong ExtNextAtUs=0; // время старта следующего замера (пауза) int ExtSent=0; // счетчик отправленных замеров int ExtDeals=0; // счетчик полученных сделок bool ExtRunning=false; // флаг: прогон активен //--- диагностические счетчики транзакций int ExtTxRequest=0; // всего REQUEST-транзакций int ExtTxRequestOk=0; // REQUEST, совпавших с активным замером int ExtTxDealAdd=0; // всего DEAL_ADD int ExtTxDealOk=0; // DEAL_ADD, совпавших с активным замером //+------------------------------------------------------------------+ //| Нормализация объема по свойствам символа | //+------------------------------------------------------------------+ double NormalizeVolume(const double volume) { double min_v=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); double max_v=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX); double step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); double v=volume; if(vmax_v) v=max_v; if(step>0) // если шаг задан { v=MathRound(v/step)*step; // округляем до шага int d=(int)MathCeil(-MathLog10(step)); // число знаков после запятой if(d<0) d=0; v=NormalizeDouble(v,d); // аккуратное округление } return(v); } //+------------------------------------------------------------------+ //| Масштаб UI по ширине окна графика. | //| 1.0 соответствует ширине ~900px; границы 0.8..1.6, чтобы шрифт | //| И отступы подстраивались под размер окна терминала. | //+------------------------------------------------------------------+ double GetUIScale() { long chart_w=ChartGetInteger(0,CHART_WIDTH_IN_PIXELS); double s=(chart_w>0?(double)chart_w/900.0:1.0); if(s<0.8) s=0.8; if(s>1.6) s=1.6; return(s); } //+------------------------------------------------------------------+ //| Замер ширины текста в пикселях при шрифте Arial заданного размера| //| Использует TextSetFont+TextGetSize (метрики рендера 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,th=0; if(!TextGetSize(text,tw,th)) return(StringLen(text)*font_size/2+20); // грубая оценка return((int)tw); } //+------------------------------------------------------------------+ //| Выбор типа исполнения по режиму заполнения символа. | //| Предпочитаем FOK (исполнить целиком), затем IOC, иначе RETURN. | //+------------------------------------------------------------------+ ENUM_ORDER_TYPE_FILLING SelectFilling() { const 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); } //+------------------------------------------------------------------+ //| Проверка модели учета позиций счета | //| True=неттинг (одна агрегированная позиция на символ). | //| False=хеджинг (несколько позиций по символу). | //+------------------------------------------------------------------+ bool IsNetting() { return((AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_NETTING)); } //+------------------------------------------------------------------+ //| Закрытие позиции по тикету (синхронно, OrderSend) | //| Закрывает весь объем позиции. Возвращает true, если заявка | //| Принята и исполнена (RETCODE_DONE) либо поставлена (PLACED). | //+------------------------------------------------------------------+ bool ClosePosition(const ulong ticket) { if(!PositionSelectByTicket(ticket)) return(false); const double volume=PositionGetDouble(POSITION_VOLUME); const long type=PositionGetInteger(POSITION_TYPE); MqlTradeRequest req; ZeroMemory(req); req.action=TRADE_ACTION_DEAL; req.symbol=PositionGetString(POSITION_SYMBOL); req.position=ticket; req.volume=NormalizeVolume(volume); req.type=(type==POSITION_TYPE_BUY?ORDER_TYPE_SELL:ORDER_TYPE_BUY); req.deviation=InpDeviationPts; req.type_filling=SelectFilling(); req.magic=InpMagic; req.comment="BM_CLOSE"; MqlTradeResult res; if(!OrderSend(req,res)) return(false); if(res.retcode!=TRADE_RETCODE_DONE&&res.retcode!=TRADE_RETCODE_PLACED) return(false); return(true); } //+------------------------------------------------------------------+ //| Закрытие всех позиций с MagicNumber замеров (очистка хвостов) | //| Перебирает открытые позиции и закрывает только те, где | //| POSITION_MAGIC==InpMagic — то есть созданные этим бенчмарком. | //+------------------------------------------------------------------+ void CloseAllBenchPositions() { int closed=0; for(int i=PositionsTotal()-1;i>=0;i--) // идем с конца массива { const ulong ticket=PositionGetTicket(i); if(ticket==0) continue; // нет тикета — пропускаем if((ulong)PositionGetInteger(POSITION_MAGIC)==InpMagic) if(ClosePosition(ticket)) closed++; } if(closed>0) PrintFormat("закрыто позиций: %d",closed); } //+------------------------------------------------------------------+ //| Минимальное значение по первым count элементам массива | //+------------------------------------------------------------------+ double MinValue(const double &arr[],const int count) { if(count<=0) return(0); double m=arr[0]; for(int i=1;im) m=arr[i]; return(m); } //+------------------------------------------------------------------+ //| Среднее арифметическое по первым count элементам массива | //+------------------------------------------------------------------+ double AvgValue(const double &arr[],const int count) { if(count<=0) return(0); double s=0; for(int i=0;i=InpCount) FinishAndRemove(); return; } //--- таймаут if(GetMicrosecondCount()>=ExtDeadlineUs) { ExtWaiting=false; CloseAllBenchPositions(); ExtNextAtUs=GetMicrosecondCount()+30000; ExtIndex=ExtWaitIndex+1; if(ExtIndex>=InpCount) FinishAndRemove(); return; } //--- еще ждем событие return; } //--- не время следующего if(GetMicrosecondCount()=InpCount) { FinishAndRemove(); return; } //--- запуск очередного замера StartMeasurement(ExtIndex); } //+------------------------------------------------------------------+ //| Один сегмент надписи (короткий OBJ_LABEL) | //+------------------------------------------------------------------+ int SegLabel(const string name,const string text, const int x0,const int y0,const int fs,const color colr) { ObjectCreate(0,name,OBJ_LABEL,0,0,0); ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER); // якорь верх-лево ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x0); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y0); ObjectSetString(0,name,OBJPROP_TEXT,text); ObjectSetString(0,name,OBJPROP_FONT,"Arial"); ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fs); ObjectSetInteger(0,name,OBJPROP_COLOR,colr); // правый край=x0+ширина*мно 1.4 (запас на реальный рендер)+30 зазор return(x0+(int)(TextWidthPx(text,fs)*GetUIScale()*1.4)+30); } //+------------------------------------------------------------------+ //| Гистограмма+строки статистики на графике | //+------------------------------------------------------------------+ void DrawResultsOnChart() { //--- собрать задержки send->DEAL_ADD по группам sync/async double d_sync[]; double d_async[]; int k_sync=0; int k_async=0; for(int i=0;ivmax_r) vmax_r=d_sync[i]; } for(int i=0;ivmax_r) vmax_r=d_async[i]; } if(vmax_r<=vmin_r) vmax_r=vmin_r+1.0; const double step_r=(vmax_r-vmin_r)/HIST_BUCKETS; // ширина корзины //--- построить гистограммы-распределение по корзинам int bins_sync[HIST_BUCKETS]; int bins_async[HIST_BUCKETS]; ZeroMemory(bins_sync); ZeroMemory(bins_async); for(int i=0;i=HIST_BUCKETS) b=HIST_BUCKETS-1; bins_sync[b]++; } for(int i=0;i=HIST_BUCKETS) b=HIST_BUCKETS-1; bins_async[b]++; } //--- геометрия графика const int plot_x1=60; const int plot_x2=700; const int base_y=340; const int max_h=180; int lfs=(int)(InpBtnFontSize*GetUIScale()); // размер шрифта с масштабом if(lfs<7) lfs=7; // минимум const int bar_w=(plot_x2-plot_x1)/(HIST_BUCKETS*2+1); // ширина столбца int max_freq=0; // максимум частоты для нормировки высот for(int b=0;bmax_freq) max_freq=bins_sync[b]; if(bins_async[b]>max_freq) max_freq=bins_async[b]; } if(max_freq<=0) max_freq=1; //--- отрисовка столбцов гистограммы+подписей корзин в мс for(int b=0;bDEAL_ADD (ms)",plot_x1,base_y-max_h-45,lfs,clrBlack); int ty=400; // вертикальная позиция первой строки (меняется в цикле) const int step_y=28; // шаг между строками string t_s[SEG]; // тексты сегментов SYNC/ASYNC string t_a[SEG]; // тексты сегментов ASYNC int c_s[SEG]; // цвета сегментов int c_a[SEG]; // цвета сегментов int w_s[SEG]; // ширины сегментов int w_a[SEG]; // ширины сегментов int xs[SEG]; // левые границы сегментов int xa[SEG]; // левые границы сегментов //--- сформировать тексты и цвета сегментов t_s[0]="SYNC send->DEAL_ADD:"; c_s[0]=clrDarkBlue; t_a[0]="ASYNC send->DEAL_ADD:"; c_a[0]=clrDarkOrange; t_s[1]=" n="+IntegerToString(k_sync); c_s[1]=clrBlack; t_a[1]=" n="+IntegerToString(k_async); c_a[1]=clrBlack; t_s[2]=" мин="+DoubleToString(MinValue(d_sync,k_sync)/1000.0,2); c_s[2]=clrBlack; t_a[2]=" мин="+DoubleToString(MinValue(d_async,k_async)/1000.0,2); c_a[2]=clrBlack; t_s[3]=" ср="+DoubleToString(AvgValue(d_sync,k_sync)/1000.0,2); c_s[3]=clrBlack; t_a[3]=" ср="+DoubleToString(AvgValue(d_async,k_async)/1000.0,2); c_a[3]=clrBlack; t_s[4]=" макс="+DoubleToString(MaxValue(d_sync,k_sync)/1000.0,2); c_s[4]=clrBlack; t_a[4]=" макс="+DoubleToString(MaxValue(d_async,k_async)/1000.0,2); c_a[4]=clrBlack; t_s[5]=" p95="+DoubleToString(Percentile(d_sync,k_sync,95)/1000.0,2); c_s[5]=clrBlack; t_a[5]=" p95="+DoubleToString(Percentile(d_async,k_async,95)/1000.0,2); c_a[5]=clrBlack; t_s[6]=" p99="+DoubleToString(Percentile(d_sync,k_sync,99)/1000.0,2)+" мс"; c_s[6]=clrBlack; t_a[6]=" p99="+DoubleToString(Percentile(d_async,k_async,99)/1000.0,2)+" мс"; c_a[6]=clrBlack; //--- рассчитать ширины сегментов for(int i=0;i0) { int sg=0; for(int i=0;i0) { int sg=0; for(int i=0;is.send_us) send_ack=(long)(s.ack_us-s.send_us); if(s.has_request&&s.request_us>s.send_us) send_request=(long)(s.request_us-s.send_us); if(s.has_deal&&s.deal_us>s.send_us) send_deal=(long)(s.deal_us-s.send_us); if(s.has_request&&s.has_deal&&s.request_us>s.send_us&&s.deal_us>s.send_us) req_deal=(long)((s.request_us>=s.deal_us)? (s.request_us-s.deal_us):(s.deal_us-s.request_us)); FileWrite(handle, i+1, (s.is_async?"async":"sync"), (long)s.send_us, (long)s.ack_us, (long)s.request_us, (long)s.deal_us, (s.has_request?1:0), (s.has_deal?1:0), (long)s.result_retcode, send_ack, send_request, send_deal, req_deal); } FileClose(handle); PrintFormat("LifecycleBenchmark: экспорт -> 'MQL5\\Files\\%s'",file_name); } //+------------------------------------------------------------------+ //| Завершение работы советника и самоликвидация | //+------------------------------------------------------------------+ void FinishAndRemove() { ExtRunning=false; if(ExtSent==0) { Print("нет отправленных замеров"); ExpertRemove(); return; } DrawResultsOnChart(); if(InpExportCsv) ExportMeasurements(); ExpertRemove(); } //+------------------------------------------------------------------+ //| OnInit: выбор символа, выделение массива замеров, очистка старых | //| Объектов/позиций, запуск таймера. | //+------------------------------------------------------------------+ int OnInit() { if(InpCount<=0) { Print("LifecycleBenchmark: InpCount<=0 недопустимо"); return(INIT_PARAMETERS_INCORRECT); } if(InpVolume<=0) { Print("LifecycleBenchmark: InpVolume<=0 недопустимо"); return(INIT_PARAMETERS_INCORRECT); } if(ArrayResize(ExtSamples,InpCount)!=InpCount) { Print("LifecycleBenchmark: не удалось выделить массив замеров"); return(INIT_FAILED); } if(!SymbolSelect(_Symbol,true)) { Print("symbol fail"); return(INIT_FAILED); } for(int i=0;i=InpCount) return; const ulong now=GetMicrosecondCount(); if(trans.type==TRADE_TRANSACTION_REQUEST&&MatchRequestEvent(trans,request)) { ExtTxRequestOk++; ExtSamples[ExtWaitIndex].request_us=now; ExtSamples[ExtWaitIndex].has_request=true; ExtSamples[ExtWaitIndex].result_retcode=result.retcode; } else if(trans.type==TRADE_TRANSACTION_DEAL_ADD&&MatchDealEvent(trans,request)) { ExtTxDealOk++; ExtSamples[ExtWaitIndex].deal_us=now; ExtSamples[ExtWaitIndex].has_deal=true; } } //+------------------------------------------------------------------+