mql5-execution-microstructu.../Include/Dependencies/Article01/Experts/OrderLifecycle/TradeLifecycle.mqh

977 lines
No EOL
37 KiB
MQL5

//+------------------------------------------------------------------+
//| TradeLifecycle.mqh |
//| Copyright 2000-2026, MetaQuotes Ltd. |
//| www.mql5.com |
//+------------------------------------------------------------------+
#ifndef TRADE_LIFECYCLE_MQH
#define TRADE_LIFECYCLE_MQH
//+------------------------------------------------------------------+
//| TradeEvent — единая структура события жизненного цикла заявки |
//+------------------------------------------------------------------+
struct TradeEvent
{
uint request_id; // request_id (мост между локальным вызовом и сервером)
ulong order_ticket; // номер ордера (0 — не определено)
ulong deal_ticket; // номер сделки (0 — не определено)
ulong position_id; // идентификатор позиции (0 — не определено)
ENUM_TRADE_TRANSACTION_TYPE type; // тип транзакции
ulong local_time_us; // GetMicrosecondCount() в момент получения
datetime server_time; // время сервера на момент обработки (TimeTradeServer())
double price; // цена события
double volume; // объем события
uint retcode; // код результата (для REQUEST)
ENUM_DEAL_ENTRY entry; // направление сделки (для DEAL_ADD)
ENUM_DEAL_REASON reason; // причина сделки (для DEAL_ADD)
bool is_send; // true — событие «момент OrderSend» (SEND)
};
//+------------------------------------------------------------------+
//| PendingRequest — локальный реестр отправленных запросов |
//+------------------------------------------------------------------+
struct PendingRequest
{
uint local_seq; // локальный порядковый номер
uint request_id; // серверный request_id
ulong magic; // Magic Number
string symbol; // символ
double volume; // объем
string comment; // комментарий
ulong send_time_us; // GetMicrosecondCount() в момент отправки
ENUM_ORDER_TYPE order_type; // тип ордера (для повтора запроса)
};
//+------------------------------------------------------------------+
//| RetcodeName — текстовое имя кода ответа сервера |
//+------------------------------------------------------------------+
string RetcodeName(const uint retcode)
{
switch(retcode)
{
case TRADE_RETCODE_DONE:
return("DONE");
case TRADE_RETCODE_PLACED:
return("PLACED");
case TRADE_RETCODE_DONE_PARTIAL:
return("DONE_PARTIAL");
case TRADE_RETCODE_REQUOTE:
return("REQUOTE");
case TRADE_RETCODE_REJECT:
return("REJECT");
case TRADE_RETCODE_CANCEL:
return("CANCEL");
case TRADE_RETCODE_ERROR:
return("ERROR");
case TRADE_RETCODE_TIMEOUT:
return("TIMEOUT");
case TRADE_RETCODE_INVALID:
return("INVALID");
case TRADE_RETCODE_INVALID_VOLUME:
return("INVALID_VOLUME");
case TRADE_RETCODE_INVALID_PRICE:
return("INVALID_PRICE");
case TRADE_RETCODE_INVALID_STOPS:
return("INVALID_STOPS");
case TRADE_RETCODE_TRADE_DISABLED:
return("TRADE_DISABLED");
case TRADE_RETCODE_MARKET_CLOSED:
return("MARKET_CLOSED");
case TRADE_RETCODE_NO_MONEY:
return("NO_MONEY");
case TRADE_RETCODE_PRICE_CHANGED:
return("PRICE_CHANGED");
case TRADE_RETCODE_PRICE_OFF:
return("PRICE_OFF");
}
return(StringFormat("code_%u",retcode));
}
//+------------------------------------------------------------------+
//| ShortTypeName — короткое имя типа транзакции для timeline |
//+------------------------------------------------------------------+
string ShortTypeName(const ENUM_TRADE_TRANSACTION_TYPE type)
{
string s=EnumToString(type);
StringReplace(s,"TRADE_TRANSACTION_","");
return(s);
}
//+------------------------------------------------------------------+
//| TimelineColor — цвет события для timeline |
//+------------------------------------------------------------------+
color TimelineColor(const ENUM_TRADE_TRANSACTION_TYPE type)
{
switch(type)
{
case TRADE_TRANSACTION_REQUEST:
return(clrDodgerBlue);
case TRADE_TRANSACTION_ORDER_ADD:
return(clrLime);
case TRADE_TRANSACTION_ORDER_UPDATE:
return(clrAqua);
case TRADE_TRANSACTION_ORDER_DELETE:
return(clrTomato);
case TRADE_TRANSACTION_DEAL_ADD:
return(clrOrange);
case TRADE_TRANSACTION_HISTORY_ADD:
return(clrGray);
case TRADE_TRANSACTION_POSITION:
return(clrYellow);
}
return(clrWhite);
}
//+------------------------------------------------------------------+
//| TimelineColorDark — темные цвета на светлом фоне |
//+------------------------------------------------------------------+
color TimelineColorDark(const ENUM_TRADE_TRANSACTION_TYPE type)
{
switch(type)
{
case TRADE_TRANSACTION_REQUEST:
return(clrBlue);
case TRADE_TRANSACTION_ORDER_ADD:
return(clrDarkGreen);
case TRADE_TRANSACTION_ORDER_UPDATE:
return(clrTeal);
case TRADE_TRANSACTION_ORDER_DELETE:
return(clrDarkRed);
case TRADE_TRANSACTION_DEAL_ADD:
return(clrDarkOrange);
case TRADE_TRANSACTION_HISTORY_ADD:
return(clrDimGray);
case TRADE_TRANSACTION_POSITION:
return(clrDarkGoldenrod);
}
return(clrBlack);
}
//+------------------------------------------------------------------+
//| IsNettingAccount — модель учета позиций счета (неттинг?) |
//+------------------------------------------------------------------+
bool IsNettingAccount()
{
return(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_NETTING);
}
//+------------------------------------------------------------------+
//| IsLastDealOurs — последняя сделка по символу принадлежит magic? |
//+------------------------------------------------------------------+
bool IsLastDealOurs(const string symbol,const ulong magic)
{
for(int i=HistoryDealsTotal()-1;i>=0;i--)
{
ulong t=HistoryDealGetTicket(i);
if(t==0)
continue;
if(HistoryDealGetString(t,DEAL_SYMBOL)!=symbol)
continue;
return(HistoryDealGetInteger(t,DEAL_MAGIC)==(long)magic);
}
return(false);
}
//+------------------------------------------------------------------+
//| DealBelongsToChain — сделка относится к ордерам цепочки? |
//+------------------------------------------------------------------+
bool DealBelongsToChain(const ulong deal,const ulong &tickets[],const int count)
{
ulong order=HistoryDealGetInteger(deal,DEAL_ORDER);
for(int k=0;k<count;k++)
{
if(tickets[k]==order)
return(true);
}
return(false);
}
//+------------------------------------------------------------------+
//| IsPendingType — ордер является отложенным (не рыночным)? |
//+------------------------------------------------------------------+
bool IsPendingType(const long order_type)
{
return(order_type==ORDER_TYPE_BUY_LIMIT||order_type==ORDER_TYPE_SELL_LIMIT||
order_type==ORDER_TYPE_BUY_STOP||order_type==ORDER_TYPE_SELL_STOP||
order_type==ORDER_TYPE_BUY_STOP_LIMIT||order_type==ORDER_TYPE_SELL_STOP_LIMIT);
}
//+------------------------------------------------------------------+
//| CTradeLifecycle — накопление,сверка,экспорт,визуализация |
//+------------------------------------------------------------------+
class CTradeLifecycle
{
private:
uint m_request_id;
TradeEvent m_events[];
PendingRequest m_pending[]; // реестр отправленных запросов
bool m_request_received;
bool m_order_seen;
bool m_deal_seen;
bool m_position_updated;
string m_symbol; // контекст для сверки
ulong m_magic; // Magic Number для фильтрации
int m_chain_start; // индекс последнего SEND — начало цепочки запроса
private:
bool EventInLastChain(const TradeEvent &e) const;
public:
//--- конструктор / деструктор
CTradeLifecycle();
~CTradeLifecycle();
//--- контекст для reconciliation
void SetContext(const string symbol,const ulong magic);
//--- накопление событий
void Add(const TradeEvent &event);
//--- реестр отправленных запросов
void RegisterRequest(const uint local_seq,const uint request_id,
const ulong magic,const string symbol,
const double volume,const string comment,
const ulong send_time_us,
const ENUM_ORDER_TYPE order_type);
bool FindRequest(const uint request_id,PendingRequest &out) const;
int PendingTotal() const;
//--- количество событий в буфере
int EventCount() const;
//--- итоговая сверка (reconciliation)
void Reconcile();
//--- критерий завершенности lifecycle
bool IsComplete() const;
//--- экспорт журнала в CSV
void ExportCsv(const string file_name,const bool chain_entry=true) const;
ulong GetEventMagic(const TradeEvent &e) const;
//--- отрисовка timeline на графике
void DrawTimeline(const long chart_id) const;
};
//+------------------------------------------------------------------+
//| Конструктор / деструктор |
//+------------------------------------------------------------------+
CTradeLifecycle::CTradeLifecycle()
{
m_request_id=0;
m_request_received=false;
m_order_seen=false;
m_deal_seen=false;
m_position_updated=false;
m_symbol="";
m_magic=0;
m_chain_start=0;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CTradeLifecycle::~CTradeLifecycle()
{
}
//+------------------------------------------------------------------+
//| SetContext — задание символа и magic для сверки |
//+------------------------------------------------------------------+
void CTradeLifecycle::SetContext(const string symbol,const ulong magic)
{
m_symbol=symbol;
m_magic=magic;
}
//+------------------------------------------------------------------+
//| Add — добавление события в буфер и установка флагов |
//+------------------------------------------------------------------+
void CTradeLifecycle::Add(const TradeEvent &event)
{
int size=ArraySize(m_events);
if(ArrayResize(m_events,size+1)!=size+1)
{
PrintFormat("CTradeLifecycle::Add(): не удалось расширить буфер (size=%d,error=%d)",
size,GetLastError());
return;
}
m_events[size]=event;
//--- SEND (момент OrderSend) фиксирует начало новой цепочки запроса
if(event.is_send)
{
m_chain_start=size;
return;
}
//--- установка флагов по типу транзакции (только события последней цепочки)
if(event.request_id!=0&&event.request_id!=m_request_id)
return;
switch(event.type)
{
case TRADE_TRANSACTION_REQUEST:
m_request_received=true;
break;
case TRADE_TRANSACTION_ORDER_ADD:
case TRADE_TRANSACTION_ORDER_UPDATE:
case TRADE_TRANSACTION_ORDER_DELETE:
m_order_seen=true;
break;
case TRADE_TRANSACTION_DEAL_ADD:
m_deal_seen=true;
break;
case TRADE_TRANSACTION_POSITION:
m_position_updated=true;
break;
}
}
//+------------------------------------------------------------------+
//| EventInLastChain — принадлежит ли событие последней цепочке? |
//+------------------------------------------------------------------+
bool CTradeLifecycle::EventInLastChain(const TradeEvent &e) const
{
if(e.is_send)
return(false);
//--- события локальных запросов должны совпадать с последним request_id
if(e.request_id!=0&&e.request_id!=m_request_id)
return(false);
return(true);
}
//+------------------------------------------------------------------+
//| RegisterRequest — регистрация отправленного запроса в реестре |
//+------------------------------------------------------------------+
void CTradeLifecycle::RegisterRequest(const uint local_seq,const uint request_id,
const ulong magic,const string symbol,
const double volume,const string comment,
const ulong send_time_us,
const ENUM_ORDER_TYPE order_type)
{
int n=ArraySize(m_pending);
if(ArrayResize(m_pending,n+1)!=n+1)
{
PrintFormat("CTradeLifecycle::RegisterRequest(): не удалось расширить реестр (size=%d,error=%d)",
n,GetLastError());
return;
}
PendingRequest p;
p.local_seq=local_seq;
p.request_id=request_id;
p.magic=magic;
p.symbol=symbol;
p.volume=volume;
p.comment=comment;
p.send_time_us=send_time_us;
p.order_type=order_type;
m_pending[n]=p;
m_request_id=request_id; // последний отправленный запрос — контекст сверки цепочки
//--- также фиксируем событие «SEND» (момент отправки) в буфере, от которого отсчитывается время остальных событий
TradeEvent s;
ZeroMemory(s);
s.is_send=true;
s.type=(ENUM_TRADE_TRANSACTION_TYPE)-1; // маркер «не реальная транзакция» (реальные типы 0..6)
s.request_id=request_id;
s.local_time_us=send_time_us;
s.server_time=TimeTradeServer();
s.volume=volume;
Add(s);
PrintFormat("CTradeLifecycle::RegisterRequest(): #%u request_id=%u %s vol=%.2f '%s' (%s)",
local_seq,request_id,symbol,volume,comment,EnumToString(order_type));
}
//+------------------------------------------------------------------+
//| FindRequest — поиск запроса в реестре по request_id |
//+------------------------------------------------------------------+
bool CTradeLifecycle::FindRequest(const uint request_id,PendingRequest &out) const
{
for(int i=ArraySize(m_pending)-1;i>=0;i--)
{
if(m_pending[i].request_id==request_id)
{
out=m_pending[i];
return(true);
}
}
return(false);
}
//+------------------------------------------------------------------+
//| PendingTotal — количество запросов в реестре |
//+------------------------------------------------------------------+
int CTradeLifecycle::PendingTotal() const
{
return(ArraySize(m_pending));
}
//+------------------------------------------------------------------+
//| EventCount — количество событий в буфере |
//+------------------------------------------------------------------+
int CTradeLifecycle::EventCount() const
{
return(ArraySize(m_events));
}
//+------------------------------------------------------------------+
//| Reconcile — итоговая сверка состояния |
//+------------------------------------------------------------------+
void CTradeLifecycle::Reconcile()
{
PrintFormat("CTradeLifecycle::Reconcile(): цепочка последнего запроса: индексы %d..%d",
m_chain_start,ArraySize(m_events)-1);
PrintFormat("CTradeLifecycle::Reconcile(): событий в буфере: %d | request=%s order=%s deal=%s position=%s",
ArraySize(m_events),
(m_request_received?"yes":"no"),
(m_order_seen?"yes":"no"),
(m_deal_seen?"yes":"no"),
(m_position_updated?"yes":"no"));
int active_orders=0;
for(int i=0;i<OrdersTotal();i++)
{
ulong ticket=OrderGetTicket(i);
if(ticket==0)
continue;
if(OrderGetString(ORDER_SYMBOL)==m_symbol&&OrderGetInteger(ORDER_MAGIC)==(long)m_magic)
active_orders++;
}
HistorySelect(0,TimeCurrent());
//--- тикеты ордеров последней цепочки (для привязки сделок и объемов)
ulong chain_orders[];
int chain_orders_count=0;
for(int i=m_chain_start;i<ArraySize(m_events);i++)
{
TradeEvent ev=m_events[i];
if(!EventInLastChain(ev))
continue;
if(ev.order_ticket==0)
continue;
bool known=false;
for(int k=0;k<chain_orders_count;k++)
{
if(chain_orders[k]==ev.order_ticket)
{
known=true;
break;
}
}
if(!known)
{
if(ArrayResize(chain_orders,chain_orders_count+1)!=chain_orders_count+1)
break;
chain_orders[chain_orders_count]=ev.order_ticket;
chain_orders_count++;
}
}
int hist_orders=0;
int hist_deals=0;
int sym_orders=0;
int sym_deals=0;
double executed_volume=0.0; // всех сделок по magic (контекст)
double chain_vol_in=0.0; // объем IN по ордерам цепочки
double chain_vol_out=0.0; // объем OUT по ордерам цепочки
for(int i=0;i<HistoryOrdersTotal();i++)
{
ulong t=HistoryOrderGetTicket(i);
if(t==0)
continue;
if(HistoryOrderGetString(t,ORDER_SYMBOL)!=m_symbol)
continue;
sym_orders++;
if(HistoryOrderGetInteger(t,ORDER_MAGIC)==(long)m_magic)
hist_orders++;
}
for(int i=0;i<HistoryDealsTotal();i++)
{
ulong t=HistoryDealGetTicket(i);
if(t==0)
continue;
if(HistoryDealGetString(t,DEAL_SYMBOL)!=m_symbol)
continue;
sym_deals++;
if(HistoryDealGetInteger(t,DEAL_MAGIC)!=(long)m_magic)
continue;
hist_deals++;
if(chain_orders_count>0&&DealBelongsToChain(t,chain_orders,chain_orders_count))
{
if(HistoryDealGetInteger(t,DEAL_ENTRY)==DEAL_ENTRY_IN)
chain_vol_in+=HistoryDealGetDouble(t,DEAL_VOLUME);
else
if(HistoryDealGetInteger(t,DEAL_ENTRY)==DEAL_ENTRY_OUT)
chain_vol_out+=HistoryDealGetDouble(t,DEAL_VOLUME);
}
if(HistoryDealGetInteger(t,DEAL_ENTRY)==DEAL_ENTRY_IN)
executed_volume+=HistoryDealGetDouble(t,DEAL_VOLUME);
}
int positions=0;
//--- netting: последняя сделка по символу — наша? (один проход истории, а не полный скан для каждой позиции отдельно)
bool ours_last=(IsNettingAccount()?IsLastDealOurs(m_symbol,m_magic):false);
if(IsNettingAccount())
{
for(int i=0;i<PositionsTotal();i++)
{
ulong t=PositionGetTicket(i);
if(t==0)
continue;
if(PositionGetString(POSITION_SYMBOL)!=m_symbol)
continue;
if(ours_last)
positions++;
}
}
else
{
for(int i=0;i<PositionsTotal();i++)
{
ulong t=PositionGetTicket(i);
if(t==0)
continue;
if(PositionGetString(POSITION_SYMBOL)==m_symbol&&PositionGetInteger(POSITION_MAGIC)==(long)m_magic)
positions++;
}
}
uint last_retcode=0;
int chain_event_count=0;
for(int i=m_chain_start;i<ArraySize(m_events);i++)
{
if(!EventInLastChain(m_events[i]))
continue;
chain_event_count++;
if(m_events[i].type==TRADE_TRANSACTION_REQUEST)
last_retcode=m_events[i].retcode;
}
string state;
if(last_retcode==TRADE_RETCODE_DONE||last_retcode==TRADE_RETCODE_PLACED)
{
if(active_orders>0)
{
if(positions>0)
state=StringFormat("ордер размещен / ожидает исполнения (позиций: %d)",positions);
else
state="ордер размещен / ожидает исполнения";
}
else
if(positions>0&&m_deal_seen)
state="исполнено полностью (позиция открыта/изменена)";
else
if(m_deal_seen)
state="исполнено (позиции нет)";
else
if(m_order_seen)
{
if(chain_vol_in>0||chain_vol_out>0)
state="исполнено (сделка в истории; событие DEAL_ADD не получено)";
else
state="ордер обработан, сделок нет (отменен/истек)";
}
else
state="запрос принят, события еще не получены";
}
else
if(last_retcode!=0)
state="отказ сервера ("+RetcodeName(last_retcode)+")";
else
if(chain_event_count>0)
state="события без локального запроса (внешние/ручные)";
else
state="нет запросов";
PrintFormat("CTradeLifecycle::Reconcile(): модель=%s | активных ордеров=%d, история ордеров=%d (символ: %d), сделок=%d (символ: %d), позиций=%d | объем IN=%.2f OUT=%.2f (все по magic: %.2f) | состояние: %s",
(IsNettingAccount()?"netting":"hedging"),
active_orders,hist_orders,sym_orders,hist_deals,sym_deals,positions,
chain_vol_in,chain_vol_out,executed_volume,state);
PrintFormat("CTradeLifecycle::IsComplete(): %s",
(IsComplete()?"да — жизненный цикл завершен":"нет — ожидание событий"));
}
//+------------------------------------------------------------------+
//| IsComplete — критерий завершенности lifecycle |
//+------------------------------------------------------------------+
bool CTradeLifecycle::IsComplete() const
{
int total=ArraySize(m_events);
if(total==0)
return(false);
bool seen_order_add=false;
bool seen_order_update=false;
bool seen_order_delete=false;
bool seen_deal=false;
ulong order_ticket=0;
uint last_request_ret=0;
for(int i=m_chain_start;i<total;i++)
{
if(!EventInLastChain(m_events[i]))
continue;
switch(m_events[i].type)
{
case TRADE_TRANSACTION_REQUEST:
if(last_request_ret==0)
last_request_ret=m_events[i].retcode;
break;
case TRADE_TRANSACTION_ORDER_ADD:
seen_order_add=true;
if(m_events[i].order_ticket!=0)
order_ticket=m_events[i].order_ticket;
break;
case TRADE_TRANSACTION_ORDER_UPDATE:
seen_order_update=true;
if(m_events[i].order_ticket!=0)
order_ticket=m_events[i].order_ticket;
break;
case TRADE_TRANSACTION_ORDER_DELETE:
seen_order_delete=true;
if(m_events[i].order_ticket!=0)
order_ticket=m_events[i].order_ticket;
break;
case TRADE_TRANSACTION_DEAL_ADD:
seen_deal=true;
if(m_events[i].order_ticket!=0)
order_ticket=m_events[i].order_ticket;
break;
}
}
if(last_request_ret!=0&&
last_request_ret!=TRADE_RETCODE_DONE&&
last_request_ret!=TRADE_RETCODE_PLACED&&
last_request_ret!=TRADE_RETCODE_DONE_PARTIAL)
return(true);
if(seen_deal)
{
if(order_ticket!=0&&OrderSelect(order_ticket))
{
if(IsPendingType(OrderGetInteger(ORDER_TYPE)))
return(false);
}
return(true);
}
if(seen_order_add&&seen_order_delete)
return(true);
//--- модификация отложенного ордера сама по себе не завершает lifecycle: ордер остается активным и ждет исполнения или удаления
if(seen_order_add&&seen_order_update&&
!(order_ticket!=0&&OrderSelect(order_ticket)&&IsPendingType(OrderGetInteger(ORDER_TYPE))))
return(true);
return(false);
}
//+------------------------------------------------------------------+
//| ExportCsv — экспорт журнала в CSV |
//+------------------------------------------------------------------+
void CTradeLifecycle::ExportCsv(const string file_name,const bool chain_entry) const
{
int handle=FileOpen(file_name,FILE_WRITE|FILE_READ|FILE_CSV|FILE_ANSI,',');
if(handle==INVALID_HANDLE)
{
PrintFormat("CTradeLifecycle::ExportCsv(): не удалось открыть '%s' (error=%d)",
file_name,GetLastError());
return;
}
int total=ArraySize(m_events);
ulong chain_tickets[];
ENUM_DEAL_ENTRY chain_entries[];
ENUM_DEAL_REASON chain_reasons[];
int chain_count=0;
if(chain_entry)
{
for(int i=0;i<total;i++)
{
TradeEvent ev=m_events[i];
if(ev.type!=TRADE_TRANSACTION_DEAL_ADD||ev.order_ticket==0)
continue;
ENUM_DEAL_ENTRY en=ev.entry;
ENUM_DEAL_REASON rs=ev.reason;
HistorySelect(0,TimeCurrent());
if(HistoryDealSelect(ev.deal_ticket))
{
en=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ev.deal_ticket,DEAL_ENTRY);
rs=(ENUM_DEAL_REASON)HistoryDealGetInteger(ev.deal_ticket,DEAL_REASON);
}
int found=-1;
for(int k=0;k<chain_count;k++)
{
if(chain_tickets[k]==ev.order_ticket)
{
found=k;
break;
}
}
if(found>=0)
{
chain_entries[found]=en;
chain_reasons[found]=rs;
}
else
{
if(ArrayResize(chain_tickets,chain_count+1)!=chain_count+1||
ArrayResize(chain_entries,chain_count+1)!=chain_count+1||
ArrayResize(chain_reasons,chain_count+1)!=chain_count+1)
break;
chain_tickets[chain_count]=ev.order_ticket;
chain_entries[chain_count]=en;
chain_reasons[chain_count]=rs;
chain_count++;
}
}
}
FileWrite(handle,
"seq","request_id","order","deal","position_id",
"type","magic","time_us","time_from_send_us","server_time","price","volume","retcode",
"entry","reason");
//--- строки событий (SEND из буфера,затем все остальное)
ulong cur_ticket=0;
ulong cur_send=0; // момент SEND текущего запроса (time_us)
ENUM_DEAL_ENTRY cur_entry=(ENUM_DEAL_ENTRY)0;
ENUM_DEAL_REASON cur_reason=(ENUM_DEAL_REASON)0;
bool cur_valid=false;
for(int i=0;i<total;i++)
{
TradeEvent e=m_events[i];
string out_entry="";
string out_reason="";
string out_type=(e.is_send?"SEND":EnumToString(e.type));
if(e.is_send)
cur_send=e.local_time_us; // обновляем нулевую точку текущего запроса
if(chain_entry)
{
if(e.type==TRADE_TRANSACTION_DEAL_ADD&&e.deal_ticket!=0)
{
HistorySelect(0,TimeCurrent());
if(HistoryDealSelect(e.deal_ticket))
{
out_entry=EnumToString((ENUM_DEAL_ENTRY)HistoryDealGetInteger(e.deal_ticket,DEAL_ENTRY));
out_reason=EnumToString((ENUM_DEAL_REASON)HistoryDealGetInteger(e.deal_ticket,DEAL_REASON));
}
}
else
{
if(e.order_ticket!=0&&e.order_ticket!=cur_ticket)
{
cur_ticket=e.order_ticket;
cur_entry=(ENUM_DEAL_ENTRY)0;
cur_reason=(ENUM_DEAL_REASON)0;
cur_valid=false;
for(int k=0;k<chain_count;k++)
{
if(chain_tickets[k]==cur_ticket)
{
cur_entry=chain_entries[k];
cur_reason=chain_reasons[k];
cur_valid=true;
break;
}
}
}
if(cur_valid&&(e.order_ticket!=0||e.type==TRADE_TRANSACTION_REQUEST))
{
out_entry=EnumToString(cur_entry);
out_reason=EnumToString(cur_reason);
}
}
}
else
{
if(e.deal_ticket!=0)
{
HistorySelect(0,TimeCurrent());
if(HistoryDealSelect(e.deal_ticket))
{
out_entry=EnumToString((ENUM_DEAL_ENTRY)HistoryDealGetInteger(e.deal_ticket,DEAL_ENTRY));
out_reason=EnumToString((ENUM_DEAL_REASON)HistoryDealGetInteger(e.deal_ticket,DEAL_REASON));
}
}
}
FileWrite(handle,
i+1,
(long)e.request_id,
(long)e.order_ticket,
(long)e.deal_ticket,
(long)e.position_id,
out_type,
(long)GetEventMagic(e),
(long)e.local_time_us,
(long)((cur_send==0||e.local_time_us<cur_send)?0:e.local_time_us-cur_send),
TimeToString(e.server_time,TIME_DATE|TIME_SECONDS),
DoubleToString(e.price,6),
DoubleToString(e.volume,2),
(long)e.retcode,
out_entry,
out_reason);
}
FileClose(handle);
PrintFormat("CTradeLifecycle::ExportCsv(): экспортировано событий: %d -> '%s'%s",total,file_name,
(chain_entry?" (entry/reason на всю цепочку)":""));
}
//+------------------------------------------------------------------+
//| DrawTimeline — отрисовка временной линии на графике |
//| Каждое событие — метка OBJ_LABEL с типом и дельтой от SEND/ |
//| Предыдущего события (мкс). |
//+------------------------------------------------------------------+
void CTradeLifecycle::DrawTimeline(const long chart_id) const
{
ObjectsDeleteAll(chart_id,"LC_TL_");
int total=ArraySize(m_events);
if(total==0)
{
Print("DrawTimeline: нет событий для отрисовки");
return;
}
int x=10;
int y=140;
int step=20;
ulong first_send=0;
ulong cur_order_ticket=0;
bool last_was_send=false;
ulong prev_us=0;
for(int i=0;i<total;i++)
{
TradeEvent e=m_events[i];
string name=StringFormat("LC_TL_%d",i);
string label;
if(e.is_send)
{
first_send=e.local_time_us;
cur_order_ticket=0;
label="SEND 0 мкс";
last_was_send=true;
prev_us=e.local_time_us;
}
else
{
//--- новая цепочка ордера (не продолжение после SEND) — например,
//--- ручное закрытие из терминала: SEND для него не было,поэтому
//--- нулевой точкой считаем первое событие этой цепочки
if(e.order_ticket!=0&&e.order_ticket!=cur_order_ticket)
{
cur_order_ticket=e.order_ticket;
if(!last_was_send)
first_send=e.local_time_us;
}
last_was_send=false;
//--- дельта от момента отправки (SEND) и от предыдущего события
if(first_send!=0)
label=StringFormat("%s от SEND +%I64u мкс",ShortTypeName(e.type),e.local_time_us-first_send);
else
label=ShortTypeName(e.type);
if(prev_us!=0)
label+=StringFormat(" prev +%I64u мкс",e.local_time_us-prev_us);
prev_us=e.local_time_us;
}
if(e.retcode!=0)
label+=StringFormat(" ret=%s",RetcodeName(e.retcode));
if(e.order_ticket!=0)
label+=StringFormat(" ord=%I64u",e.order_ticket);
if(e.deal_ticket!=0)
label+=StringFormat(" deal=%I64u",e.deal_ticket);
color clr=(e.is_send?clrDarkBlue:TimelineColorDark(e.type));
if(!ObjectCreate(chart_id,name,OBJ_LABEL,0,0,0))
continue;
ObjectSetInteger(chart_id,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(chart_id,name,OBJPROP_YDISTANCE,y+i*step);
ObjectSetInteger(chart_id,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetString(chart_id,name,OBJPROP_TEXT,label);
ObjectSetString(chart_id,name,OBJPROP_FONT,"Arial");
ObjectSetInteger(chart_id,name,OBJPROP_FONTSIZE,10);
ObjectSetInteger(chart_id,name,OBJPROP_COLOR,clr);
ObjectSetInteger(chart_id,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(chart_id,name,OBJPROP_HIDDEN,true);
}
ChartRedraw(chart_id);
PrintFormat("DrawTimeline: отрисовано событий: %d",total);
}
//+------------------------------------------------------------------+
//| PrintEntityInfo — сводка по четырем сущностям для символа |
//+------------------------------------------------------------------+
void PrintEntityInfo(const string symbol)
{
int orders=0;
for(int i=0;i<OrdersTotal();i++)
{
if(OrderGetTicket(i)!=0&&OrderGetString(ORDER_SYMBOL)==symbol)
orders++;
}
int positions=0;
for(int i=0;i<PositionsTotal();i++)
{
if(PositionGetTicket(i)!=0&&PositionGetString(POSITION_SYMBOL)==symbol)
positions++;
}
if(!HistorySelect(0,TimeCurrent()))
{
PrintFormat("PrintEntityInfo: %s | HistorySelect: error=%d",symbol,GetLastError());
return;
}
int hist_orders=0;
int hist_deals=0;
for(int i=0;i<HistoryOrdersTotal();i++)
{
ulong t=HistoryOrderGetTicket(i);
if(t!=0&&HistoryOrderGetString(t,ORDER_SYMBOL)==symbol)
hist_orders++;
}
for(int i=0;i<HistoryDealsTotal();i++)
{
ulong t=HistoryDealGetTicket(i);
if(t!=0&&HistoryDealGetString(t,DEAL_SYMBOL)==symbol)
hist_deals++;
}
PrintFormat("PrintEntityInfo: %s | активные ордера=%d, история ордеров=%d, история сделок=%d, позиции=%d",
symbol,orders,hist_orders,hist_deals,positions);
}
//+------------------------------------------------------------------+
//| GetEventMagic — magic события |
//| Для сделки/ордера — из истории (DEAL_MAGIC / ORDER_MAGIC), |
//| Для SEND/REQUEST — из реестра по request_id |
//+------------------------------------------------------------------+
ulong CTradeLifecycle::GetEventMagic(const TradeEvent &e) const
{
//--- гарантия наличия истории для HistoryDealSelect/HistoryOrderSelect
HistorySelect(0,TimeCurrent());
if(e.deal_ticket!=0&&HistoryDealSelect(e.deal_ticket))
return(ulong)HistoryDealGetInteger(e.deal_ticket,DEAL_MAGIC);
if(e.order_ticket!=0&&HistoryOrderSelect(e.order_ticket))
return(ulong)HistoryOrderGetInteger(e.order_ticket,ORDER_MAGIC);
//--- SEND / REQUEST: ищем в реестре по request_id
for(int i=0;i<ArraySize(m_pending);i++)
{
if(m_pending[i].request_id==e.request_id)
return(m_pending[i].magic);
}
return(0);
}
#endif // TRADE_LIFECYCLE_MQH
//+------------------------------------------------------------------+