977 lines
No EOL
37 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+ |