mql5-execution-microstructu.../Include/Dependencies/Article01/Experts/LifecycleBenchmark/LifecycleBenchmark.mq5

834 lines
No EOL
34 KiB
MQL5

//+------------------------------------------------------------------+
//| 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;
//--- ждем события активного замера: сделка или таймаут
if(ExtWaiting)
{
//--- сделка получена
if(ExtSamples[ExtWaitIndex].has_deal)
{
ExtDeals++;
ExtWaiting=false;
CloseAllBenchPositions();
//--- пауза 30 мс
ExtNextAtUs=GetMicrosecondCount()+30000;
ExtIndex=ExtWaitIndex+1;
if(ExtIndex>=InpCount)
FinishAndRemove();
return;
}
//--- таймаут
if(GetMicrosecondCount()>=ExtDeadlineUs)
{
ExtWaiting=false;
CloseAllBenchPositions();
ExtNextAtUs=GetMicrosecondCount()+30000;
ExtIndex=ExtWaitIndex+1;
if(ExtIndex>=InpCount)
FinishAndRemove();
return;
}
//--- еще ждем событие
return;
}
//--- не время следующего
if(GetMicrosecondCount()<ExtNextAtUs)
return;
//--- все замеры отправлены — завершаем
if(ExtIndex>=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;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;
}
}
//+------------------------------------------------------------------+