2026-09-10 16:43:28 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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(v<min_v)
|
| | | v=min_v;
|
| | | if(v>max_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;i<count;i++)
|
| | | if(arr[i]<m)
|
| | | m=arr[i];
|
| | | return(m);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Максимальное значение по первым count элементам массива |
|
| | | //+------------------------------------------------------------------+
|
| | | double MaxValue(const double &arr[],const int count)
|
| | | {
|
| | | if(count<=0)
|
| | | return(0);
|
| | | double m=arr[0];
|
| | | for(int i=1;i<count;i++)
|
| | | if(arr[i]>m)
|
| | | 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<count;i++)
|
| | | s+=arr[i];
|
| | | return(s/count);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Перцентиль p (0..100) по первым count элементам массива |
|
| | | //+------------------------------------------------------------------+
|
| | | double Percentile(const double &arr[],const int count,const double p)
|
| | | {
|
| | | if(count<=0)
|
| | | return(0);
|
| | | if(count==1)
|
| | | return(arr[0]);
|
| | | double tmp[];
|
| | | ArrayResize(tmp,count);
|
| | | for(int i=0;i<count;i++)
|
| | | tmp[i]=arr[i];
|
| | | ArraySort(tmp); // сортируем копию
|
| | | const double pos=p/100.0*(count-1); // позиция перцентиля
|
| | | const int lo=(int)MathFloor(pos);
|
| | | const int hi=(int)MathCeil(pos);
|
| | | if(lo==hi)
|
| | | return(tmp[lo]);
|
| | | const double frac=pos-lo;
|
| | | return(tmp[lo]+(tmp[hi]-tmp[lo])*frac); // линейная интерполяция
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Совпадение события REQUEST с ожидаемым замером |
|
| | | //+------------------------------------------------------------------+
|
| | | bool MatchRequestEvent(const MqlTradeTransaction&trans,const MqlTradeRequest&request)
|
| | | {
|
| | | if(request.comment==ExtWaitComment)
|
| | | return(true);
|
| | | HistorySelect(0,TimeCurrent()); // гарантия выбора истории для HistoryOrderSelect
|
| | | if(trans.order!=0&&HistoryOrderSelect(trans.order))
|
| | | if((ulong)HistoryOrderGetInteger(trans.order,ORDER_MAGIC)==InpMagic&&
|
| | | HistoryOrderGetString(trans.order,ORDER_COMMENT)==ExtWaitComment)
|
| | | return(true);
|
| | | return(false);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Совпадение события DEAL_ADD с ожидаемым замером |
|
| | | //| Проверка по comment запроса и по сделке: идеал комм/магик/вход |
|
| | | //| DEAL_ENTRY_IN (вход в позицию), чтобы не спутать со сделкой |
|
| | | //| Закрытия. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool MatchDealEvent(const MqlTradeTransaction&trans,const MqlTradeRequest&request)
|
| | | {
|
| | | if(request.comment==ExtWaitComment)
|
| | | return(true);
|
| | | const ulong d=trans.deal;
|
| | | HistorySelect(0,TimeCurrent()); // гарантия, что истории сделка уже есть
|
| | | if(d!=0&&HistoryDealSelect(d))
|
| | | if(HistoryDealGetString(d,DEAL_COMMENT)==ExtWaitComment&&
|
| | | (ulong)HistoryDealGetInteger(d,DEAL_MAGIC)==InpMagic&&
|
| | | HistoryDealGetInteger(d,DEAL_ENTRY)==DEAL_ENTRY_IN)
|
| | | return(true);
|
| | | return(false);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Определяет режим замера (sync или async) по индексу i |
|
| | | //+------------------------------------------------------------------+
|
| | | bool IsAsyncSample(const int i)
|
| | | {
|
| | | if(InpMode==MODE_SYNC_ONLY)
|
| | | return(false);
|
| | | if(InpMode==MODE_ASYNC_ONLY)
|
| | | return(true);
|
| | | return((i%2==1));
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Отправка одного замера: рыночный BUY с уникальным comment |
|
| | | //+------------------------------------------------------------------+
|
| | | void StartMeasurement(const int i)
|
| | | {
|
| | | const string comment=StringFormat("BM_%d_%d",ExtRunId,i); // уникальный comment
|
| | | const bool async=IsAsyncSample(i);
|
| | |
|
| | | MqlTradeRequest req;
|
| | | ZeroMemory(req);
|
| | | req.action=TRADE_ACTION_DEAL;
|
| | | req.symbol=ExtSymbol;
|
| | | req.volume=NormalizeVolume(InpVolume);
|
| | | req.type=ORDER_TYPE_BUY;
|
| | | req.type_filling=SelectFilling();
|
| | | req.magic=InpMagic;
|
| | | req.comment=comment;
|
| | |
|
| | | MqlTradeResult res;
|
| | | ExtSamples[i].is_async=async;
|
| | | ExtSamples[i].send_us=GetMicrosecondCount(); // метка ДО вызова
|
| | | const bool ok=(async?OrderSendAsync(req,res):OrderSend(req,res));
|
| | | ExtSamples[i].ack_us=GetMicrosecondCount(); // метка ПОСЛЕ вызова
|
| | |
|
| | | if(!ok) // не удалось отправить — переходим к следующему
|
| | | {
|
| | | ExtIndex=i+1;
|
| | | ExtNextAtUs=GetMicrosecondCount()+100000;
|
| | | return;
|
| | | }
|
| | | ExtSent++; // успешно отправлено
|
| | | ExtWaitIndex=i;
|
| | | ExtWaitComment=comment;
|
| | | ExtWaiting=true;
|
| | | ExtDeadlineUs=GetMicrosecondCount()+(ulong)InpTimeoutMs*1000;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Основной цикл решения (вызывается из OnTick/OnTimer) |
|
| | | //+------------------------------------------------------------------+
|
| | | void Pump()
|
| | | {
|
| | | if(!ExtRunning)
|
| | | return;
|
2026-09-16 15:37:55 +03:00 | | | //--- ждем события активного замера: сделка или таймаут
|
2026-09-10 16:43:28 +03:00 | | | if(ExtWaiting)
|
| | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- сделка получена
|
| | | if(ExtSamples[ExtWaitIndex].has_deal)
|
2026-09-10 16:43:28 +03:00 | | | {
|
| | | ExtDeals++;
|
| | | ExtWaiting=false;
|
| | | CloseAllBenchPositions();
|
2026-09-16 15:37:55 +03:00 | | | //--- пауза 30 мс
|
| | | ExtNextAtUs=GetMicrosecondCount()+30000;
|
2026-09-10 16:43:28 +03:00 | | | ExtIndex=ExtWaitIndex+1;
|
| | | if(ExtIndex>=InpCount)
|
| | | FinishAndRemove();
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- таймаут
|
| | | if(GetMicrosecondCount()>=ExtDeadlineUs)
|
2026-09-10 16:43:28 +03:00 | | | {
|
| | | ExtWaiting=false;
|
| | | CloseAllBenchPositions();
|
| | | ExtNextAtUs=GetMicrosecondCount()+30000;
|
| | | ExtIndex=ExtWaitIndex+1;
|
| | | if(ExtIndex>=InpCount)
|
| | | FinishAndRemove();
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- еще ждем событие
|
| | |
|
| | | return;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- не время следующего
|
2026-09-10 16:43:28 +03:00 | | | if(GetMicrosecondCount()<ExtNextAtUs)
|
2026-09-16 15:37:55 +03:00 | | | return;
|
| | | //--- все замеры отправлены — завершаем
|
2026-09-10 16:43:28 +03:00 | | | if(ExtIndex>=InpCount)
|
| | | {
|
| | | FinishAndRemove();
|
2026-09-16 15:37:55 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | return;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- запуск очередного замера
|
2026-09-10 16:43:28 +03:00 | | | 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;i<InpCount;i++)
|
| | | {
|
| | | if(!ExtSamples[i].has_deal||ExtSamples[i].deal_us<=ExtSamples[i].send_us)
|
| | | continue;
|
| | | const double dd=(double)(ExtSamples[i].deal_us-ExtSamples[i].send_us);
|
| | | if(ExtSamples[i].is_async)
|
| | | {
|
| | | ArrayResize(d_async,k_async+1);
|
| | | d_async[k_async++]=dd;
|
| | | }
|
| | | else
|
| | | {
|
| | | ArrayResize(d_sync,k_sync+1);
|
| | | d_sync[k_sync++]=dd;
|
| | | }
|
| | | }
|
| | | if(k_sync==0&&k_async==0)
|
| | | {
|
| | | Print("нет данных");
|
| | | return;
|
| | | }
|
| | |
|
| | | //--- очистить старые объекты бенчмарка
|
| | | ObjectsDeleteAll(0,"LC_BENCH_");
|
| | | ObjectsDeleteAll(0,"LC_BAR_");
|
| | | ObjectsDeleteAll(0,"LC_SS_");
|
| | | ObjectsDeleteAll(0,"LC_AA_");
|
| | | ObjectsDeleteAll(0,"LC_CC_");
|
| | |
|
| | | //--- определить диапазон (min/max) задержек для корзин
|
| | | double vmin_r=1e18;
|
| | | double vmax_r=-1e18;
|
| | | for(int i=0;i<k_sync;i++)
|
| | | {
|
| | | if(d_sync[i]<vmin_r)
|
| | | vmin_r=d_sync[i];
|
| | | if(d_sync[i]>vmax_r)
|
| | | vmax_r=d_sync[i];
|
| | | }
|
| | | for(int i=0;i<k_async;i++)
|
| | | {
|
| | | if(d_async[i]<vmin_r)
|
| | | vmin_r=d_async[i];
|
| | | if(d_async[i]>vmax_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<k_sync;i++)
|
| | | {
|
| | | int b=(int)((d_sync[i]-vmin_r)/step_r);
|
| | | if(b>=HIST_BUCKETS)
|
| | | b=HIST_BUCKETS-1;
|
| | | bins_sync[b]++;
|
| | | }
|
| | | for(int i=0;i<k_async;i++)
|
| | | {
|
| | | int b=(int)((d_async[i]-vmin_r)/step_r);
|
| | | if(b>=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;b<HIST_BUCKETS;b++)
|
| | | {
|
| | | if(bins_sync[b]>max_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;b<HIST_BUCKETS;b++)
|
| | | {
|
| | | const int x0=plot_x1+b*(bar_w*2+1);
|
| | | const int h_s=(int)((double)bins_sync[b]/max_freq*max_h);
|
| | | const int x1=x0+bar_w+1;
|
| | | const int h_a=(int)((double)bins_async[b]/max_freq*max_h);
|
| | | SegRect("LC_BAR_S_"+IntegerToString(b),x0,base_y-h_s,bar_w,h_s,clrDarkBlue); // sync
|
| | | SegRect("LC_BAR_A_"+IntegerToString(b),x1,base_y-h_a,bar_w,h_a,clrDarkOrange); // async
|
| | | SegFree("LC_BENCH_BIN_"+IntegerToString(b),StringFormat("%.1f",(vmin_r+b*step_r)/1000.0),x0,base_y+5,8,clrDimGray); // шкала мс
|
| | | }
|
| | |
|
| | | //--- легенда над графиком
|
| | | SegFree("LC_BENCH_LEGEND","SYNC / ASYNC send->DEAL_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;i<SEG;i++)
|
| | | {
|
| | | w_s[i]=(int)(TextWidthPx(t_s[i],lfs)*GetUIScale()*1.4)+30;
|
| | | w_a[i]=(int)(TextWidthPx(t_a[i],lfs)*GetUIScale()*1.4)+30;
|
| | | }
|
| | |
|
| | | //--- цепочки левых границ для каждой строки
|
| | | xs[0]=20;
|
| | | xa[0]=20;
|
| | | for(int i=1;i<SEG;i++)
|
| | | {
|
| | | xs[i]=xs[i-1]+w_s[i-1];
|
| | | xa[i]=xa[i-1]+w_a[i-1];
|
| | | }
|
| | |
|
| | | //--- выравнивание: для каждой позиции берем максимум из двух правых границ
|
| | | for(int i=0;i<SEG;i++)
|
| | | {
|
| | | if(i==0)
|
| | | {
|
| | | xs[i]=20;
|
| | | xa[i]=20;
|
| | | }
|
| | | else
|
| | | {
|
| | | const int m=MathMax(xs[i],xa[i]);
|
| | | xs[i]=m;
|
| | | xa[i]=m;
|
| | | }
|
| | | }
|
| | |
|
| | | //--- отрисовка строк SYNC и ASYNC
|
| | | if(k_sync>0)
|
| | | {
|
| | | int sg=0;
|
| | | for(int i=0;i<SEG;i++)
|
| | | SegLabel("LC_SS_"+IntegerToString(sg++),t_s[i],xs[i],ty,lfs,c_s[i]);
|
| | | }
|
| | | ty+=step_y;
|
| | | if(k_async>0)
|
| | | {
|
| | | int sg=0;
|
| | | for(int i=0;i<SEG;i++)
|
| | | SegLabel("LC_AA_"+IntegerToString(sg++),t_a[i],xa[i],ty,lfs,c_a[i]);
|
| | | }
|
| | |
|
| | | ChartRedraw(0);
|
| | | PrintFormat("нарисовано: sync n=%d,async n=%d",k_sync,k_async);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Создание залитого прямоугольника (столбец гистограммы) |
|
| | | //+------------------------------------------------------------------+
|
| | | void SegRect(const string name,const int x,const int y,const int w,const int h,const color c)
|
| | | {
|
| | | ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0);
|
| | | ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| | | ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
|
| | | ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
|
| | | ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
|
| | | ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
|
| | | ObjectSetInteger(0,name,OBJPROP_BGCOLOR,c);
|
| | | ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
|
| | | ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrBlack);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Свободная простая метка OBJ_LABEL (для легенды и подписей корзин)|
|
| | | //+------------------------------------------------------------------+
|
| | | void SegFree(const string name,const string text,const int x,const int y,const int fs,const color col)
|
| | | {
|
| | | ObjectCreate(0,name,OBJ_LABEL,0,0,0);
|
| | | ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
|
| | | ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
|
| | | ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
|
| | | ObjectSetString(0,name,OBJPROP_TEXT,text);
|
| | | ObjectSetString(0,name,OBJPROP_FONT,"Arial");
|
| | | ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fs);
|
| | | ObjectSetInteger(0,name,OBJPROP_COLOR,col);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Экспорт замеров в CSV-файл (в каталог Files) |
|
| | | //+------------------------------------------------------------------+
|
| | | void ExportMeasurements()
|
| | | {
|
| | | MqlDateTime dt;
|
| | | TimeToStruct(TimeCurrent(),dt);
|
| | | const string file_name=StringFormat("benchmark_export_%04d%02d%02d_%02d%02d%02d.csv",
|
| | | dt.year,dt.mon,dt.day,dt.hour,dt.min,dt.sec);
|
| | | int handle=FileOpen(file_name,FILE_WRITE|FILE_READ|FILE_CSV|FILE_ANSI,',');
|
| | | if(handle==INVALID_HANDLE)
|
| | | {
|
| | | Print("LifecycleBenchmark: не удалось открыть CSV");
|
| | | return;
|
| | | }
|
| | | FileWrite(handle,"seq","mode",
|
| | | "send_us","ack_us","request_us","deal_us",
|
| | | "has_request","has_deal","retcode",
|
| | | "send_ack_us","send_request_us","send_deal_us","req_deal_us");
|
| | | for(int i=0;i<InpCount;i++)
|
| | | {
|
| | | const BenchSample s=ExtSamples[i];
|
| | | long send_ack=0;
|
| | | long send_request=0;
|
| | | long send_deal=0;
|
| | | long req_deal=0;
|
| | | if(s.ack_us>s.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;i++)
|
| | | ZeroMemory(ExtSamples[i]);
|
| | | ExtSymbol=_Symbol;
|
| | | ExtRunId=(int)TimeLocal();
|
| | | //--- удалить старые объекты бенчмарка и закрыть хвосты прошлых прогонов
|
| | | ObjectsDeleteAll(0,"LC_BENCH_");
|
| | | ObjectsDeleteAll(0,"LC_BAR_");
|
| | | ObjectsDeleteAll(0,"LC_SS_");
|
| | | ObjectsDeleteAll(0,"LC_AA_");
|
| | | ObjectsDeleteAll(0,"LC_CC_");
|
| | | CloseAllBenchPositions();
|
| | | ExtNextAtUs=GetMicrosecondCount()+500000; // стартовая пауза 0.5 с
|
| | | ExtRunning=true;
|
| | | EventSetMillisecondTimer(10);
|
| | | return(INIT_SUCCEEDED);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnDeinit: остановка таймера и закрытие незакрытых позиций. |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnDeinit(const int reason)
|
| | | {
|
| | | EventKillTimer();
|
| | | if(ExtRunning)
|
| | | CloseAllBenchPositions();
|
| | | ExtRunning=false;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnTick / OnTimer: продвижение автомата. |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnTick(){Pump();}
|
| | | void OnTimer(){Pump();}
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnTradeTransaction: снятие меток REQUEST и DEAL_ADD для активного|
|
| | | //| Замера. Сопоставление события по comment. |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnTradeTransaction(const MqlTradeTransaction&trans,
|
| | | const MqlTradeRequest&request,
|
| | | const MqlTradeResult&result)
|
| | | {
|
| | | //--- считаем все транзакции
|
| | | if(trans.type==TRADE_TRANSACTION_REQUEST)
|
| | | ExtTxRequest++;
|
| | | else
|
| | | if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
|
| | | ExtTxDealAdd++;
|
| | |
|
| | | if(!ExtRunning)
|
| | | return;
|
| | | if(ExtWaitIndex<0||ExtWaitIndex>=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;
|
| | | }
|
| | | }
|
| | | //+------------------------------------------------------------------+
|