2026-08-12 19:54:12 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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>
|
| | |
|
2026-08-16 11:12:59 +07:00 | | | //--- 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
|
| | | };
|
| | |
|
2026-08-12 19:54:12 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| |
|
| | | //+------------------------------------------------------------------+
|
| | | 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
|
| | |
|
2026-08-16 11:12:59 +07:00 | | | //--- 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
|
2026-08-16 21:27:41 +07:00 | | | //--- 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_")
|
2026-08-16 11:12:59 +07:00 | | | ulong m_last_flush_ms; // GetTickCount64() of last flush
|
| | | bool m_mgmt_flush_failed; // set when a flush could not complete (governance event)
|
| | |
|
2026-08-12 19:54:12 +07:00 | | | //--- 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 ---
|
2026-08-16 11:12:59 +07:00 | | | 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();
|
2026-08-12 19:54:12 +07:00 | | |
|
| | | 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);
|
2026-08-16 21:27:41 +07:00 | | | //--- 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; }
|
2026-08-12 19:54:12 +07:00 | | |
|
| | | //--- read access ---
|
| | | bool IsReady() const { return m_ready; }
|
| | | string Symbol() const { return m_symbol; }
|
2026-08-16 11:12:59 +07:00 | | |
|
| | | //--- 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; }
|
2026-08-12 19:54:12 +07:00 | | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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),
|
2026-08-16 11:12:59 +07:00 | | | 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),
|
2026-08-16 21:27:41 +07:00 | | | m_mgmt_flush_failed(false),
|
| | | //--- EXP-DSO-001 (additive) ---
|
| | | m_fixed_risk_mode(false),
|
| | | m_fixed_risk_pct(0.25),
|
| | | m_evidence_prefix("exp003b_")
|
2026-08-12 19:54:12 +07:00 | | | {
|
| | | 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)
|
| | | {
|
2026-08-16 11:12:59 +07:00 | | | //--- Gate B evidence: per-attempt reset (additive observability) ---
|
| | | ResetLastOrderResult();
|
2026-08-12 19:54:12 +07:00 | | | //--- 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;
|
2026-08-16 21:27:41 +07:00 | | | 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
|
2026-08-12 19:54:12 +07:00 | | | 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;
|
| | | }
|
2026-08-16 11:12:59 +07:00 | | | m_last_requested_lot = lot; // Gate B evidence: requested volume (additive observability)
|
2026-08-12 19:54:12 +07:00 | | | //--- 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);
|
2026-08-16 11:12:59 +07:00 | | | //--- 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();
|
2026-08-12 19:54:12 +07:00 | | | 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;
|
2026-08-13 09:57:09 +07:00 | | | //--- 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;
|
| | | }
|
2026-08-12 19:54:12 +07:00 | | | // 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)))
|
| | | {
|
2026-08-16 11:12:59 +07:00 | | | if(ModifyStop(ticket, new_sl, cur_tp, "BE_MOVE"))
|
2026-08-12 19:54:12 +07:00 | | | 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)))
|
| | | {
|
2026-08-16 11:12:59 +07:00 | | | if(ModifyStop(ticket, new_sl, cur_tp, "TRAIL"))
|
2026-08-12 19:54:12 +07:00 | | | PrintFormat("[COrderExecutor] INFO: ticket %I64u trailed to SL %.5f.", ticket, new_sl);
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| ModifyStop — central SL modify with CTrade failure handling. |
|
| | | //+------------------------------------------------------------------+
|
2026-08-16 11:12:59 +07:00 | | | bool COrderExecutor::ModifyStop(const ulong ticket, const double new_sl, const double cur_tp, const string ev_type)
|
2026-08-12 19:54:12 +07:00 | | | {
|
2026-08-16 11:12:59 +07:00 | | | 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)
|
2026-08-12 19:54:12 +07:00 | | | 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);
|
| | | }
|
| | |
|
2026-08-16 11:12:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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;
|
2026-08-16 21:27:41 +07:00 | | | const string path = m_evidence_prefix + "management.csv";
|
2026-08-16 11:12:59 +07:00 | | | 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;
|
| | | }
|
| | |
|
2026-08-12 19:54:12 +07:00 | | | #endif // ORDEREXECUTOR_MQH
|