//+------------------------------------------------------------------+ //| 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=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;i0&&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;i0) { 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=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 '%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