Centaur_Quant_Architecture/MQL5/Include/Execution/COrderExecutor.mqh

544 lines
25 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| COrderExecutor.mqh |
//| Centaur Quant Architecture — Execution Module |
//| Anti-Veto Execution & Centaur-Edge Trade Manager |
//+------------------------------------------------------------------+
//| PURPOSE |
//| Executes OB+FVG setups (SOrderBlockZone) as market orders via |
//| CTrade. Position size follows the Anti-Veto principle: the AI |
//| confidence score scales RISK only — it NEVER blocks execution. |
//| Dynamic management: low-confidence trades get an aggressive ATR |
//| trailing stop; high/moderate trades are left to breathe and only |
//| move to break-even at 1:1 RR. The original score + initial SL |
//| persist in the order comment ("CEN:<score>:<sl>") so management |
//| survives terminal restarts. |
//+------------------------------------------------------------------+
#property strict
#ifndef ORDEREXECUTOR_MQH
#define ORDEREXECUTOR_MQH
#include "COrderBlockScanner.mqh"
#include <Trade\Trade.mqh>
//--- Gate B evidence: ratified management-event batching parameters (human decision 2026-08-16) ---
#define MGMT_BATCH_MAX 100 // batch size: 100 records
#define MGMT_FLUSH_MS 1000 // flush interval: 1000 ms maximum
#define MGMT_BUFFER_MAX_BYTES (1048576) // max in-memory buffer: 1 MB
//--- Gate B evidence: batched management-event record (additive observability) ---
struct SManagementEventRecord
{
ulong ticket; // position ticket
datetime ev_time; // simulated time of the event
string ev_type; // BE_MOVE | TRAIL | MODIFY_OK | MODIFY_FAIL
double new_sl; // requested new stop level
double cur_tp; // preserved take-profit
uint retcode; // trade server retcode (0 = success)
string retcode_desc; // retcode description
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
class COrderExecutor
{
private:
string m_symbol; // instrument traded
CSymbolNormalizer *m_normalizer; // non-owning sizing/normalization source
CTrade m_trade; // CTrade wrapper for order routing
bool m_ready;
long m_magic; // EA magic filter
int m_deviation_points; // max slippage in points
double m_min_risk_pct; // floor for low-confidence risk
int m_atr_period; // ATR period for buffers
double m_trail_atr_multiplier; // aggressive trail distance (ATR x)
double m_be_atr_multiplier; // break-even offset distance (ATR x)
double m_be_threshold; // scores >= this get BE-only management
//--- Gate B evidence: additive observability (no logic change) ---
double m_last_requested_lot; // requested volume of the last order attempt
uint m_last_retcode; // last order retcode
string m_last_retcode_desc; // last order retcode description
ulong m_last_deal; // last order deal id
//--- Gate B evidence: management-event buffer (batched; zero-loss) ---
SManagementEventRecord m_mgmt_buf[]; // in-memory buffer
int m_mgmt_count; // buffered records
//--- EXP-DSO-001: isolated fixed-risk diagnostic mode (D4; production unchanged when disabled) ---
bool m_fixed_risk_mode; // true = fixed diagnostic risk (fused score MUST NOT scale risk)
double m_fixed_risk_pct; // fixed risk % (ratified D4 = 0.25)
string m_evidence_prefix; // evidence file prefix ("exp003b_" | "exp_dso_001_")
ulong m_last_flush_ms; // GetTickCount64() of last flush
bool m_mgmt_flush_failed; // set when a flush could not complete (governance event)
//--- recover "CEN:<score>:<sl>" metadata from an order comment ---
bool ParseStoredData(const string comment, double &score, double &orig_sl);
//--- highest high / lowest low since the position opened ---
double ExtremeSinceOpen(const datetime open_time, const bool want_high);
//--- single SL modify point with robust CTrade failure logging ---
bool ModifyStop(const ulong ticket, const double new_sl, const double cur_tp, const string ev_type);
//--- Gate B evidence: management-event logging (batched; zero-loss) ---
void LogManagementEvent(const ulong ticket, const string ev_type, const double new_sl, const double cur_tp, const uint retcode, const string retcode_desc);
void FlushManagementLog();
void ResetLastOrderResult();
public:
COrderExecutor(CSymbolNormalizer *normalizer,
const string symbol = "",
const long magic = 0);
~COrderExecutor();
//--- anti-veto setup execution; returns the position/order ticket (0 on failure) ---
ulong ExecuteSetup(const SOrderBlockZone &zone, const double risk_percent,
const double ai_confidence_score);
//--- dynamic trade management for open positions on the symbol ---
void ManagePositions(const double ai_confidence_score);
//--- tuning ---
void SetDeviationPoints(const int points);
void SetMinimumRiskPercent(const double pct);
void SetTrailMultiplier(const double mult);
void SetBreakEvenMultiplier(const double mult);
//--- EXP-DSO-001: isolated fixed-risk diagnostic mode (additive; production unchanged when disabled) ---
void SetFixedRiskMode(const bool enable, const double pct) { m_fixed_risk_mode = enable; m_fixed_risk_pct = (pct > 0.0 ? pct : 0.25); }
void SetEvidencePrefix(const string prefix) { if(StringLen(prefix) > 0) m_evidence_prefix = prefix; }
//--- read access ---
bool IsReady() const { return m_ready; }
string Symbol() const { return m_symbol; }
//--- Gate B evidence: read access (additive observability) ---
double LastRequestedLot() const { return m_last_requested_lot; }
uint LastRetcode() const { return m_last_retcode; }
string LastRetcodeDescription() const { return m_last_retcode_desc; }
ulong LastDeal() const { return m_last_deal; }
void FlushManagementEvents() { FlushManagementLog(); }
bool ManagementFlushFailed() const { return m_mgmt_flush_failed; }
};
//+------------------------------------------------------------------+
//| Constructor — bind normalizer (non-owning) and resolve symbol. |
//+------------------------------------------------------------------+
COrderExecutor::COrderExecutor(CSymbolNormalizer *normalizer,
const string symbol,
const long magic)
: m_symbol(symbol),
m_normalizer(normalizer),
m_ready(false),
m_magic(magic),
m_deviation_points(20),
m_min_risk_pct(0.25),
m_atr_period(14),
m_trail_atr_multiplier(1.0),
m_be_atr_multiplier(0.1),
m_be_threshold(50.0),
//--- Gate B evidence (additive) ---
m_last_requested_lot(0.0),
m_last_retcode(0),
m_last_deal(0),
m_mgmt_count(0),
m_last_flush_ms(0),
m_mgmt_flush_failed(false),
//--- EXP-DSO-001 (additive) ---
m_fixed_risk_mode(false),
m_fixed_risk_pct(0.25),
m_evidence_prefix("exp003b_")
{
if(m_normalizer == NULL)
{
PrintFormat("[COrderExecutor] ERROR: null CSymbolNormalizer pointer.");
return;
}
if(StringLen(m_symbol) == 0)
m_symbol = m_normalizer.Symbol();
if(StringLen(m_symbol) == 0)
{
PrintFormat("[COrderExecutor] ERROR: no symbol available (normalizer not bound).");
return;
}
m_ready = m_normalizer.IsReady();
if(!m_ready)
PrintFormat("[COrderExecutor] ERROR: normalizer not ready for %s.", m_symbol);
else
PrintFormat("[COrderExecutor] INFO: ready on %s (magic %I64d).", m_symbol, m_magic);
}
//+------------------------------------------------------------------+
//| Destructor — non-owning pointer; nothing to release. |
//+------------------------------------------------------------------+
COrderExecutor::~COrderExecutor()
{
}
//+------------------------------------------------------------------+
//| ExecuteSetup — Anti-Veto execution of a detected OB+FVG zone. |
//| Risk scaling by AI confidence (score is advisory, NEVER a veto): |
//| >= 70% : full risk_percent |
//| 50-69% : half risk (0.5 * risk_percent) |
//| < 50% : STILL EXECUTED at quarter-risk (floored at a strictly |
//| defined minimum, m_min_risk_pct) |
//| Lot size is computed by CSymbolNormalizer::CalculateLotSize() |
//| from the adjusted risk and the zone's SL distance. |
//+------------------------------------------------------------------+
ulong COrderExecutor::ExecuteSetup(const SOrderBlockZone &zone,
const double risk_percent,
const double ai_confidence_score)
{
//--- Gate B evidence: per-attempt reset (additive observability) ---
ResetLastOrderResult();
//--- rigid validation ---
if(!m_ready)
{
PrintFormat("[COrderExecutor] ERROR: executor not ready.");
return 0;
}
if(!zone.valid)
{
PrintFormat("[COrderExecutor] ERROR: invalid zone passed to ExecuteSetup.");
return 0;
}
if(!MathIsValidNumber(ai_confidence_score))
{
PrintFormat("[COrderExecutor] ERROR: AI confidence is NaN/Inf. Order refused.");
return 0;
}
if(risk_percent <= 0.0)
{
PrintFormat("[COrderExecutor] ERROR: risk_percent must be > 0 (got %G).", risk_percent);
return 0;
}
if(zone.entry <= 0.0 || zone.sl <= 0.0 || zone.tp <= 0.0)
{
PrintFormat("[COrderExecutor] ERROR: zone prices invalid (entry=%G sl=%G tp=%G).",
zone.entry, zone.sl, zone.tp);
return 0;
}
//--- Anti-Veto risk scaling from the AI score ---
const double score = MathMax(0.0, MathMin(ai_confidence_score, 100.0));
double adjusted_risk = risk_percent;
if(m_fixed_risk_mode)
adjusted_risk = m_fixed_risk_pct; // EXP-DSO-001 (D4): FIXED diagnostic risk; fused score MUST NOT scale risk
else
if(score >= 70.0)
adjusted_risk = risk_percent;
else
if(score >= 50.0)
adjusted_risk = 0.5 * risk_percent;
else
adjusted_risk = MathMax(0.25 * risk_percent, m_min_risk_pct); // execute anyway, quarter-risk
//--- precise lot from dynamic normalization ---
const double sl_distance = MathAbs(zone.entry - zone.sl);
if(sl_distance <= 0.0)
{
PrintFormat("[COrderExecutor] ERROR: zero SL distance. Order refused.");
return 0;
}
const double lot = m_normalizer.CalculateLotSize(adjusted_risk, sl_distance);
if(lot <= 0.0)
{
PrintFormat("[COrderExecutor] ERROR: CalculateLotSize returned %.5f. Order refused.", lot);
return 0;
}
m_last_requested_lot = lot; // Gate B evidence: requested volume (additive observability)
//--- trading guards (never send blind) ---
if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
{
PrintFormat("[COrderExecutor] ERROR: MQL_TRADE_ALLOWED is false. Order refused.");
return 0;
}
if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
{
PrintFormat("[COrderExecutor] ERROR: TERMINAL_TRADE_ALLOWED is false. Order refused.");
return 0;
}
//--- CTrade configuration for this symbol ---
m_trade.SetExpertMagicNumber(m_magic);
m_trade.SetDeviationInPoints(m_deviation_points);
m_trade.SetTypeFillingBySymbol(m_symbol);
//--- persist original score + initial SL in the comment for management ---
const string comment = StringFormat("CEN:%.1f:%s", score,
DoubleToString(zone.sl, m_normalizer.Digits()));
//--- market order; zone SL/TP passed through untouched ---
const bool ok = zone.is_bullish ?
m_trade.Buy(lot, m_symbol, 0.0, zone.sl, zone.tp, comment) :
m_trade.Sell(lot, m_symbol, 0.0, zone.sl, zone.tp, comment);
//--- Gate B evidence: order result capture (additive observability) ---
m_last_retcode = m_trade.ResultRetcode();
m_last_retcode_desc = m_trade.ResultRetcodeDescription();
m_last_deal = m_trade.ResultDeal();
if(!ok)
{
PrintFormat("[COrderExecutor] ERROR: %s order failed. Retcode=%u (%s) deal=%I64u comment='%s'.",
zone.is_bullish ? "BUY" : "SELL",
m_trade.ResultRetcode(), m_trade.ResultRetcodeDescription(),
m_trade.ResultDeal(), m_trade.ResultComment());
return 0;
}
const ulong ticket = m_trade.ResultOrder(); // market order ticket == position ticket
PrintFormat("[COrderExecutor] INFO: %s executed. Ticket=%I64u deal=%I64u lot=%.2f score=%.1f risk=%.2f%% entry=%.5f sl=%.5f tp=%.5f.",
zone.is_bullish ? "BUY" : "SELL", ticket, m_trade.ResultDeal(),
lot, score, adjusted_risk, zone.entry, zone.sl, zone.tp);
return ticket;
}
//+------------------------------------------------------------------+
//| ManagePositions — Centaur Edge dynamic management. |
//| Iterates open positions of this symbol/magic and classifies each |
//| by its ORIGINAL AI score (recovered from the order comment). |
//| HIGH / MODERATE (>= threshold): let it breathe — SL only moves |
//| to break-even once price reaches 1:1 RR (entry +/- initial |
//| risk). |
//| LOW (< threshold): aggressive ATR trailing stop locked to the |
//| extreme high/low since the position opened. |
//| SL is only ever tightened; the current TP is preserved. |
//+------------------------------------------------------------------+
void COrderExecutor::ManagePositions(const double ai_confidence_score)
{
if(!m_ready)
return;
//--- connection guard: offline = positions keep static SL/TP (server-side), skip client management ---
if(!TerminalInfoInteger(TERMINAL_CONNECTED))
{
static datetime s_last_mgmt_warn = 0;
if(TimeCurrent() - s_last_mgmt_warn >= 30)
{
s_last_mgmt_warn = TimeCurrent();
Print("[COrderExecutor] WARNING: market disconnected - position management suspended (SL/TP server-side tetap aktif).");
}
return;
}
// ATR-derived buffers for the two management tiers (one pass per call)
const double trail = m_normalizer.GetATRBuffer(m_atr_period, m_trail_atr_multiplier);
const double be_buffer = m_normalizer.GetATRBuffer(m_atr_period, m_be_atr_multiplier);
if(trail <= 0.0 || be_buffer <= 0.0)
{
PrintFormat("[COrderExecutor] WARNING: ATR buffers unavailable; position management skipped.");
return;
}
const double bid = SymbolInfoDouble(m_symbol, SYMBOL_BID);
const double ask = SymbolInfoDouble(m_symbol, SYMBOL_ASK);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0 || !PositionSelectByTicket(ticket))
continue;
if(PositionGetString(POSITION_SYMBOL) != m_symbol)
continue;
if((long)PositionGetInteger(POSITION_MAGIC) != m_magic)
continue;
const long type = PositionGetInteger(POSITION_TYPE);
const double entry = PositionGetDouble(POSITION_PRICE_OPEN);
const double cur_sl = PositionGetDouble(POSITION_SL);
const double cur_tp = PositionGetDouble(POSITION_TP);
// original score + initial SL recovered from the order comment
double orig_score = -1.0;
double orig_sl = 0.0;
if(!ParseStoredData(PositionGetString(POSITION_COMMENT), orig_score, orig_sl))
{
orig_score = ai_confidence_score; // legacy position fallback
orig_sl = cur_sl;
}
//================ HIGH / MODERATE TIER: break-even at 1:1 ================
if(orig_score >= m_be_threshold)
{
const double initial_risk = MathAbs(entry - (orig_sl > 0.0 ? orig_sl : cur_sl));
if(initial_risk <= 0.0)
continue;
double new_sl = 0.0;
if(type == POSITION_TYPE_BUY && bid >= entry + initial_risk)
new_sl = m_normalizer.NormalizePrice(entry + be_buffer);
else
if(type == POSITION_TYPE_SELL && ask <= entry - initial_risk)
new_sl = m_normalizer.NormalizePrice(entry - be_buffer);
if(new_sl > 0.0 &&
((type == POSITION_TYPE_BUY && new_sl > cur_sl) ||
(type == POSITION_TYPE_SELL && new_sl < cur_sl)))
{
if(ModifyStop(ticket, new_sl, cur_tp, "BE_MOVE"))
PrintFormat("[COrderExecutor] INFO: ticket %I64u moved to break-even (SL %.5f).", ticket, new_sl);
}
}
//================ LOW TIER: aggressive ATR trailing stop ================
else
{
const datetime open_time = (datetime)PositionGetInteger(POSITION_TIME);
double new_sl = 0.0;
if(type == POSITION_TYPE_BUY)
{
const double highest = ExtremeSinceOpen(open_time, true);
if(highest > 0.0)
new_sl = m_normalizer.NormalizePrice(highest - trail);
}
else
if(type == POSITION_TYPE_SELL)
{
const double lowest = ExtremeSinceOpen(open_time, false);
if(lowest > 0.0)
new_sl = m_normalizer.NormalizePrice(lowest + trail);
}
if(new_sl > 0.0 &&
((type == POSITION_TYPE_BUY && new_sl > cur_sl) ||
(type == POSITION_TYPE_SELL && new_sl < cur_sl)))
{
if(ModifyStop(ticket, new_sl, cur_tp, "TRAIL"))
PrintFormat("[COrderExecutor] INFO: ticket %I64u trailed to SL %.5f.", ticket, new_sl);
}
}
}
}
//+------------------------------------------------------------------+
//| ModifyStop — central SL modify with CTrade failure handling. |
//+------------------------------------------------------------------+
bool COrderExecutor::ModifyStop(const ulong ticket, const double new_sl, const double cur_tp, const string ev_type)
{
const bool mgmt_ok = m_trade.PositionModify(ticket, new_sl, cur_tp);
LogManagementEvent(ticket, ev_type, new_sl, cur_tp,
(mgmt_ok ? (uint)0 : m_trade.ResultRetcode()),
(mgmt_ok ? "OK" : m_trade.ResultRetcodeDescription())); // Gate B evidence
if(mgmt_ok)
return true;
PrintFormat("[COrderExecutor] WARNING: ticket %I64u SL modify failed. Retcode=%u (%s).",
ticket, m_trade.ResultRetcode(), m_trade.ResultRetcodeDescription());
return false;
}
//+------------------------------------------------------------------+
//| ParseStoredData — extract "CEN:<score>:<sl>" from the comment. |
//+------------------------------------------------------------------+
bool COrderExecutor::ParseStoredData(const string comment, double &score, double &orig_sl)
{
score = -1.0;
orig_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));
orig_sl = StringToDouble(StringSubstr(rest, sep + 1));
return (score >= 0.0 && orig_sl > 0.0);
}
//+------------------------------------------------------------------+
//| ExtremeSinceOpen — highest high (want_high=true) or lowest low |
//| over all bars from the position's open time up to the live bar. |
//+------------------------------------------------------------------+
double COrderExecutor::ExtremeSinceOpen(const datetime open_time, const bool want_high)
{
const int shift = iBarShift(m_symbol, PERIOD_CURRENT, open_time);
if(shift < 0)
return 0.0;
double arr[];
const int got = want_high ?
CopyHigh(m_symbol, PERIOD_CURRENT, 0, shift + 1, arr) :
CopyLow(m_symbol, PERIOD_CURRENT, 0, shift + 1, arr);
if(got <= 0)
return 0.0;
double ext = arr[0];
for(int k = 1; k < got; k++)
{
if(want_high)
ext = MathMax(ext, arr[k]);
else
ext = MathMin(ext, arr[k]);
}
return ext;
}
//+------------------------------------------------------------------+
//| SetDeviationPoints — max slippage in points for market orders. |
//+------------------------------------------------------------------+
void COrderExecutor::SetDeviationPoints(const int points)
{
m_deviation_points = MathMax(points, 0);
}
//+------------------------------------------------------------------+
//| SetMinimumRiskPercent — floor applied to low-confidence risk. |
//+------------------------------------------------------------------+
void COrderExecutor::SetMinimumRiskPercent(const double pct)
{
m_min_risk_pct = MathMax(pct, 0.0);
}
//+------------------------------------------------------------------+
//| SetTrailMultiplier — ATR multiplier of the aggressive trail stop. |
//+------------------------------------------------------------------+
void COrderExecutor::SetTrailMultiplier(const double mult)
{
m_trail_atr_multiplier = MathMax(mult, 0.1);
}
//+------------------------------------------------------------------+
//| SetBreakEvenMultiplier — ATR multiplier of the break-even offset. |
//+------------------------------------------------------------------+
void COrderExecutor::SetBreakEvenMultiplier(const double mult)
{
m_be_atr_multiplier = MathMax(mult, 0.01);
}
//+------------------------------------------------------------------+
//| Gate B evidence — management-event logging (batched; zero-loss). |
//| Batching parameters: 100 records / 1000 ms / 1 MB / flush-on- |
//| overflow / ordered flush / zero loss (human decision 2026-08-16). |
//+------------------------------------------------------------------+
void COrderExecutor::LogManagementEvent(const ulong ticket, const string ev_type,
const double new_sl, const double cur_tp,
const uint retcode, const string retcode_desc)
{
const int n = m_mgmt_count;
ArrayResize(m_mgmt_buf, n + 1);
m_mgmt_buf[n].ticket = ticket;
m_mgmt_buf[n].ev_time = TimeCurrent();
m_mgmt_buf[n].ev_type = ev_type;
m_mgmt_buf[n].new_sl = new_sl;
m_mgmt_buf[n].cur_tp = cur_tp;
m_mgmt_buf[n].retcode = retcode;
m_mgmt_buf[n].retcode_desc= retcode_desc;
m_mgmt_count = n + 1;
//--- ratified batching controls: batch size / flush interval / max buffer ---
const bool batch_full = (m_mgmt_count >= MGMT_BATCH_MAX);
const bool interval_elapsed = (GetTickCount64() - m_last_flush_ms >= MGMT_FLUSH_MS);
const bool buffer_overflow = ((ulong)m_mgmt_count * (ulong)sizeof(SManagementEventRecord) >= (ulong)MGMT_BUFFER_MAX_BYTES);
if(batch_full || interval_elapsed || buffer_overflow)
FlushManagementLog();
}
//+------------------------------------------------------------------+
void COrderExecutor::FlushManagementLog()
{
if(m_mgmt_count <= 0)
return;
const string path = m_evidence_prefix + "management.csv";
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
//--- zero-loss: keep the buffer; flag the governance event (Evidence Protocol R / AD-18) ---
m_mgmt_flush_failed = true;
PrintFormat("[COrderExecutor] GOVERNANCE EVENT: management-event flush FAILED (err %d); %d records buffered (no drop).",
GetLastError(), m_mgmt_count);
return;
}
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "seq,ticket,sim_time,event_type,new_sl,cur_tp,retcode,retcode_desc\r\n");
for(int i = 0; i < m_mgmt_count; i++)
{
FileWriteString(h, StringFormat("%d,%I64u,%s,%s,%.5f,%.5f,%u,%s\r\n",
i + 1, m_mgmt_buf[i].ticket,
TimeToString(m_mgmt_buf[i].ev_time, TIME_DATE | TIME_SECONDS),
m_mgmt_buf[i].ev_type, m_mgmt_buf[i].new_sl, m_mgmt_buf[i].cur_tp,
m_mgmt_buf[i].retcode, m_mgmt_buf[i].retcode_desc));
}
FileClose(h);
m_mgmt_count = 0;
ArrayResize(m_mgmt_buf, 0);
m_last_flush_ms = GetTickCount64();
}
//+------------------------------------------------------------------+
void COrderExecutor::ResetLastOrderResult()
{
m_last_requested_lot = 0.0;
m_last_retcode = 0;
m_last_retcode_desc = "";
m_last_deal = 0;
}
#endif // ORDEREXECUTOR_MQH