//+------------------------------------------------------------------+ //| 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::"), 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 //--- 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::" 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) }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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; //--- 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; 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); position_type = (HistoryDealGetInteger(deal, DEAL_TYPE) == DEAL_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; // Gate B evidence 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; //--- 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); } } if(exit_volume > 0.0) // Gate B evidence: volume-weighted exit price exit_price /= exit_volume; 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; //--- 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; return true; } //+------------------------------------------------------------------+ //| ParseCenComment — extract "CEN::" 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