894 lines
No EOL
34 KiB
MQL5
894 lines
No EOL
34 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| OrderLifecycleRecorder.mq5 |
|
|
//| Copyright 2000-2026, MetaQuotes Ltd. |
|
|
//| www.mql5.com |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2000-2026, MetaQuotes Ltd."
|
|
#property link "https://www.mql5.com"
|
|
#property version "1.00"
|
|
#property description "Вывод полного жизненного цикла заявки:"
|
|
#property description "запрос, ордер, сделка, позиция."
|
|
#property description "Накопление событий, reconciliation, CSV, timeline."
|
|
|
|
#include "TradeLifecycle.mqh"
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Входные параметры |
|
|
//+------------------------------------------------------------------+
|
|
//--- Magic Number
|
|
input ulong InpMagic=20260806;
|
|
//--- Объем тестовой заявки,лот
|
|
input double InpVolume=0.01;
|
|
//--- символ (пусто=текущий график)
|
|
input string InpSymbol="";
|
|
//--- OrderSendAsync() вместо OrderSend()
|
|
input bool InpUseAsync=true;
|
|
//--- таймаут ожидания события,мс
|
|
input int InpReconcileTimeoutMs=5000;
|
|
//--- число повторов запроса при отказе (0=без ограничений)
|
|
input int InpRetries=0;
|
|
//--- пауза между повторами,мс
|
|
input int InpRetryDelayMs=5000;
|
|
//--- entry/reason на всю цепочку в CSV (не только DEAL_ADD)
|
|
input bool InpCsvChainEntry=true;
|
|
//--- размер шрифта кнопок (базовый,масштабируется по ширине окна)
|
|
input int InpBtnFontSize=10;
|
|
//--- доп. множитель шрифта поверх DPI (1.0=только авто)
|
|
input double InpFontScale=1.24;
|
|
|
|
//--- торговые константы
|
|
#define TRADE_DEVIATION_POINTS 20 // допустимое отклонение для рыночного ордера, пункты
|
|
#define PENDING_DISTANCE_POINTS 100 // расстояние до отложенного ордера от рынка, пункты
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Имена кнопок |
|
|
//+------------------------------------------------------------------+
|
|
const string BUTTON_NAMES[]=
|
|
{
|
|
"Market Buy", // 0 — покупка по рынку
|
|
"Market Sell", // 1 — продажа по рынку
|
|
"Pending Buy Limit", // 2 — отложенный ордер Buy Limit
|
|
"Cancel", // 3 — отмена первого отложенного ордера
|
|
"Modify", // 4 — модификация отложенного ордера
|
|
"Export CSV", // 5 — экспорт событий в CSV
|
|
"Draw Timeline", // 6 — построение timeline на графике
|
|
"Close" // 7 — закрытие позиций по одной за нажатие
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Базовые размеры панели (при макете окна ~900px) |
|
|
//+------------------------------------------------------------------+
|
|
#define UI_BTN_BASE_WIDTH 70 // минимальная ширина кнопки
|
|
#define UI_BTN_BASE_HEIGHT 36 // высота кнопки
|
|
#define UI_BTN_BASE_Y 44 // отступ сверху
|
|
#define UI_BTN_PADDING 16 // внутреннее поле кнопки слева/справа
|
|
#define UI_BTN_GAP 6 // зазор между кнопками
|
|
#define UI_PANEL_BASE_W 720 // эталонная ширина панели при scale=1.0
|
|
#define UI_SCALE_MIN 0.75 // нижняя граница масштаба
|
|
#define UI_SCALE_MAX 1.50 // верхняя граница масштаба
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Глобальные переменные |
|
|
//+------------------------------------------------------------------+
|
|
CTradeLifecycle ExtLifecycle; // накопитель событий+реестр запросов
|
|
string ExtSymbol; // рабочий символ
|
|
ulong ExtStartUs; // GetMicrosecondCount() для стартовой метки
|
|
bool ExtHasEvents; // появились ли события
|
|
ulong ExtLastEventUs; // GetMicrosecondCount() последнего события
|
|
bool ExtWatchdogFired; // watchdog уже сработал в этом «тихом» периоде
|
|
double ExtUiScale; // текущий масштаб панели
|
|
uint ExtLocalSeq; // локальный порядковый номер отправленных запросов
|
|
//+------------------------------------------------------------------+
|
|
//| Состояние повторов запроса (InpRetries) |
|
|
//+------------------------------------------------------------------+
|
|
ENUM_ORDER_TYPE ExtRetryType; // тип ордера для повтора
|
|
bool ExtRetryPending; // запланирован ли повтор
|
|
ulong ExtRetryAtUs; // момент повтора (GetMicrosecondCount)
|
|
int ExtRetriesLeft; // осталось повторов (-1=без ограничений)
|
|
//+------------------------------------------------------------------+
|
|
//| Масштаб UI по ширине окна графика: панель не должна выходить |
|
|
//| За пределы окна (UI_PANEL_BASE_W — ширина панели при scale=1.0),|
|
|
//| С границами UI_SCALE_MIN..UI_SCALE_MAX. |
|
|
//+------------------------------------------------------------------+
|
|
double GetUIScale()
|
|
{
|
|
long chart_w=ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
|
|
|
|
double s=(chart_w>0?(double)chart_w/UI_PANEL_BASE_W:1.0);
|
|
|
|
if(s<UI_SCALE_MIN)
|
|
s=UI_SCALE_MIN;
|
|
if(s>UI_SCALE_MAX)
|
|
s=UI_SCALE_MAX;
|
|
|
|
return(s);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Итоговый коэффициент масштабирования шрифта |
|
|
//+------------------------------------------------------------------+
|
|
double GetFontScale()
|
|
{
|
|
double base=(InpFontScale>0.1?InpFontScale:1.0);
|
|
|
|
int dpi=(int)TerminalInfoInteger(TERMINAL_SCREEN_DPI);
|
|
if(dpi<=0)
|
|
return(base);
|
|
|
|
return(base*(double)dpi/96.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Измерение ширины надписи по метрикам шрифта MQL5 |
|
|
//+------------------------------------------------------------------+
|
|
int TextWidthPx(const string text,const int font_size)
|
|
{
|
|
//--- шрифт не задался — грубая оценка по длине строки
|
|
if(!TextSetFont("Arial",font_size,0))
|
|
return(StringLen(text)*font_size/2+20);
|
|
|
|
uint tw=0;
|
|
uint th=0;
|
|
//--- размер метки не получен — грубая оценка
|
|
if(!TextGetSize(text,tw,th))
|
|
return(StringLen(text)*font_size/2+20);
|
|
|
|
return((int)tw);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Нормализация цены по SYMBOL_DIGITS |
|
|
//+------------------------------------------------------------------+
|
|
double NormalizePrice(const string symbol,const double price)
|
|
{
|
|
int digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
|
|
|
|
return(NormalizeDouble(price,digits));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Нормализация объема: диапазон SYMBOL_VOLUME_MIN..MAX и шаг STEP |
|
|
//+------------------------------------------------------------------+
|
|
double NormalizeVolume(const string symbol,const double volume)
|
|
{
|
|
double min_v=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN);
|
|
double max_v=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MAX);
|
|
double step_v=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP);
|
|
|
|
double v=volume;
|
|
if(v<min_v)
|
|
v=min_v;
|
|
if(v>max_v)
|
|
v=max_v;
|
|
if(step_v>0)
|
|
{
|
|
v=MathRound(v/step_v)*step_v;
|
|
int vdigits=(int)MathCeil(-MathLog10(step_v));
|
|
if(vdigits<0)
|
|
vdigits=0;
|
|
v=NormalizeDouble(v,vdigits);
|
|
}
|
|
|
|
return(v);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Выбор режима исполнения из SYMBOL_FILLING_MODE |
|
|
//+------------------------------------------------------------------+
|
|
ENUM_ORDER_TYPE_FILLING SelectFilling(const string symbol)
|
|
{
|
|
long mode=SymbolInfoInteger(symbol,SYMBOL_FILLING_MODE);
|
|
|
|
if((mode&SYMBOL_FILLING_FOK)!=0)
|
|
return(ORDER_FILLING_FOK);
|
|
if((mode&SYMBOL_FILLING_IOC)!=0)
|
|
return(ORDER_FILLING_IOC);
|
|
return(ORDER_FILLING_RETURN);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| BuildRequest — заполнение MqlTradeRequest |
|
|
//+------------------------------------------------------------------+
|
|
bool BuildRequest(const string symbol,const ENUM_ORDER_TYPE type,
|
|
const double volume,MqlTradeRequest &request)
|
|
{
|
|
ZeroMemory(request);
|
|
|
|
bool is_market=(type==ORDER_TYPE_BUY||type==ORDER_TYPE_SELL);
|
|
request.action=(is_market?TRADE_ACTION_DEAL:TRADE_ACTION_PENDING);
|
|
request.magic=InpMagic;
|
|
request.symbol=symbol;
|
|
request.type=type;
|
|
request.volume=NormalizeVolume(symbol,volume);
|
|
request.deviation=TRADE_DEVIATION_POINTS;
|
|
request.type_time=ORDER_TIME_GTC;
|
|
request.type_filling=SelectFilling(symbol);
|
|
request.comment=StringFormat("LC_%u",ExtLocalSeq+1);
|
|
|
|
//--- цена: рыночная — Ask/Bid; отложенная — с отступом от рынка
|
|
double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);
|
|
double bid=SymbolInfoDouble(symbol,SYMBOL_BID);
|
|
double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
|
|
if(point<=0)
|
|
point=1e-6;
|
|
|
|
switch(type)
|
|
{
|
|
case ORDER_TYPE_BUY:
|
|
request.price=ask;
|
|
break;
|
|
case ORDER_TYPE_SELL:
|
|
request.price=bid;
|
|
break;
|
|
case ORDER_TYPE_BUY_LIMIT:
|
|
request.price=ask-PENDING_DISTANCE_POINTS*point;
|
|
break;
|
|
case ORDER_TYPE_SELL_LIMIT:
|
|
request.price=bid+PENDING_DISTANCE_POINTS*point;
|
|
break;
|
|
default:
|
|
PrintFormat("BuildRequest: тип '%s' пока не поддерживается",EnumToString(type));
|
|
return(false);
|
|
}
|
|
|
|
request.price=NormalizePrice(symbol,request.price);
|
|
|
|
PrintFormat("BuildRequest: %s %s vol=%.2f price=%.5f filling=%s",
|
|
EnumToString(type),symbol,request.volume,request.price,
|
|
EnumToString(request.type_filling));
|
|
|
|
return(true);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| PreCheck — предварительная проверка OrderCheck |
|
|
//+------------------------------------------------------------------+
|
|
bool PreCheck(const MqlTradeRequest &request,MqlTradeCheckResult &check_result)
|
|
{
|
|
ZeroMemory(check_result);
|
|
|
|
if(!OrderCheck(request,check_result))
|
|
{
|
|
PrintFormat("PreCheck: отказ | retcode=%u '%s'",check_result.retcode,check_result.comment);
|
|
return(false);
|
|
}
|
|
|
|
PrintFormat("PreCheck: ок | retcode=%u margin_free=%.2f",check_result.retcode,check_result.margin_free);
|
|
|
|
return(true);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| ScheduleRetry — планирование повтора запроса (InpRetries) |
|
|
//+------------------------------------------------------------------+
|
|
void ScheduleRetry(const ENUM_ORDER_TYPE type)
|
|
{
|
|
if(InpRetries==0||ExtRetriesLeft>0)
|
|
{
|
|
if(ExtRetriesLeft>0)
|
|
ExtRetriesLeft--;
|
|
|
|
ExtRetryType=type;
|
|
ExtRetryPending=true;
|
|
ExtRetryAtUs=GetMicrosecondCount()+(ulong)InpRetryDelayMs*1000;
|
|
|
|
PrintFormat("ScheduleRetry: %s через %d мс | осталось повторов: %s",
|
|
EnumToString(type),InpRetryDelayMs,
|
|
(InpRetries==0?"без ограничений":(string)ExtRetriesLeft));
|
|
}
|
|
else
|
|
{
|
|
PrintFormat("ScheduleRetry: лимит повторов исчерпан (%d), %s не повторяем",
|
|
InpRetries,EnumToString(type));
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| SendTradeRequest — BuildRequest -> PreCheck -> OrderSend/Async |
|
|
//+------------------------------------------------------------------+
|
|
bool SendTradeRequest(const ENUM_ORDER_TYPE type,const double volume)
|
|
{
|
|
MqlTradeRequest request;
|
|
if(!BuildRequest(ExtSymbol,type,volume,request))
|
|
return(false);
|
|
|
|
MqlTradeCheckResult check_result;
|
|
if(!PreCheck(request,check_result))
|
|
{
|
|
ScheduleRetry(type);
|
|
return(false);
|
|
}
|
|
|
|
MqlTradeResult result;
|
|
ulong t0=GetMicrosecondCount();
|
|
bool ok=(InpUseAsync?OrderSendAsync(request,result):OrderSend(request,result));
|
|
|
|
PrintFormat("SendTradeRequest: %s | ok=%s retcode=%u '%s' request_id=%u dt=%I64u мкс",
|
|
EnumToString(type),
|
|
(ok?"true":"false"),
|
|
result.retcode,result.comment,result.request_id,
|
|
GetMicrosecondCount()-t0);
|
|
|
|
if(ok)
|
|
{
|
|
ExtLocalSeq++;
|
|
ExtLifecycle.RegisterRequest(ExtLocalSeq,result.request_id,InpMagic,ExtSymbol,
|
|
request.volume,request.comment,t0,type);
|
|
}
|
|
else
|
|
{
|
|
//--- отправка не удалась (нет соединения и т.п.) — планируем повтор
|
|
ScheduleRetry(type);
|
|
}
|
|
|
|
return(ok);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| FindFirstPendingOrder — первый активный отложенный ордер |
|
|
//+------------------------------------------------------------------+
|
|
ulong FindFirstPendingOrder()
|
|
{
|
|
for(int i=0;i<OrdersTotal();i++)
|
|
{
|
|
ulong ticket=OrderGetTicket(i);
|
|
if(ticket==0)
|
|
continue;
|
|
if(OrderGetString(ORDER_SYMBOL)!=ExtSymbol)
|
|
continue;
|
|
if((ulong)OrderGetInteger(ORDER_MAGIC)!=InpMagic)
|
|
continue;
|
|
|
|
long type=OrderGetInteger(ORDER_TYPE);
|
|
if(type==ORDER_TYPE_BUY_LIMIT||type==ORDER_TYPE_SELL_LIMIT||
|
|
type==ORDER_TYPE_BUY_STOP||type==ORDER_TYPE_SELL_STOP||
|
|
type==ORDER_TYPE_BUY_STOP_LIMIT||type==ORDER_TYPE_SELL_STOP_LIMIT)
|
|
return(ticket);
|
|
}
|
|
|
|
return(0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| SendManageRequest — отправка управляющего запроса (REMOVE/MODIFY)|
|
|
//+------------------------------------------------------------------+
|
|
bool SendManageRequest(MqlTradeRequest &request,const double volume,
|
|
const ENUM_ORDER_TYPE order_type)
|
|
{
|
|
MqlTradeCheckResult check_result;
|
|
if(!PreCheck(request,check_result))
|
|
return(false);
|
|
|
|
MqlTradeResult result;
|
|
ulong t0=GetMicrosecondCount();
|
|
bool ok=(InpUseAsync?OrderSendAsync(request,result):OrderSend(request,result));
|
|
|
|
PrintFormat("SendManageRequest: %s order=%I64u | ok=%s retcode=%u '%s' request_id=%u dt=%I64u мкс",
|
|
EnumToString(request.action),request.order,
|
|
(ok?"true":"false"),
|
|
result.retcode,result.comment,result.request_id,
|
|
GetMicrosecondCount()-t0);
|
|
|
|
if(ok)
|
|
{
|
|
ExtLocalSeq++;
|
|
ExtLifecycle.RegisterRequest(ExtLocalSeq,result.request_id,InpMagic,ExtSymbol,
|
|
volume,request.comment,t0,order_type);
|
|
}
|
|
|
|
return(ok);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnInit |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
ExtSymbol=(InpSymbol==""?_Symbol:InpSymbol);
|
|
ExtStartUs=GetMicrosecondCount();
|
|
ExtHasEvents=false;
|
|
ExtLastEventUs=0;
|
|
ExtWatchdogFired=false;
|
|
ExtUiScale=0.0;
|
|
ExtLocalSeq=0;
|
|
|
|
//--- состояние повторов
|
|
ExtRetryPending=false;
|
|
ExtRetryAtUs=0;
|
|
ExtRetriesLeft=(InpRetries==0?-1:InpRetries);
|
|
|
|
//--- подписка на символ
|
|
if(!SymbolSelect(ExtSymbol,true))
|
|
PrintFormat("OnInit: не удалось выбрать символ %s (error=%d)",ExtSymbol,GetLastError());
|
|
|
|
//--- контекст для reconciliation (символ+magic)
|
|
ExtLifecycle.SetContext(ExtSymbol,InpMagic);
|
|
|
|
//--- создание кнопок
|
|
CreateButtons();
|
|
ExtUiScale=GetUIScale();
|
|
LogButtonWidths();
|
|
|
|
//--- таймер для запуска reconciliation
|
|
EventSetTimer(1);
|
|
|
|
PrintFormat("OrderLifecycleRecorder: init | symbol=%s magic=%I64u volume=%.2f async=%s ui_scale=%.2f font_scale=%.2f (dpi=%d, mult=%.2f)",
|
|
ExtSymbol,InpMagic,InpVolume,(InpUseAsync?"true":"false"),ExtUiScale,
|
|
GetFontScale(),(int)TerminalInfoInteger(TERMINAL_SCREEN_DPI),
|
|
(InpFontScale>0.1?InpFontScale:1.0));
|
|
|
|
//--- модель учета позиций
|
|
PrintFormat("Модель учета позиций: %s",
|
|
EnumToString((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)));
|
|
|
|
//--- сводка для проверки доступности
|
|
PrintEntityInfo(ExtSymbol);
|
|
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnDeinit |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
//--- экспорт журнала при завершении
|
|
if(reason!=REASON_CHARTCHANGE&&reason!=REASON_RECOMPILE)
|
|
{
|
|
string file_name=MakeExportFileName();
|
|
ExtLifecycle.ExportCsv(file_name,InpCsvChainEntry);
|
|
}
|
|
|
|
EventKillTimer();
|
|
|
|
//--- удаление графических объектов (кнопки+timeline)
|
|
int total=ArraySize(BUTTON_NAMES);
|
|
for(int i=0;i<total;i++)
|
|
ObjectDelete(0,"LC_BTN_"+(string)i);
|
|
ObjectsDeleteAll(0,"LC_TL_");
|
|
|
|
Print("OrderLifecycleRecorder: завершение");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnTradeTransaction — регистрация событий жизненного цикла заявки |
|
|
//+------------------------------------------------------------------+
|
|
void OnTradeTransaction(const MqlTradeTransaction &trans,
|
|
const MqlTradeRequest &request,
|
|
const MqlTradeResult &result)
|
|
{
|
|
//--- фильтр по символу (если транзакция не глобальная)
|
|
if(trans.symbol!=""&&trans.symbol!=ExtSymbol)
|
|
return;
|
|
|
|
//--- глобальные события (symbol='') — принимаем только запросы и локального реестра по request_id,иначе это чужие запросы
|
|
if(trans.symbol=="")
|
|
{
|
|
PendingRequest _pr;
|
|
if(result.request_id==0||!ExtLifecycle.FindRequest(result.request_id,_pr))
|
|
return;
|
|
}
|
|
|
|
//--- формирование события
|
|
TradeEvent e;
|
|
ZeroMemory(e);
|
|
e.request_id=result.request_id;
|
|
e.order_ticket=trans.order;
|
|
e.deal_ticket=trans.deal;
|
|
e.position_id=trans.position;
|
|
e.type=trans.type;
|
|
e.local_time_us=GetMicrosecondCount();
|
|
//--- берем время сервера на момент обработки
|
|
e.server_time=TimeTradeServer();
|
|
e.price=trans.price;
|
|
e.volume=trans.volume;
|
|
e.retcode=result.retcode;
|
|
|
|
//--- для сделок: направление и причина
|
|
if(trans.type==TRADE_TRANSACTION_DEAL_ADD&&trans.deal!=0)
|
|
{
|
|
HistorySelect(0,TimeCurrent());
|
|
if(HistoryDealSelect(trans.deal))
|
|
{
|
|
e.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal,DEAL_ENTRY);
|
|
e.reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(trans.deal,DEAL_REASON);
|
|
}
|
|
}
|
|
|
|
//--- добавление в буфер
|
|
ExtLifecycle.Add(e);
|
|
ExtHasEvents=true;
|
|
ExtLastEventUs=e.local_time_us;
|
|
ExtWatchdogFired=false;
|
|
|
|
PrintFormat("OnTradeTransaction: %s | order=%I64u deal=%I64u pos=%I64u price=%.5f vol=%.2f retcode=%u dt=%I64u мкс",
|
|
EnumToString(trans.type),
|
|
trans.order,
|
|
trans.deal,
|
|
trans.position,
|
|
trans.price,
|
|
trans.volume,
|
|
result.retcode,
|
|
e.local_time_us-ExtStartUs);
|
|
|
|
//--- для сделок: вывод в лог
|
|
if(e.deal_ticket!=0)
|
|
PrintFormat(" -> сделка: entry=%s reason=%s",
|
|
EnumToString(e.entry),EnumToString(e.reason));
|
|
|
|
//--- привязка события к локальному реестру по request_id
|
|
if(result.request_id!=0)
|
|
{
|
|
PendingRequest pr;
|
|
if(ExtLifecycle.FindRequest(result.request_id,pr))
|
|
{
|
|
PrintFormat(" -> локальный запрос #%u | %s vol=%.2f '%s' | с момента отправки %I64u мкс",
|
|
pr.local_seq,pr.symbol,pr.volume,pr.comment,
|
|
e.local_time_us-pr.send_time_us);
|
|
|
|
//--- при отказе сервера (REQUEST с ошибкой) — планируем повтор
|
|
if(trans.type==TRADE_TRANSACTION_REQUEST)
|
|
{
|
|
uint rc=result.retcode;
|
|
if(rc!=0&&rc!=TRADE_RETCODE_DONE&&rc!=TRADE_RETCODE_PLACED&&rc!=TRADE_RETCODE_DONE_PARTIAL)
|
|
{
|
|
//--- бессмысленные повторы не планируем: рынок закрыт или торговля отключена
|
|
if(rc==TRADE_RETCODE_MARKET_CLOSED||rc==TRADE_RETCODE_TRADE_DISABLED)
|
|
PrintFormat(" -> повтор пропущен: %s (бессмысленно до изменения условий)",
|
|
RetcodeName(rc));
|
|
else
|
|
ScheduleRetry(pr.order_type);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnTimer — повторы запросов+reconciliation по таймауту |
|
|
//+------------------------------------------------------------------+
|
|
void OnTimer()
|
|
{
|
|
ulong now_us=GetMicrosecondCount();
|
|
|
|
//--- повтор запроса по расписанию (InpRetries)
|
|
if(ExtRetryPending&&now_us>=ExtRetryAtUs)
|
|
{
|
|
ExtRetryPending=false;
|
|
PrintFormat("OnTimer: повтор запроса %s",EnumToString(ExtRetryType));
|
|
SendTradeRequest(ExtRetryType,InpVolume);
|
|
}
|
|
|
|
//--- запуск reconciliation по таймауту только при появлении новых событий
|
|
if(!ExtHasEvents)
|
|
return;
|
|
|
|
static ulong last_reconcile_us=0;
|
|
static int last_reconcile_cnt=-1;
|
|
|
|
if(now_us-last_reconcile_us>=(ulong)InpReconcileTimeoutMs*1000)
|
|
{
|
|
int cnt=ExtLifecycle.EventCount();
|
|
if(cnt!=last_reconcile_cnt)
|
|
{
|
|
ExtLifecycle.Reconcile();
|
|
last_reconcile_cnt=cnt;
|
|
}
|
|
last_reconcile_us=now_us;
|
|
}
|
|
//--- watchdog: если события не приходили InpReconcileTimeoutMs,а цепочка не завершена — принудительная сверка (обнаружение потери последнего события)
|
|
if(ExtHasEvents&&ExtLastEventUs!=0&&
|
|
now_us-ExtLastEventUs>=(ulong)InpReconcileTimeoutMs*1000&&
|
|
!ExtLifecycle.IsComplete())
|
|
{
|
|
if(!ExtWatchdogFired)
|
|
{
|
|
ExtWatchdogFired=true;
|
|
PrintFormat("OnTimer: watchdog | событий не было %d мс, цепочка не завершена — принудительная сверка",
|
|
InpReconcileTimeoutMs);
|
|
ExtLifecycle.Reconcile();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnChartEvent — обработка событий графика |
|
|
//+------------------------------------------------------------------+
|
|
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
|
|
{
|
|
//--- при изменении размеров окна обновляется панель,если изменился масштаб
|
|
if(id==CHARTEVENT_CHART_CHANGE)
|
|
{
|
|
double s=GetUIScale();
|
|
if(MathAbs(s-ExtUiScale)>0.02)
|
|
{
|
|
ExtUiScale=s;
|
|
CreateButtons();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if(id!=CHARTEVENT_OBJECT_CLICK)
|
|
return;
|
|
|
|
int total=ArraySize(BUTTON_NAMES);
|
|
for(int i=0;i<total;i++)
|
|
{
|
|
if(sparam!="LC_BTN_"+(string)i)
|
|
continue;
|
|
|
|
PrintFormat("OnChartEvent: нажата кнопка '%s'",BUTTON_NAMES[i]);
|
|
|
|
//--- сброс нажатого состояния кнопки
|
|
ObjectSetInteger(0,sparam,OBJPROP_STATE,false);
|
|
ChartRedraw(0);
|
|
|
|
switch(i)
|
|
{
|
|
case 0:
|
|
OnButtonMarketBuy();
|
|
break;
|
|
case 1:
|
|
OnButtonMarketSell();
|
|
break;
|
|
case 2:
|
|
OnButtonPendingBuyLimit();
|
|
break;
|
|
case 3:
|
|
OnButtonCancel();
|
|
break;
|
|
case 4:
|
|
OnButtonModify();
|
|
break;
|
|
case 5:
|
|
OnButtonExportCsv();
|
|
break;
|
|
case 6:
|
|
OnButtonDrawTimeline();
|
|
break;
|
|
case 7:
|
|
OnButtonClose();
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Вывод фактической ширины каждой кнопки в лог |
|
|
//+------------------------------------------------------------------+
|
|
void LogButtonWidths()
|
|
{
|
|
int total=ArraySize(BUTTON_NAMES);
|
|
Print("Фактическая ширина кнопок (px):");
|
|
for(int i=0;i<total;i++)
|
|
{
|
|
string name="LC_BTN_"+(string)i;
|
|
long w=ObjectGetInteger(0,name,OBJPROP_XSIZE);
|
|
PrintFormat(" [%d] %-20s width=%d",i,BUTTON_NAMES[i],(int)w);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Размеры и шрифт масштабируются по ширине окна |
|
|
//+------------------------------------------------------------------+
|
|
void CreateButtons()
|
|
{
|
|
double scale=GetUIScale();
|
|
double fscale=GetFontScale();
|
|
|
|
int btn_h=(int)MathRound(UI_BTN_BASE_HEIGHT*scale);
|
|
int btn_y=(int)MathRound(UI_BTN_BASE_Y*scale);
|
|
int btn_fs=(int)MathRound(InpBtnFontSize*scale);
|
|
int gap=(int)MathRound(UI_BTN_GAP*scale);
|
|
int padding=(int)MathRound(UI_BTN_PADDING*scale);
|
|
|
|
if(btn_fs<7)
|
|
btn_fs=7;
|
|
if(btn_h<24)
|
|
btn_h=24;
|
|
if(gap<4)
|
|
gap=4;
|
|
if(padding<8)
|
|
padding=8;
|
|
|
|
int x_pos=(int)MathRound(10*scale);
|
|
int total=ArraySize(BUTTON_NAMES);
|
|
|
|
for(int i=0;i<total;i++)
|
|
{
|
|
string name="LC_BTN_"+(string)i;
|
|
|
|
//--- Создать объект,если его еще нет
|
|
if(ObjectFind(0,name)<0)
|
|
{
|
|
if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0))
|
|
continue;
|
|
}
|
|
|
|
int text_w=TextWidthPx(BUTTON_NAMES[i],btn_fs);
|
|
int btn_w=(int)MathRound((text_w+2*padding)*fscale);
|
|
int min_w=(int)MathRound(UI_BTN_BASE_WIDTH*scale*fscale);
|
|
if(btn_w<min_w)
|
|
btn_w=min_w;
|
|
|
|
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x_pos);
|
|
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,btn_y);
|
|
ObjectSetInteger(0,name,OBJPROP_XSIZE,btn_w);
|
|
ObjectSetInteger(0,name,OBJPROP_YSIZE,btn_h);
|
|
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
|
|
ObjectSetString(0,name,OBJPROP_TEXT,BUTTON_NAMES[i]);
|
|
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,btn_fs);
|
|
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
|
ObjectSetInteger(0,name,OBJPROP_STATE,false);
|
|
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
|
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
|
|
|
//--- Следующая кнопка сдвигается с учетом фактической ширины
|
|
x_pos+=btn_w+gap;
|
|
}
|
|
|
|
//--- Принудительная перерисовка,чтобы панель появилась сразу
|
|
ChartRedraw(0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Имя файла экспорта: lifecycle_export_YYYYMMDD_HHMMSS.csv |
|
|
//+------------------------------------------------------------------+
|
|
string MakeExportFileName()
|
|
{
|
|
MqlDateTime dt;
|
|
TimeToStruct(TimeCurrent(),dt);
|
|
|
|
return(StringFormat("lifecycle_export_%04d%02d%02d_%02d%02d%02d.csv",
|
|
dt.year,dt.mon,dt.day,dt.hour,dt.min,dt.sec));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Обработчики кнопок |
|
|
//+------------------------------------------------------------------+
|
|
void OnButtonMarketBuy()
|
|
{
|
|
Print("Market Buy: BuildRequest -> PreCheck -> OrderSend");
|
|
SendTradeRequest(ORDER_TYPE_BUY,InpVolume);
|
|
}
|
|
|
|
void OnButtonMarketSell()
|
|
{
|
|
Print("Market Sell: BuildRequest -> PreCheck -> OrderSend");
|
|
SendTradeRequest(ORDER_TYPE_SELL,InpVolume);
|
|
}
|
|
|
|
void OnButtonPendingBuyLimit()
|
|
{
|
|
Print("Pending Buy Limit: BuildRequest -> PreCheck -> OrderSend");
|
|
SendTradeRequest(ORDER_TYPE_BUY_LIMIT,InpVolume);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Cancel — отмена отложенного ордера |
|
|
//+------------------------------------------------------------------+
|
|
void OnButtonCancel()
|
|
{
|
|
ulong ticket=FindFirstPendingOrder();
|
|
if(ticket==0||!OrderSelect(ticket))
|
|
{
|
|
Print("Cancel: нет активных отложенных ордеров");
|
|
return;
|
|
}
|
|
|
|
ENUM_ORDER_TYPE otype=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
|
double vol=OrderGetDouble(ORDER_VOLUME_CURRENT);
|
|
|
|
MqlTradeRequest request;
|
|
ZeroMemory(request);
|
|
request.action=TRADE_ACTION_REMOVE;
|
|
request.order=ticket;
|
|
request.symbol=ExtSymbol;
|
|
request.magic=InpMagic;
|
|
request.comment=StringFormat("LC_%u",ExtLocalSeq+1);
|
|
|
|
PrintFormat("Cancel: отмена ордера %I64u (%s, vol=%.2f)",ticket,EnumToString(otype),vol);
|
|
SendManageRequest(request,vol,otype);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Modify — модификация отложенного ордера |
|
|
//+------------------------------------------------------------------+
|
|
void OnButtonModify()
|
|
{
|
|
ulong ticket=FindFirstPendingOrder();
|
|
if(ticket==0||!OrderSelect(ticket))
|
|
{
|
|
Print("Modify: нет активных отложенных ордеров");
|
|
return;
|
|
}
|
|
|
|
ENUM_ORDER_TYPE otype=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
|
double vol=OrderGetDouble(ORDER_VOLUME_CURRENT);
|
|
double old_price=OrderGetDouble(ORDER_PRICE_OPEN);
|
|
double point=SymbolInfoDouble(ExtSymbol,SYMBOL_POINT);
|
|
if(point<=0)
|
|
point=1e-6;
|
|
|
|
//--- сдвиг в сторону исполнения: buy-лимиты ниже,sell-лимиты выше
|
|
bool buy_like=(otype==ORDER_TYPE_BUY_LIMIT||otype==ORDER_TYPE_BUY_STOP);
|
|
double new_price=old_price+(buy_like?-PENDING_DISTANCE_POINTS:PENDING_DISTANCE_POINTS)*point;
|
|
new_price=NormalizePrice(ExtSymbol,new_price);
|
|
|
|
MqlTradeRequest request;
|
|
ZeroMemory(request);
|
|
request.action=TRADE_ACTION_MODIFY;
|
|
request.order=ticket;
|
|
request.symbol=ExtSymbol;
|
|
request.price=new_price;
|
|
request.magic=InpMagic;
|
|
request.comment=StringFormat("LC_%u",ExtLocalSeq+1);
|
|
|
|
PrintFormat("Modify: ордер %I64u (%s) цена %.5f -> %.5f",
|
|
ticket,EnumToString(otype),old_price,new_price);
|
|
SendManageRequest(request,vol,otype);
|
|
}
|
|
|
|
void OnButtonExportCsv()
|
|
{
|
|
string file_name=MakeExportFileName();
|
|
ExtLifecycle.ExportCsv(file_name,InpCsvChainEntry);
|
|
}
|
|
|
|
void OnButtonDrawTimeline()
|
|
{
|
|
//--- режим OBJ_LABEL (метки)
|
|
ExtLifecycle.DrawTimeline(ChartID());
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnButtonClose - закрытие одной позиции за нажатие |
|
|
//+------------------------------------------------------------------+
|
|
void OnButtonClose()
|
|
{
|
|
int total=PositionsTotal();
|
|
if(total==0)
|
|
{
|
|
Print("OnButtonClose: no open positions");
|
|
return;
|
|
}
|
|
|
|
//--- close position index 0; next press closes the next one
|
|
ulong ticket=PositionGetTicket(0);
|
|
if(ticket==0)
|
|
{
|
|
Print("OnButtonClose: failed to get position");
|
|
return;
|
|
}
|
|
|
|
string symbol=PositionGetString(POSITION_SYMBOL);
|
|
double volume=PositionGetDouble(POSITION_VOLUME);
|
|
long type=PositionGetInteger(POSITION_TYPE);
|
|
double price=(type==POSITION_TYPE_BUY?SymbolInfoDouble(symbol,SYMBOL_BID):SymbolInfoDouble(symbol,SYMBOL_ASK));
|
|
|
|
MqlTradeRequest req={};
|
|
MqlTradeResult res={};
|
|
req.action=TRADE_ACTION_DEAL;
|
|
req.symbol=symbol;
|
|
req.volume=volume;
|
|
req.type=(type==POSITION_TYPE_BUY?ORDER_TYPE_SELL:ORDER_TYPE_BUY);
|
|
req.price=price;
|
|
req.deviation=30;
|
|
req.magic=InpMagic;
|
|
req.position=ticket;
|
|
req.comment="LC_CLOSE";
|
|
|
|
bool ok=(InpUseAsync?OrderSendAsync(req,res):OrderSend(req,res));
|
|
PrintFormat("OnButtonClose: %s pos=%I64u vol=%.2f | ok=%s retcode=%u '%s'",
|
|
symbol,ticket,volume,(ok?"true":"false"),res.retcode,res.comment);
|
|
|
|
if(ok)
|
|
{
|
|
ExtLocalSeq++;
|
|
ExtLifecycle.RegisterRequest(ExtLocalSeq,res.request_id,InpMagic,symbol,
|
|
volume,"LC_CLOSE",GetMicrosecondCount(),req.type);
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+ |