2026-09-10 16:43:28 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-16 15:37:55 +03:00 | | | //| TradeLifecycle.mqh |
|
| | | //| Copyright 2000-2026, MetaQuotes Ltd. |
|
| | | //| www.mql5.com |
|
2026-09-10 16:43:28 +03:00 | | | //+------------------------------------------------------------------+
|
| | | #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
|
| | | //+------------------------------------------------------------------+
|