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
|