2026-08-12 19:54:12 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| CHistoryTracker.mqh |
|
| | | //| Centaur Quant Architecture — Execution Module |
|
| | | //| Trade_Closed Feedback-Loop Emitter |
|
| | | //+------------------------------------------------------------------+
|
| | | //| PURPOSE |
|
| | | //| Detects positions opened by this EA (symbol + magic) that have |
|
| | | //| since closed, recovers the original AI score and initial SL from |
|
| | | //| the order comment ("CEN:<score>:<sl>"), and computes the final |
|
| | | //| net profit + R-multiple. The composition root turns each record |
|
| | | //| into an SDP Trade_Closed payload via CSDPEncoder. |
|
| | | //| State memory: every closed ticket is emitted exactly once. |
|
| | | //+------------------------------------------------------------------+
|
| | | #property strict
|
| | |
|
| | | #ifndef HISTORYTRACKER_MQH
|
| | | #define HISTORYTRACKER_MQH
|
| | |
|
| | | //--- structured closed-trade record consumed by the composition root ---
|
| | | struct SClosedTrade
|
| | | {
|
| | | bool valid; // true when the record is complete
|
| | | ulong ticket; // position/order ticket
|
| | | double profit; // net PnL (profit + swap + commission)
|
| | | double r_multiple; // net PnL / initial risk amount
|
| | | double initial_ai_score; // original AI score from the comment
|
| | | datetime close_time; // closing deal time (server)
|
| | | string symbol; // instrument
|
2026-08-16 11:12:59 +07:00 | | | //--- Gate B evidence (additive observability; Evidence Protocol v0.1 §4) ---
|
| | | double entry_price; // actual fill price (opening deal)
|
| | | double initial_sl; // initial SL recovered from "CEN:<score>:<sl>"
|
| | | long position_type; // POSITION_TYPE_BUY / POSITION_TYPE_SELL
|
| | | double exit_price; // volume-weighted mean exit price (closing deals)
|
| | | double exit_volume; // total closed volume (closing deals)
|
| | | ulong exit_deal; // last closing deal ticket
|
| | | long deal_reason; // DEAL_REASON of the last closing deal
|
| | | double swap; // total swap (closing side)
|
| | | double commission; // total commission (closing side)
|
2026-08-12 19:54:12 +07:00 | | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| |
|
| | | //+------------------------------------------------------------------+
|
| | | class CHistoryTracker
|
| | | {
|
| | | private:
|
| | | string m_symbol; // instrument filter
|
| | | long m_magic; // magic filter
|
| | | bool m_ready;
|
| | |
|
| | | ulong m_tracked[]; // open tickets from the previous scan
|
| | | SClosedTrade m_pending[]; // detected closed trades awaiting delivery
|
| | | int m_pending_head;
|
| | | enum { COMPACT_THRESHOLD = 32 };
|
| | |
|
| | | void SnapshotPositions(ulong &tickets[]);
|
| | | bool Contains(const ulong &list[], const ulong ticket);
|
| | | bool BuildClosedRecord(const ulong ticket, SClosedTrade &out);
|
| | | bool ParseCenComment(const string comment, double &score, double &initial_sl);
|
| | | void Enqueue(const SClosedTrade &rec);
|
| | | void CompactQueue();
|
| | |
|
| | | public:
|
| | | CHistoryTracker(const string symbol, const long magic);
|
| | | ~CHistoryTracker();
|
| | |
|
| | | //--- returns true while a newly closed trade is available ---
|
| | | bool Check(SClosedTrade &out_trade);
|
| | |
|
| | | //--- read access ---
|
| | | bool IsReady() const { return m_ready; }
|
| | | string Symbol() const { return m_symbol; }
|
| | | int PendingCount() const;
|
| | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| Constructor — bind symbol/magic filters. |
|
| | | //+------------------------------------------------------------------+
|
| | | CHistoryTracker::CHistoryTracker(const string symbol, const long magic)
|
| | | : m_symbol(symbol),
|
| | | m_magic(magic),
|
| | | m_ready(false),
|
| | | m_pending_head(0)
|
| | | {
|
| | | if(StringLen(m_symbol) == 0)
|
| | | {
|
| | | PrintFormat("[CHistoryTracker] ERROR: empty symbol.");
|
| | | return;
|
| | | }
|
| | | m_ready = (SymbolInfoDouble(m_symbol, SYMBOL_POINT) > 0.0);
|
| | | if(!m_ready)
|
| | | PrintFormat("[CHistoryTracker] ERROR: symbol '%s' not available.", m_symbol);
|
| | | else
|
| | | PrintFormat("[CHistoryTracker] INFO: ready on %s (magic %I64d).", m_symbol, m_magic);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| Destructor — nothing to release. |
|
| | | //+------------------------------------------------------------------+
|
| | | CHistoryTracker::~CHistoryTracker()
|
| | | {
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| Check — deliver one newly closed trade per call (true) until the |
|
| | | //| queue is drained (false). First call snapshots the open set; a |
|
| | | //| ticket tracked earlier and missing now has been closed. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CHistoryTracker::Check(SClosedTrade &out_trade)
|
| | | {
|
| | | if(!m_ready)
|
| | | return false;
|
| | | //--- serve closures detected in a previous scan first ---
|
| | | if(m_pending_head < ArraySize(m_pending))
|
| | | {
|
| | | out_trade = m_pending[m_pending_head++];
|
| | | if(m_pending_head >= COMPACT_THRESHOLD)
|
| | | CompactQueue();
|
| | | return true;
|
| | | }
|
| | | //--- snapshot the EA's currently open positions ---
|
| | | ulong current[];
|
| | | SnapshotPositions(current);
|
| | | //--- any previously tracked ticket now missing = closed ---
|
| | | for(int i = 0; i < ArraySize(m_tracked); i++)
|
| | | {
|
| | | const ulong ticket = m_tracked[i];
|
| | | if(Contains(current, ticket))
|
| | | continue;
|
| | | SClosedTrade rec;
|
| | | if(BuildClosedRecord(ticket, rec))
|
| | | Enqueue(rec);
|
| | | }
|
| | | //--- advance the tracked set to the live snapshot ---
|
| | | ArrayResize(m_tracked, ArraySize(current));
|
| | | for(int i = 0; i < ArraySize(current); i++)
|
| | | m_tracked[i] = current[i];
|
| | | if(m_pending_head < ArraySize(m_pending))
|
| | | {
|
| | | out_trade = m_pending[m_pending_head++];
|
| | | return true;
|
| | | }
|
| | | return false;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| SnapshotPositions — tickets of open positions matching filters. |
|
| | | //+------------------------------------------------------------------+
|
| | | void CHistoryTracker::SnapshotPositions(ulong &tickets[])
|
| | | {
|
| | | ArrayResize(tickets, 0);
|
| | | for(int i = PositionsTotal() - 1; i >= 0; i--)
|
| | | {
|
| | | const ulong t = PositionGetTicket(i);
|
| | | if(t == 0 || !PositionSelectByTicket(t))
|
| | | continue;
|
| | | if(PositionGetString(POSITION_SYMBOL) != m_symbol)
|
| | | continue;
|
| | | if((long)PositionGetInteger(POSITION_MAGIC) != m_magic)
|
| | | continue;
|
| | | const int n = ArraySize(tickets);
|
| | | ArrayResize(tickets, n + 1);
|
| | | tickets[n] = t;
|
| | | }
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| Contains — linear membership test over the ticket list. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CHistoryTracker::Contains(const ulong &list[], const ulong ticket)
|
| | | {
|
| | | for(int i = 0; i < ArraySize(list); i++)
|
| | | if(list[i] == ticket)
|
| | | return true;
|
| | | return false;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| BuildClosedRecord — reconstruct the closed trade from deal |
|
| | | //| history: opening deal (entry price, volume, order comment) and |
|
| | | //| closing deal (net PnL, close time). R-multiple = net PnL divided |
|
| | | //| by the initial risk amount (|entry - initial_sl| * tick value per |
|
| | | //| point * volume). |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CHistoryTracker::BuildClosedRecord(const ulong ticket, SClosedTrade &out)
|
| | | {
|
| | | ZeroMemory(out);
|
| | | if(!HistorySelectByPosition(ticket))
|
| | | return false;
|
| | | const double tick_size = SymbolInfoDouble(m_symbol, SYMBOL_TRADE_TICK_SIZE);
|
| | | const double tick_value = SymbolInfoDouble(m_symbol, SYMBOL_TRADE_TICK_VALUE);
|
| | | if(tick_size <= 0.0 || tick_value <= 0.0)
|
| | | return false;
|
| | | double profit = 0.0;
|
| | | double volume = 0.0;
|
| | | double entry_price = 0.0;
|
| | | datetime close_time = 0;
|
| | | string comment = "";
|
| | | bool have_close = false;
|
2026-08-16 11:12:59 +07:00 | | | //--- Gate B evidence accumulators (additive observability) ---
|
| | | long position_type = -1; // from the opening deal
|
| | | double exit_price = 0.0; // volume-weighted
|
| | | double exit_volume = 0.0;
|
| | | ulong exit_deal = 0;
|
| | | long deal_reason = 0;
|
| | | double swap_sum = 0.0;
|
| | | double commission_sum= 0.0;
|
2026-08-12 19:54:12 +07:00 | | | const int n = HistoryDealsTotal();
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | const ulong deal = HistoryDealGetTicket(i);
|
| | | if(deal == 0)
|
| | | continue;
|
| | | if((long)HistoryDealGetInteger(deal, DEAL_POSITION_ID) != (long)ticket)
|
| | | continue;
|
| | | const long entry_type = HistoryDealGetInteger(deal, DEAL_ENTRY);
|
| | | if(entry_type == DEAL_ENTRY_IN)
|
| | | {
|
| | | volume = HistoryDealGetDouble(deal, DEAL_VOLUME);
|
2026-08-16 11:12:59 +07:00 | | | position_type = (HistoryDealGetInteger(deal, DEAL_TYPE) == DEAL_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; // Gate B evidence
|
2026-08-12 19:54:12 +07:00 | | | entry_price = HistoryDealGetDouble(deal, DEAL_PRICE);
|
| | | const ulong order = HistoryDealGetInteger(deal, DEAL_ORDER);
|
| | | if(order != 0 && HistoryOrderSelect(order))
|
| | | comment = HistoryOrderGetString(order, ORDER_COMMENT);
|
| | | }
|
| | | else
|
| | | if(entry_type == DEAL_ENTRY_OUT || entry_type == DEAL_ENTRY_INOUT)
|
| | | {
|
| | | // net outcome in deposit currency (raw PnL + swap + commission)
|
| | | profit += HistoryDealGetDouble(deal, DEAL_PROFIT)
|
| | | + HistoryDealGetDouble(deal, DEAL_SWAP)
|
| | | + HistoryDealGetDouble(deal, DEAL_COMMISSION);
|
| | | close_time = (datetime)HistoryDealGetInteger(deal, DEAL_TIME);
|
| | | have_close = true;
|
2026-08-16 11:12:59 +07:00 | | | //--- Gate B evidence accumulators (additive) ---
|
| | | swap_sum += HistoryDealGetDouble(deal, DEAL_SWAP);
|
| | | commission_sum += HistoryDealGetDouble(deal, DEAL_COMMISSION);
|
| | | const double dvol = HistoryDealGetDouble(deal, DEAL_VOLUME);
|
| | | const double dprc = HistoryDealGetDouble(deal, DEAL_PRICE);
|
| | | if(dvol > 0.0)
|
| | | {
|
| | | exit_price += dvol * dprc;
|
| | | exit_volume += dvol;
|
| | | }
|
| | | exit_deal = deal;
|
| | | deal_reason = HistoryDealGetInteger(deal, DEAL_REASON);
|
2026-08-12 19:54:12 +07:00 | | | }
|
| | | }
|
2026-08-16 11:12:59 +07:00 | | | if(exit_volume > 0.0) // Gate B evidence: volume-weighted exit price
|
| | | exit_price /= exit_volume;
|
2026-08-12 19:54:12 +07:00 | | | if(!have_close || volume <= 0.0)
|
| | | return false;
|
| | | //--- recover the original AI score and initial SL from the comment ---
|
| | | double score = -1.0, initial_sl = 0.0;
|
| | | if(!ParseCenComment(comment, score, initial_sl))
|
| | | return false; // not a Centaur trade
|
| | | //--- initial risk amount in deposit currency ---
|
| | | const double risk_per_lot = MathAbs(entry_price - initial_sl) * (tick_value / tick_size);
|
| | | if(risk_per_lot <= 0.0)
|
| | | return false;
|
| | | const double risk_amount = risk_per_lot * volume;
|
| | | out.valid = true;
|
| | | out.ticket = ticket;
|
| | | out.profit = profit;
|
| | | out.r_multiple = (risk_amount > 0.0) ? profit / risk_amount : 0.0;
|
| | | out.initial_ai_score = score;
|
| | | out.close_time = close_time;
|
| | | out.symbol = m_symbol;
|
2026-08-16 11:12:59 +07:00 | | | //--- Gate B evidence fields (additive) ---
|
| | | out.entry_price = entry_price;
|
| | | out.initial_sl = initial_sl;
|
| | | out.position_type = position_type;
|
| | | out.exit_price = exit_price;
|
| | | out.exit_volume = exit_volume;
|
| | | out.exit_deal = exit_deal;
|
| | | out.deal_reason = deal_reason;
|
| | | out.swap = swap_sum;
|
| | | out.commission = commission_sum;
|
2026-08-12 19:54:12 +07:00 | | | return true;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| ParseCenComment — extract "CEN:<score>:<sl>" from the comment. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CHistoryTracker::ParseCenComment(const string comment, double &score, double &initial_sl)
|
| | | {
|
| | | score = -1.0;
|
| | | initial_sl = 0.0;
|
| | | const int p = StringFind(comment, "CEN:");
|
| | | if(p < 0)
|
| | | return false;
|
| | | const string rest = StringSubstr(comment, p + 4);
|
| | | const int sep = StringFind(rest, ":");
|
| | | if(sep < 0)
|
| | | return false;
|
| | | score = StringToDouble(StringSubstr(rest, 0, sep));
|
| | | initial_sl = StringToDouble(StringSubstr(rest, sep + 1));
|
| | | return (score >= 0.0 && initial_sl > 0.0);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| Enqueue — append a closed-trade record to the pending queue. |
|
| | | //+------------------------------------------------------------------+
|
| | | void CHistoryTracker::Enqueue(const SClosedTrade &rec)
|
| | | {
|
| | | const int n = ArraySize(m_pending);
|
| | | ArrayResize(m_pending, n + 1);
|
| | | m_pending[n] = rec;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| CompactQueue — shift live records to the front and shrink. |
|
| | | //| (ArrayCopy is not permitted on struct arrays — manual shift.) |
|
| | | //+------------------------------------------------------------------+
|
| | | void CHistoryTracker::CompactQueue()
|
| | | {
|
| | | const int size = ArraySize(m_pending);
|
| | | if(m_pending_head <= 0)
|
| | | return;
|
| | | const int remaining = size - m_pending_head;
|
| | | for(int i = 0; i < remaining; i++)
|
| | | m_pending[i] = m_pending[m_pending_head + i];
|
| | | ArrayResize(m_pending, remaining);
|
| | | m_pending_head = 0;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| PendingCount — closed trades awaiting delivery. |
|
| | | //+------------------------------------------------------------------+
|
| | | int CHistoryTracker::PendingCount() const
|
| | | {
|
| | | return ArraySize(m_pending) - m_pending_head;
|
| | | }
|
| | |
|
| | | #endif // HISTORYTRACKER_MQH
|