//+------------------------------------------------------------------+ //| 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::") so management | //| survives terminal restarts. | //+------------------------------------------------------------------+ #property strict #ifndef ORDEREXECUTOR_MQH #define ORDEREXECUTOR_MQH #include "COrderBlockScanner.mqh" #include //--- 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::" 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::" 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