834 lines
No EOL
34 KiB
MQL5
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;
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+ |