Centaur_Quant_Architecture/MQL5/Include/Execution/CHistoryTracker.mqh

325 lines
14 KiB
MQL5

//+------------------------------------------------------------------+
//| 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
//--- 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)
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
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:<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