Centaur_Quant_Architecture/MQL5/Experts/CentaurQuant.mq5

1185 lines
57 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| CentaurQuant.mq5 |
//| Centaur Quant Architecture — Composition Root |
//| Multi-Asset SMC Executor + SDP Telemetry Bridge (MQL5) |
//+------------------------------------------------------------------+
//| PURPOSE |
//| Thin composition root wiring all foundation modules: |
//| Core : CSymbolNormalizer (dynamic normalization) |
//| Network : CSDPEncoder, CSocketClient (SDP + non-blocking TCP) |
//| Data : CDataHarvester, CContextPackager (telemetry/context)|
//| Execution : COrderBlockScanner, COrderExecutor (SMC + anti-veto)|
//| Harvest, structural scanning, AI advisory round-trip and trade |
//| management run from a bounded 100 ms timer + tick handlers. |
//| The AI advisory NEVER vetoes execution (Anti-Veto principle). |
//+------------------------------------------------------------------+
#property strict
//--- foundation modules -------------------------------------------------
#include "..\Include\Core\CSymbolNormalizer.mqh"
#include "..\Include\Network\CSDPEncoder.mqh"
#include "..\Include\Network\CSocketClient.mqh"
#include "..\Include\Data\DataHarvester.mqh"
#include "..\Include\Data\ContextPackager.mqh"
#include "..\Include\Execution\COrderBlockScanner.mqh"
#include "..\Include\Execution\COrderExecutor.mqh"
#include "..\Include\Execution\CHistoryTracker.mqh"
#include "..\Include\Models\CLLMClient.mqh"
#include "..\Include\Models\CFuzzyFusion.mqh"
//--- inputs -------------------------------------------------------------
sinput string InpHost = "127.0.0.1"; // TCP host (Python router)
sinput int InpPort = 5555; // TCP port
sinput double InpRiskPercent = 1.0; // risk % of free margin per trade
sinput long InpMagic = 20260812; // EA magic number
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
sinput int InpTimerMs = 100; // orchestration timer (ms)
sinput int InpAiTimeoutMs = 2000; // AI response wait budget (ms)
sinput bool InpEnableTrading = true; // allow order execution
sinput bool InpExecuteWithoutAi = true; // anti-veto: trade on AI failure
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
sinput int InpHarvestIntervalMs = 1000; // tick harvest throttle (ms)
sinput int InpHeartbeatIntervalMs = 30000; // heartbeat period (ms)
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
sinput int InpScanDepth = 200; // OB/FVG scan depth (bars)
sinput int InpFractalRadius = 2; // swing fractal radius
sinput double InpAtrMultiplier = 1.0; // swing significance (x ATR)
sinput int InpAtrPeriod = 14; // ATR period
sinput int InpDeviationPoints = 20; // max slippage (points)
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
sinput bool InpEnableLLM = true; // use "The Brain" via MCP bridge (satu-satunya jalur)
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
sinput int InpLLMTimeoutMs = 15000; // LLM fallback timeout (ms)
sinput double InpFuzzyThreshold = 55.0; // fuzzy gate: min fused confidence utk buka posisi
sinput double InpFuzzyAlpha = 0.10; // laju adaptasi bobot dinamis
sinput double InpWStructure = 0.35; // bobot dasar: struktur (OB+FVG)
sinput double InpWTrend = 0.20; // bobot dasar: trend (EMA)
sinput double InpWMomentum = 0.15; // bobot dasar: momentum (RSI)
sinput double InpWVolatility = 0.10; // bobot dasar: volatilitas (ATR)
sinput double InpWAI = 0.20; // bobot dasar: AI advisory (LLM)
sinput double InpFuzzyMinW = 0.05; // clamp bobot bawah
sinput double InpFuzzyMaxW = 0.60; // clamp bobot atas
sinput bool InpPersistWeights = true; // simpan/muat bobot dinamis
sinput double InpProximityMult = 0.25; // EXP-003: proksimitas ATR scanner (B1)
sinput double InpExpansionMin = 1.2; // EXP-003: ambang ekspansi OB (B4)
sinput bool InpAllowMitigatedOB = false; // EXP-003: izinkan OB ter-mitigasi (B4)
sinput bool InpDSOEnable = false; // EXP-DSO-001: diagnostic mode (fused score OBSERVATIONAL; threshold 55 reference only)
sinput double InpDSORiskPercent = 0.25; // EXP-DSO-001: FIXED diagnostic risk % (D4 = 0.25)
//--- module instances (composition root; all heap-allocated) ------------
CSymbolNormalizer *g_norm = NULL;
CSDPEncoder *g_enc = NULL;
CSocketClient *g_sock = NULL;
CDataHarvester *g_harv = NULL;
CContextPackager *g_ctx = NULL;
COrderBlockScanner *g_scan = NULL;
COrderExecutor *g_exec = NULL;
CHistoryTracker *g_history = NULL;
CLLMClient *g_llm = NULL;
CFuzzyFusion *g_fuzzy = NULL;
SOrderBlockZone g_pending_zone; // zona setup menunggu skor LLM asinkron
string g_pending_context = ""; // konteks SDP utk eksekusi saat response LLM tiba
double g_last_trade_scores[FZ_AGENTS_TOTAL]; // skor agen saat entry (feedback bobot)
bool g_has_last_scores = false;
double g_fused_last = 0.0; // confidence fusi terakhir
int g_ema_fast = INVALID_HANDLE;
int g_ema_slow = INVALID_HANDLE;
int g_rsi = INVALID_HANDLE;
int g_atr_short = INVALID_HANDLE;
int g_atr_long = INVALID_HANDLE;
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
double g_last_ai_score = 0.0; // last AI confidence received (fallback for mgmt)
ulong g_last_connect_attempt = 0; // reconnect throttle
//--- EXP-DSO-001 diagnostic arm state (isolated; observational score) ---
string g_dso_run_id = "EXP-DSO-001-RUN1";
ulong g_dso_candidate_seq = 0; // candidate_id (monotonic)
ulong g_dso_last_candidate_id = 0; // last candidate_id written (attempt linkage)
string g_dso_used_zones[]; // same-zone re-entry prohibition (D14)
//--- Gate B evidence: additive observability (Evidence Protocol v0.1 §4/§5.1) ---
struct SRoundTripCtx
{
ulong ticket; // position ticket == round_trip_id
ulong attempt_id; // producing attempt
string zone_id; // (direction, zone_low, zone_high)
};
SRoundTripCtx g_rt_ctx[]; // ticket -> (attempt_id, zone_id) linkage
ulong g_attempt_seq = 0; // attempt_id (monotonic; traceable per zone/decision)
datetime g_last_decision_time = 0; // decision reference timestamp (FuseAndExecute)
//--- helpers ------------------------------------------------------------
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
string TfName();
bool HasOpenPosition();
void EnsureConnection();
void HarvestAndFlush();
void ScanAndTrade();
ulong TryExecute(const SOrderBlockZone &zone, const double ai_score);
void EmitTradeOpened(const SOrderBlockZone &zone, const ulong ticket, const double ai_score, const ulong attempt_id);
double StructuralConfidence(const SOrderBlockZone &zone);
double AgentScoreTrend(const SOrderBlockZone &zone);
double AgentScoreMomentum(const SOrderBlockZone &zone);
double AgentScoreVolatility();
void FuseAndExecute(const SOrderBlockZone &zone, const string context_json);
void AppendSetupCSV(const SOrderBlockZone &zone, const string decision, const string reason);
//--- EXP-DSO-001 (isolated diagnostic arm; observational score) ---
void AppendDSOCandidate(const SOrderBlockZone &zone, const double fused, const string decision, const string reason, const string diag_state, const string diag_reason);
void AppendDSOAttempt(const SOrderBlockZone &zone, const double fused, const ulong ticket, const ulong attempt_id, const string reject_extra);
void AppendDSOCensoredPositions();
string ScoreBand(const double s);
bool DSOZoneUsed(const string zid);
void DSOAddZone(const string zid);
//--- Gate B evidence: additive observability helpers (Evidence Protocol v0.1 §4) ---
string ZoneID(const SOrderBlockZone &zone);
bool FindRTContext(const ulong ticket, SRoundTripCtx &ctx);
void AppendAttemptRecord(const SOrderBlockZone &zone, const double fused, const ulong ticket, const ulong attempt_id, const string reject_extra);
void AppendPositionOpen(const SOrderBlockZone &zone, const ulong ticket, const ulong attempt_id, const double ai_score);
void AppendExitRecord(const SClosedTrade &closed);
void AppendRoundTripRecord(const SClosedTrade &closed);
void WriteRunCompleteMarker(const int reason);
//+------------------------------------------------------------------+
//| OnInit — construct modules, connect, start the timer. |
//+------------------------------------------------------------------+
int OnInit()
{
g_norm = new CSymbolNormalizer(_Symbol);
if(g_norm == NULL || !g_norm.IsReady())
{
PrintFormat("[CentaurQuant] FATAL: CSymbolNormalizer failed for %s.", _Symbol);
if(g_norm != NULL)
delete g_norm;
g_norm = NULL;
return INIT_FAILED;
}
g_enc = new CSDPEncoder();
g_sock = new CSocketClient();
g_harv = new CDataHarvester(_Symbol);
g_ctx = new CContextPackager(_Symbol, PERIOD_CURRENT);
g_scan = new COrderBlockScanner(g_norm, _Symbol, PERIOD_CURRENT);
g_exec = new COrderExecutor(g_norm, _Symbol, InpMagic);
g_history = new CHistoryTracker(_Symbol, InpMagic);
if(InpEnableLLM)
{
g_llm = new CLLMClient(InpLLMTimeoutMs);
}
//--- multi-agent fuzzy fusion engine ---
g_fuzzy = new CFuzzyFusion();
{
double base_w[FZ_AGENTS_TOTAL];
base_w[FZ_STRUCTURE] = InpWStructure;
base_w[FZ_TREND] = InpWTrend;
base_w[FZ_MOMENTUM] = InpWMomentum;
base_w[FZ_VOLATILITY] = InpWVolatility;
base_w[FZ_AI] = InpWAI;
g_fuzzy.Configure(base_w, InpFuzzyAlpha, InpFuzzyMinW, InpFuzzyMaxW);
if(InpPersistWeights)
g_fuzzy.Load("centaur_weights.txt");
}
//--- indicator handles utk agen trend/momentum/volatilitas ---
g_ema_fast = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_EMA, PRICE_CLOSE);
g_ema_slow = iMA(_Symbol, PERIOD_CURRENT, 200, 0, MODE_EMA, PRICE_CLOSE);
g_rsi = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
g_atr_short = iATR(_Symbol, PERIOD_CURRENT, InpAtrPeriod);
g_atr_long = iATR(_Symbol, PERIOD_CURRENT, 50);
if(g_ema_fast == INVALID_HANDLE || g_ema_slow == INVALID_HANDLE || g_rsi == INVALID_HANDLE ||
g_atr_short == INVALID_HANDLE || g_atr_long == INVALID_HANDLE)
Print("[CentaurQuant] WARNING: sebagian indikator agen gagal dibuat - skor agen fallback ke netral 50.");
//--- runtime tuning from inputs ---
g_harv.SetHarvestInterval(InpHarvestIntervalMs);
g_harv.SetHeartbeatInterval(InpHeartbeatIntervalMs);
g_exec.SetDeviationPoints(InpDeviationPoints);
if(InpDSOEnable)
{
g_exec.SetFixedRiskMode(true, InpDSORiskPercent); // EXP-DSO-001 (D4): fixed diagnostic risk; score MUST NOT scale risk
g_exec.SetEvidencePrefix("exp_dso_001_");
PrintFormat("[CentaurQuant] EXP-DSO-001 DIAGNOSTIC MODE ACTIVE: fixed risk %.2f%%, fused_score OBSERVATIONAL (threshold %.1f reference only).",
InpDSORiskPercent, InpFuzzyThreshold);
}
//--- transport: non-fatal; auto-reconnect runs inside OnTimer ---
if(!MQLInfoInteger(MQL_TESTER))
g_sock.Connect(InpHost, InpPort, 3000);
else
Print("[CentaurQuant] INFO: Strategy Tester mode - TCP transport disabled (sockets not allowed); telemetry via file fallback.");
if(!InpEnableTrading)
FileDelete("exp003_setups.csv"); // EXP-003: run diagnostic mulai clean slate
EventSetMillisecondTimer(InpTimerMs);
PrintFormat("[CentaurQuant] INFO: initialized on %s %s | magic=%I64d risk=%.2f%% | TCP %s:%d",
_Symbol, TfName(), InpMagic, InpRiskPercent, InpHost, InpPort);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| OnDeinit — kill timer, disconnect, release all instances. |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
EventKillTimer();
if(g_sock != NULL)
{
g_sock.Disconnect();
delete g_sock;
g_sock = NULL;
}
if(g_exec != NULL)
{
g_exec.FlushManagementEvents(); // Gate B evidence: ordered flush of buffered V/W records (zero-loss)
delete g_exec;
g_exec = NULL;
}
if(g_llm != NULL)
{
delete g_llm;
g_llm = NULL;
}
if(g_history != NULL)
{
delete g_history;
g_history = NULL;
}
if(g_scan != NULL)
{
delete g_scan;
g_scan = NULL;
}
if(g_ctx != NULL)
{
delete g_ctx;
g_ctx = NULL;
}
if(g_harv != NULL)
{
delete g_harv;
g_harv = NULL;
}
if(g_enc != NULL)
{
delete g_enc;
g_enc = NULL;
}
if(g_norm != NULL)
{
delete g_norm;
g_norm = NULL;
}
if(g_fuzzy != NULL)
{
if(InpPersistWeights)
g_fuzzy.Save("centaur_weights.txt");
delete g_fuzzy;
g_fuzzy = NULL;
}
if(g_ema_fast != INVALID_HANDLE)
{
IndicatorRelease(g_ema_fast);
g_ema_fast = INVALID_HANDLE;
}
if(g_ema_slow != INVALID_HANDLE)
{
IndicatorRelease(g_ema_slow);
g_ema_slow = INVALID_HANDLE;
}
if(g_rsi != INVALID_HANDLE)
{
IndicatorRelease(g_rsi);
g_rsi = INVALID_HANDLE;
}
if(g_atr_short != INVALID_HANDLE)
{
IndicatorRelease(g_atr_short);
g_atr_short = INVALID_HANDLE;
}
if(g_atr_long != INVALID_HANDLE)
{
IndicatorRelease(g_atr_long);
g_atr_long = INVALID_HANDLE;
}
PrintFormat("[CentaurQuant] INFO: deinitialized (reason %d).", reason);
if(InpDSOEnable)
AppendDSOCensoredPositions(); // EXP-DSO-001 (D17): censor open-at-end positions
WriteRunCompleteMarker(reason); // Gate B evidence: ordered-shutdown completion marker
}
//+------------------------------------------------------------------+
//| OnTimer — orchestration pipeline (bounded, non-blocking): |
//| reconnect -> harvest flush -> scan/AI/execute -> manage positions.|
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| EmitClosedTrades — Trade_Closed feedback loop: drain the history |
//| tracker and push each closed trade as an SDP payload to the |
//| Python Router (score -> outcome training signal). |
//+------------------------------------------------------------------+
void EmitClosedTrades()
{
if(g_history == NULL || g_enc == NULL || g_sock == NULL)
return;
SClosedTrade closed;
while(g_history.Check(closed))
{
//--- feedback: adaptasi bobot dinamis agen dari outcome trade ---
if(g_fuzzy != NULL && g_has_last_scores && !InpDSOEnable) // EXP-DSO-001 (D5): weights FROZEN during diagnostic run
{
g_fuzzy.UpdateWeights(closed.profit > 0.0, g_last_trade_scores);
g_has_last_scores = false;
if(InpPersistWeights)
g_fuzzy.Save("centaur_weights.txt");
}
//--- Gate B evidence: exit + round-trip records (additive; Evidence Protocol §4 X–AG) ---
AppendExitRecord(closed);
AppendRoundTripRecord(closed);
const string payload = g_enc.EncodeTradeClosed(g_norm.Symbol(), closed.ticket,
closed.profit, closed.r_multiple, closed.initial_ai_score);
if(StringLen(payload) > 0 && !g_sock.Send(payload))
AppendTelemetryFile(payload);
}
}
//+------------------------------------------------------------------+
//| OnTimer — orchestration pipeline (bounded, non-blocking): |
//| reconnect -> harvest flush -> scan/AI/execute -> manage positions.|
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| LoadLLMKey — read the LLM API key from MQL5\Files\mql5_ai_key.txt|
//| when InpLLMKey is empty; the key is never stored in inputs/repo. |
//+------------------------------------------------------------------+
string LoadLLMKey()
{
const int h = FileOpen("mql5_ai_key.txt", FILE_READ | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
PrintFormat("[CentaurQuant] WARNING: InpLLMKey kosong dan MQL5\\Files\\mql5_ai_key.txt tidak ada. LLM dinonaktifkan.");
return "";
}
string k = "";
while(!FileIsEnding(h))
{
k += FileReadString(h);
if(StringFind(k, "\n") >= 0)
break;
}
FileClose(h);
StringTrimLeft(k);
StringTrimRight(k);
StringReplace(k, "\r", "");
StringReplace(k, "\n", "");
return k;
}
//+------------------------------------------------------------------+
//| OnTimer — orchestration pipeline (bounded, non-blocking): |
//| reconnect -> harvest flush -> scan/AI/execute -> manage positions.|
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| CheckPendingLLM — baca response LLM (file bridge) atau fallback. |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| ZoneStillValid — S1: revalidasi zona sebelum eksekusi async. |
//| Harga live masih dalam buffer proksimitas (ATR) DAN zona masih |
//| dalam jendela scan (umur < depth bar). Zona basi ditolak. |
//+------------------------------------------------------------------+
bool ZoneStillValid(const SOrderBlockZone &zone)
{
if(!zone.valid || zone.ob_time <= 0)
return false;
if(g_norm == NULL)
return false;
const double buf = g_norm.GetATRBuffer(InpAtrPeriod, InpProximityMult);
if(buf <= 0.0)
return false;
MqlTick t;
if(!SymbolInfoTick(_Symbol, t) || t.bid <= 0.0 || t.ask <= 0.0)
return false;
const datetime max_age = (datetime)(InpScanDepth * PeriodSeconds(PERIOD_CURRENT));
if(TimeCurrent() - zone.ob_time > max_age)
return false;
const double lo = zone.zone_low - buf;
const double hi = zone.zone_high + buf;
return ((t.bid >= lo && t.bid <= hi) || (t.ask >= lo && t.ask <= hi));
}
//+------------------------------------------------------------------+
//| CheckPendingLLM — baca response LLM (file bridge) atau fallback. |
//+------------------------------------------------------------------+
void CheckPendingLLM()
{
if(!InpEnableLLM || g_llm == NULL || !g_llm.IsReady() || !g_llm.IsPending())
return;
double llm_score = 0.0;
string llm_reason = "";
if(g_llm.TryReadResponse(llm_score, llm_reason))
{
g_last_ai_score = llm_score; // S5: skor AI masuk sebagai satu agen
if(ZoneStillValid(g_pending_zone)) // S1: zona masih valid?
{
if(g_fuzzy != NULL)
g_fuzzy.SetScore(FZ_AI, llm_score); // kontribusi agen AI
FuseAndExecute(g_pending_zone, g_pending_context); // keputusan kolektif fuzzy
}
else
{
PrintFormat("[CentaurQuant] WARNING: zona basi saat response tiba — dilewati.");
g_llm.ResetPending();
}
PrintFormat("[CentaurQuant] INFO: LLM score %.1f — %s", llm_score, llm_reason);
}
else
if(g_llm.DeadlinePassed())
{
PrintFormat("[CentaurQuant] WARNING: LLM response timeout; fallback AI=40 (anti-veto).");
g_llm.ResetPending();
g_last_ai_score = 40.0;
if(ZoneStillValid(g_pending_zone)) // S1: revalidasi pada timeout juga
{
if(g_fuzzy != NULL)
g_fuzzy.SetScore(FZ_AI, 40.0);
FuseAndExecute(g_pending_zone, g_pending_context);
}
else
PrintFormat("[CentaurQuant] WARNING: zona basi pada timeout — dilewati.");
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void OnTimer()
{
EnsureConnection();
HarvestAndFlush();
ScanAndTrade();
CheckPendingLLM();
g_exec.ManagePositions(g_last_ai_score);
EmitClosedTrades();
}
//+------------------------------------------------------------------+
//| OnTick — lightweight tick-rate duties: harvest freshness and |
//| dynamic trade management (Centaur Edge) on every tick. |
//+------------------------------------------------------------------+
void OnTick()
{
HarvestAndFlush();
CheckPendingLLM();
g_exec.ManagePositions(g_last_ai_score);
EmitClosedTrades();
}
//+------------------------------------------------------------------+
//| TfName — wire name of the chart timeframe. |
//+------------------------------------------------------------------+
string TfName()
{
string name = EnumToString((ENUM_TIMEFRAMES)_Period);
StringReplace(name, "PERIOD_", "");
return name;
}
//+------------------------------------------------------------------+
//| HasOpenPosition — true when this EA already holds a position on |
//| the symbol (single-position guard against stacking). |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Gate B evidence helpers — additive observability (EP v0.1 §4). |
//| All records are write-after-decision; zero-loss; no semantics |
//| change. File layout: MQL5\Files\exp003b_*.csv (+ run marker). |
//+------------------------------------------------------------------+
string ZoneID(const SOrderBlockZone &zone)
{
return StringFormat("%s|%.5f|%.5f", (zone.is_bullish ? "LONG" : "SHORT"),
zone.zone_low, zone.zone_high);
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| EXP-DSO-001 helpers — observational-score diagnostics (D1–D22). |
//| Isolated from production; active only when InpDSOEnable=true. |
//+------------------------------------------------------------------+
string ScoreBand(const double s)
{
if(s < 40.0) return "<40";
if(s < 45.0) return "40-44.99";
if(s < 50.0) return "45-49.99";
if(s < 55.0) return "50-54.99";
if(s < 60.0) return "55-59.99";
if(s < 65.0) return "60-64.99";
if(s < 70.0) return "65-69.99";
if(s < 75.0) return "70-74.99";
return ">=75";
}
string ThresholdSide(const double s)
{ return (s < InpFuzzyThreshold ? "BELOW_THRESHOLD" : "AT_OR_ABOVE_THRESHOLD"); }
bool DSOZoneUsed(const string zid)
{
for(int i = 0; i < ArraySize(g_dso_used_zones); i++)
if(g_dso_used_zones[i] == zid)
return true;
return false;
}
void DSOAddZone(const string zid)
{
const int n = ArraySize(g_dso_used_zones);
ArrayResize(g_dso_used_zones, n + 1);
g_dso_used_zones[n] = zid;
}
//+------------------------------------------------------------------+
//| AppendDSOCandidate — candidate census (observational; full range).|
//+------------------------------------------------------------------+
void AppendDSOCandidate(const SOrderBlockZone &zone, const double fused, const string decision,
const string reason, const string diag_state, const string diag_reason)
{
g_dso_last_candidate_id = ++g_dso_candidate_seq;
const string path = "exp_dso_001_candidates.csv";
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
PrintFormat("[CentaurQuant] GOVERNANCE EVENT: DSO candidate write FAILED (err %d).", GetLastError());
return;
}
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "candidate_id,timestamp,symbol,timeframe,zone_id,direction,entry,sl,tp,zone_low,zone_high,fvg_size,"
"structure_score,trend_score,momentum_score,volatility_score,ai_score,"
"w_structure,w_trend,w_momentum,w_volatility,w_ai,"
"fused_score,threshold_reference,score_band,threshold_side,decision,diagnostic_state,diagnostic_reason,experiment_id\r\n");
FileWriteString(h, StringFormat(
"%I64u,%s,%s,%s,%s,%s,%.5f,%.5f,%.5f,%.5f,%.5f,%.5f,"
"%.2f,%.2f,%.2f,%.2f,%.2f,"
"%.4f,%.4f,%.4f,%.4f,%.4f,"
"%.2f,%.2f,%s,%s,%s,%s,%s,%s\r\n",
g_dso_last_candidate_id,
TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
_Symbol, TfName(), ZoneID(zone),
(zone.is_bullish ? "LONG" : "SHORT"),
zone.entry, zone.sl, zone.tp, zone.zone_low, zone.zone_high, zone.fvg_size,
g_fuzzy.Score(FZ_STRUCTURE), g_fuzzy.Score(FZ_TREND),
g_fuzzy.Score(FZ_MOMENTUM), g_fuzzy.Score(FZ_VOLATILITY),
g_fuzzy.Score(FZ_AI),
g_fuzzy.Weight(FZ_STRUCTURE), g_fuzzy.Weight(FZ_TREND),
g_fuzzy.Weight(FZ_MOMENTUM), g_fuzzy.Weight(FZ_VOLATILITY),
g_fuzzy.Weight(FZ_AI),
fused, InpFuzzyThreshold, ScoreBand(fused), ThresholdSide(fused),
decision, diag_state, diag_reason, g_dso_run_id));
FileClose(h);
}
//+------------------------------------------------------------------+
//| AppendDSOAttempt — attempt record (EXECUTION_REJECTED captured). |
//+------------------------------------------------------------------+
void AppendDSOAttempt(const SOrderBlockZone &zone, const double fused, const ulong ticket,
const ulong attempt_id, const string reject_extra)
{
const string path = "exp_dso_001_attempts.csv";
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
PrintFormat("[CentaurQuant] GOVERNANCE EVENT: DSO attempt record write FAILED (err %d).", GetLastError());
return;
}
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "attempt_id,candidate_id,timestamp,decision_timestamp,zone_id,direction,decision,fused_score,threshold_reference,"
"score_band,risk_percent,order_type,requested_lot,requested_entry,initial_sl,initial_tp,"
"retcode,retcode_desc,reject_reason,order_ticket,deal_ticket,outcome_state,experiment_id\r\n");
const string reason = (StringLen(reject_extra) > 0 ? reject_extra : g_exec.LastRetcodeDescription());
const string ostate = ((ticket != 0) ? "COMPLETED" : "EXECUTION_REJECTED");
FileWriteString(h, StringFormat(
"%I64u,%I64u,%s,%s,%s,%s,PASS,%.2f,%.2f,%s,%.2f,%s,%.2f,%.5f,%.5f,%.5f,%u,%s,%s,%I64u,%I64u,%s,%s\r\n",
attempt_id, g_dso_last_candidate_id,
TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
TimeToString(g_last_decision_time, TIME_DATE | TIME_SECONDS),
ZoneID(zone),
(zone.is_bullish ? "LONG" : "SHORT"),
fused, InpFuzzyThreshold, ScoreBand(fused),
InpDSORiskPercent,
(zone.is_bullish ? "BUY" : "SELL"),
g_exec.LastRequestedLot(), zone.entry, zone.sl, zone.tp,
g_exec.LastRetcode(), g_exec.LastRetcodeDescription(),
reason, ticket, g_exec.LastDeal(), ostate, g_dso_run_id));
FileClose(h);
}
//+------------------------------------------------------------------+
//| AppendDSOCensoredPositions — open-at-end positions (D17). |
//+------------------------------------------------------------------+
void AppendDSOCensoredPositions()
{
const string path = "exp_dso_001_censored.csv";
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
return;
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "round_trip_id,attempt_id,zone_id,direction,entry_price,initial_sl,risk0,open_time,current_price,fused_score,outcome_state,censor_reason,experiment_id\r\n");
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0 || !PositionSelectByTicket(ticket))
continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol)
continue;
if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic)
continue;
SRoundTripCtx ctx;
const bool have_ctx = FindRTContext(ticket, ctx);
const double entry = PositionGetDouble(POSITION_PRICE_OPEN);
const double sl = PositionGetDouble(POSITION_SL);
const double risk0 = MathAbs(entry - sl);
const long type = PositionGetInteger(POSITION_TYPE);
double fused_score = 0.0;
const string comment = PositionGetString(POSITION_COMMENT);
const int pp = StringFind(comment, "CEN:");
if(pp >= 0)
fused_score = StringToDouble(StringSubstr(comment, pp + 4));
FileWriteString(h, StringFormat(
"%I64u,%I64u,%s,%s,%.5f,%.5f,%.5f,%s,%.5f,%.2f,CENSORED_OPEN_AT_END,open_at_window_end,%s\r\n",
ticket,
(have_ctx ? ctx.attempt_id : 0),
(have_ctx ? ctx.zone_id : "UNKNOWN"),
(type == POSITION_TYPE_BUY ? "LONG" : "SHORT"),
entry, sl, risk0,
TimeToString((datetime)PositionGetInteger(POSITION_TIME), TIME_DATE | TIME_SECONDS),
PositionGetDouble(POSITION_PRICE_CURRENT),
fused_score, g_dso_run_id));
}
FileClose(h);
}
//+------------------------------------------------------------------+
bool FindRTContext(const ulong ticket, SRoundTripCtx &ctx)
{
for(int i = 0; i < ArraySize(g_rt_ctx); i++)
if(g_rt_ctx[i].ticket == ticket)
{
ctx = g_rt_ctx[i];
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| AppendAttemptRecord — ORDER ATTEMPT evidence (fields H–P; §4). |
//+------------------------------------------------------------------+
void AppendAttemptRecord(const SOrderBlockZone &zone, const double fused, const ulong ticket,
const ulong attempt_id, const string reject_extra)
{
const string path = (InpDSOEnable ? "exp_dso_001_attempts.csv" : "exp003b_attempts.csv"); // EXP-DSO-001 (D6)
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
PrintFormat("[CentaurQuant] GOVERNANCE EVENT: attempt record write FAILED (err %d).", GetLastError());
return;
}
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "attempt_id,timestamp,decision_timestamp,zone_id,direction,decision,fused_score,threshold,order_type,"
"requested_lot,requested_entry,initial_sl,initial_tp,retcode,retcode_desc,reject_reason,order_ticket,deal_ticket\r\n");
const string reason = (StringLen(reject_extra) > 0 ? reject_extra : g_exec.LastRetcodeDescription());
FileWriteString(h, StringFormat(
"%I64u,%s,%s,%s,%s,PASS,%.2f,%.2f,%s,%.2f,%.5f,%.5f,%.5f,%u,%s,%s,%I64u,%I64u\r\n",
attempt_id,
TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
TimeToString(g_last_decision_time, TIME_DATE | TIME_SECONDS),
ZoneID(zone),
(zone.is_bullish ? "LONG" : "SHORT"),
fused, InpFuzzyThreshold,
(zone.is_bullish ? "BUY" : "SELL"),
g_exec.LastRequestedLot(), zone.entry, zone.sl, zone.tp,
g_exec.LastRetcode(), g_exec.LastRetcodeDescription(),
reason, ticket, g_exec.LastDeal()));
FileClose(h);
}
//+------------------------------------------------------------------+
//| AppendPositionOpen — POSITION OPEN evidence (fields Q–U; §4). |
//+------------------------------------------------------------------+
void AppendPositionOpen(const SOrderBlockZone &zone, const ulong ticket, const ulong attempt_id, const double ai_score)
{
if(!PositionSelectByTicket(ticket))
return;
const string path = (InpDSOEnable ? "exp_dso_001_positions.csv" : "exp003b_positions.csv"); // EXP-DSO-001 (D6)
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
PrintFormat("[CentaurQuant] GOVERNANCE EVENT: position-open record write FAILED (err %d).", GetLastError());
return;
}
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "round_trip_id,attempt_id,zone_id,direction,open_timestamp,fill_price,volume,initial_sl,initial_tp,ai_score,deal_ticket\r\n");
FileWriteString(h, StringFormat(
"%I64u,%I64u,%s,%s,%s,%.5f,%.2f,%.5f,%.5f,%.2f,%I64u\r\n",
ticket, attempt_id, ZoneID(zone),
(zone.is_bullish ? "LONG" : "SHORT"),
TimeToString((datetime)PositionGetInteger(POSITION_TIME), TIME_DATE | TIME_SECONDS),
PositionGetDouble(POSITION_PRICE_OPEN),
PositionGetDouble(POSITION_VOLUME),
zone.sl, zone.tp, ai_score,
g_exec.LastDeal()));
FileClose(h);
//--- referential context for exit/round-trip linkage (zone_id → attempt_id → round_trip_id) ---
const int n = ArraySize(g_rt_ctx);
ArrayResize(g_rt_ctx, n + 1);
g_rt_ctx[n].ticket = ticket;
g_rt_ctx[n].attempt_id = attempt_id;
g_rt_ctx[n].zone_id = ZoneID(zone);
}
//+------------------------------------------------------------------+
//| AppendExitRecord — POSITION CLOSE evidence (fields X–AA; §4). |
//+------------------------------------------------------------------+
void AppendExitRecord(const SClosedTrade &closed)
{
const string path = (InpDSOEnable ? "exp_dso_001_exits.csv" : "exp003b_exits.csv"); // EXP-DSO-001 (D6)
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
PrintFormat("[CentaurQuant] GOVERNANCE EVENT: exit record write FAILED (err %d).", GetLastError());
return;
}
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "round_trip_id,attempt_id,zone_id,direction,close_timestamp,exit_price,exit_volume,exit_deal,deal_reason,profit,initial_ai_score\r\n");
SRoundTripCtx ctx;
const bool have_ctx = FindRTContext(closed.ticket, ctx);
FileWriteString(h, StringFormat(
"%I64u,%I64u,%s,%s,%s,%.5f,%.2f,%I64u,%I64d,%.2f,%.2f\r\n",
closed.ticket,
(have_ctx ? ctx.attempt_id : 0),
(have_ctx ? ctx.zone_id : "UNKNOWN"),
(closed.position_type == POSITION_TYPE_BUY ? "LONG" : "SHORT"),
TimeToString(closed.close_time, TIME_DATE | TIME_SECONDS),
closed.exit_price, closed.exit_volume, closed.exit_deal,
closed.deal_reason, closed.profit, closed.initial_ai_score));
FileClose(h);
}
//+------------------------------------------------------------------+
//| AppendRoundTripRecord — ROUND-TRIP summary (fields AB–AG; §4). |
//| R per ratified EP §10 (price units); costs per §11 (R units). |
//| Monetary P&L secondary per ratified Y (pending reconciliation). |
//+------------------------------------------------------------------+
void AppendRoundTripRecord(const SClosedTrade &closed)
{
SRoundTripCtx ctx;
const bool have_ctx = FindRTContext(closed.ticket, ctx);
//--- R-multiple per ratified EP §10 (price units) ---
const double risk0 = MathAbs(closed.entry_price - closed.initial_sl);
const bool zero_risk = (risk0 <= 0.0);
const bool missing_sl = (closed.initial_sl <= 0.0);
double gross_R = 0.0;
if(!zero_risk)
gross_R = (closed.position_type == POSITION_TYPE_BUY)
? (closed.exit_price - closed.entry_price) / risk0
: (closed.entry_price - closed.exit_price) / risk0;
//--- costs in R units (deterministic via symbol tick spec; pending independent reconciliation per ratified Y) ---
double cost_R = 0.0;
string recon = "PENDING_TICK_VALUE_RECONCILIATION";
const double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
const double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
if(!zero_risk && tick_size > 0.0 && tick_value > 0.0 && closed.exit_volume > 0.0)
{
const double risk_per_lot = risk0 * (tick_value / tick_size);
if(risk_per_lot > 0.0)
{
const double cost_deposit = MathAbs(closed.swap) + MathAbs(closed.commission);
cost_R = cost_deposit / (risk_per_lot * closed.exit_volume);
}
}
const double net_R = gross_R - cost_R;
const string path = (InpDSOEnable ? "exp_dso_001_roundtrips.csv" : "exp003b_roundtrips.csv"); // EXP-DSO-001 (D6)
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
PrintFormat("[CentaurQuant] GOVERNANCE EVENT: round-trip record write FAILED (err %d).", GetLastError());
return;
}
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "round_trip_id,attempt_id,zone_id,direction,entry_price,initial_sl,risk0,exit_price,"
"gross_R,swap,commission,cost_R,net_R,monetary_profit,reconciliation_status,invalid_zero_risk,invalid_missing_sl" + (InpDSOEnable ? ",outcome_state\r\n" : "\r\n"));
FileWriteString(h, StringFormat(
"%I64u,%I64u,%s,%s,%.5f,%.5f,%.5f,%.5f,%.4f,%.2f,%.2f,%.4f,%.4f,%.2f,%s,%d,%d" + (InpDSOEnable ? ",%s\r\n" : "\r\n"),
closed.ticket,
(have_ctx ? ctx.attempt_id : 0),
(have_ctx ? ctx.zone_id : "UNKNOWN"),
(closed.position_type == POSITION_TYPE_BUY ? "LONG" : "SHORT"),
closed.entry_price, closed.initial_sl, risk0, closed.exit_price,
gross_R, closed.swap, closed.commission, cost_R, net_R,
closed.profit, recon, (zero_risk ? 1 : 0), (missing_sl ? 1 : 0),
(InpDSOEnable ? ((zero_risk || missing_sl) ? "INVALID" : "COMPLETED") : "")));
FileClose(h);
}
//+------------------------------------------------------------------+
//| WriteRunCompleteMarker — ordered-shutdown completion marker. |
//| Absence of this marker at run end indicates interruption (AD-17). |
//+------------------------------------------------------------------+
void WriteRunCompleteMarker(const int reason)
{
const string path = (InpDSOEnable ? "exp_dso_001_run_complete.marker" : "exp003b_run_complete.marker"); // EXP-DSO-001 (D6)
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
return;
FileSeek(h, 0, SEEK_END);
FileWriteString(h, StringFormat("COMPLETE,%s,deinit_reason=%d\r\n",
TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS), reason));
FileClose(h);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool HasOpenPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0 || !PositionSelectByTicket(ticket))
continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol)
continue;
if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic)
continue;
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| EnsureConnection — throttled reconnect when the router is offline.|
//+------------------------------------------------------------------+
void EnsureConnection()
{
if(MQLInfoInteger(MQL_TESTER))
return; // tester: no socket transport (SocketCreate=4014); file fallback aktif
if(g_sock == NULL || g_sock.IsConnected())
{
g_last_connect_attempt = 0;
return;
}
const ulong now = GetTickCount64();
if(g_last_connect_attempt == 0 || now - g_last_connect_attempt >= 5000)
{
g_last_connect_attempt = now;
g_sock.Connect(InpHost, InpPort, 3000); // tester guard sudah di atas (MQL_TESTER -> return)
}
}
//+------------------------------------------------------------------+
//| HarvestAndFlush — harvest telemetry and drain the pending queue |
//| into the transport. Bounded and O(queue) — safe per tick/timer. |
//+------------------------------------------------------------------+
void HarvestAndFlush()
{
if(g_harv == NULL || !g_harv.IsReady())
return;
g_harv.Harvest();
string payload = "";
while(g_harv.TakePending(payload))
{
if(StringLen(payload) > 0)
if(!g_sock.IsConnected())
AppendTelemetryFile(payload); // tester/transport down -> file fallback (harvest tetap terekam)
else
g_sock.Send(payload); // bounded; logs its own failures
}
}
//+------------------------------------------------------------------+
//| ScanAndTrade — OB+FVG proximity scan, SDP Setup_Detected request, |
//| bounded AI read, confidence parse, Anti-Veto execution. |
//+------------------------------------------------------------------+
void ScanAndTrade()
{
if(g_scan == NULL || !g_scan.IsReady() || g_enc == NULL || g_sock == NULL || g_exec == NULL)
return;
if(HasOpenPosition())
return; // single-position discipline
SOrderBlockZone zone;
ZeroMemory(zone);
if(!g_scan.Scan(zone, InpScanDepth, InpExpansionMin, 0.0, InpAtrPeriod, InpProximityMult, 0.25, 2.0, InpAllowMitigatedOB))
return; // no proximity-triggered OB+FVG (normal outcome)
//--- structural swings -> SDP historical_context array ---
g_ctx.Scan(InpScanDepth, InpFractalRadius, InpAtrMultiplier, InpAtrPeriod);
const string context_json = g_ctx.ContextJSON();
//--- multi-agent: skor kontributor (indikator & struktur pasar) ---
if(g_fuzzy == NULL)
return;
g_fuzzy.SetScore(FZ_STRUCTURE, StructuralConfidence(zone));
g_fuzzy.SetScore(FZ_TREND, AgentScoreTrend(zone));
g_fuzzy.SetScore(FZ_MOMENTUM, AgentScoreMomentum(zone));
g_fuzzy.SetScore(FZ_VOLATILITY, AgentScoreVolatility());
//--- agen AI (LLM bridge = satu-satunya jalur "The Brain"; anti-veto) ---
if(InpEnableLLM && g_llm != NULL && g_llm.IsReady())
{
if(g_llm.IsPending())
g_llm.ResetPending(); // S2: zona baru lebih fresh — batalkan request lama
const string setup_type = zone.is_bullish ? "OB_FVG_BULLISH" : "OB_FVG_BEARISH";
g_pending_zone = zone;
g_pending_context = context_json;
g_llm.SendRequest(g_norm.Symbol(), g_ctx.Timeframe(),
setup_type, zone.entry, zone.sl, zone.tp,
context_json);
return; // skor AI asinkron -> FuseAndExecute() di CheckPendingLLM()
}
//--- LLM off: AI netral 50, keputusan kolektif langsung ---
g_fuzzy.SetScore(FZ_AI, 50.0);
FuseAndExecute(zone, context_json);
}
//+------------------------------------------------------------------+
//| TryExecute — Anti-Veto execution + Trade_Opened telemetry. |
//+------------------------------------------------------------------+
ulong TryExecute(const SOrderBlockZone &zone, const double ai_score)
{
//--- Gate B evidence: attempt identity (Evidence Protocol §4/§5.1) ---
const ulong attempt_id = ++g_attempt_seq;
//--- market connection guard (internet/broker outage): never execute on stale data ---
if(!TerminalInfoInteger(TERMINAL_CONNECTED) || (TimeCurrent() - (datetime)SymbolInfoInteger(_Symbol, SYMBOL_TIME) > 120))
{
if(InpDSOEnable) AppendDSOAttempt(zone, ai_score, 0, attempt_id, "market_disconnected_or_stale"); else AppendAttemptRecord(zone, ai_score, 0, attempt_id, "market_disconnected_or_stale"); // Gate B evidence: every attempt is recorded
static datetime s_last_conn_warn = 0;
if(TimeCurrent() - s_last_conn_warn >= 30)
{
s_last_conn_warn = TimeCurrent();
Print("[CentaurQuant] WARNING: market disconnected/stale - execution suspended.");
}
return 0;
}
const ulong ticket = g_exec.ExecuteSetup(zone, InpRiskPercent, ai_score);
//--- Gate B evidence: every order attempt is recorded (zero-loss) ---
if(InpDSOEnable) AppendDSOAttempt(zone, ai_score, ticket, attempt_id, ""); else AppendAttemptRecord(zone, ai_score, ticket, attempt_id, "");
if(ticket != 0)
{
EmitTradeOpened(zone, ticket, ai_score, attempt_id);
//--- simpan skor agen utk feedback bobot dinamis saat trade ditutup ---
if(g_fuzzy != NULL)
{
for(int i = 0; i < FZ_AGENTS_TOTAL; i++)
g_last_trade_scores[i] = g_fuzzy.Score((ENUM_FUZZY_AGENT)i);
g_has_last_scores = true;
}
}
return ticket;
}
//+------------------------------------------------------------------+
//| AppendSetupCSV — EXP-003 evidence logger: mencatat SEMUA |
//| kandidat setup (PASS & SKIP) + skor agen + bobot + fused. |
//+------------------------------------------------------------------+
void AppendSetupCSV(const SOrderBlockZone &zone, const string decision, const string reason)
{
const string path = "exp003_setups.csv";
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
return;
const bool fresh = (FileSize(h) == 0);
FileSeek(h, 0, SEEK_END);
if(fresh)
FileWriteString(h, "timestamp,symbol,timeframe,direction,entry,sl,tp,ob_index,ob_time_age_bars,zone_low,zone_high,fvg_size,"
"depth,proximity_mult,expansion_min,allow_mitigated,"
"structure_score,trend_score,momentum_score,volatility_score,ai_score,"
"w_structure,w_trend,w_momentum,w_volatility,w_ai,"
"fused_score,threshold,decision,reason,trading\r\n");
const int age = (int)(TimeCurrent() - zone.ob_time);
FileWriteString(h, StringFormat(
"%s,%s,%s,%s,%.5f,%.5f,%.5f,%d,%d,%.5f,%.5f,%.5f,"
"%d,%.3f,%.3f,%d,"
"%.2f,%.2f,%.2f,%.2f,%.2f,"
"%.4f,%.4f,%.4f,%.4f,%.4f,"
"%.2f,%.2f,%s,%s,%d\r\n",
TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
_Symbol, TfName(),
(zone.is_bullish ? "LONG" : "SHORT"),
zone.entry, zone.sl, zone.tp, zone.ob_index, age,
zone.zone_low, zone.zone_high, zone.fvg_size,
InpScanDepth, InpProximityMult, InpExpansionMin,
(InpAllowMitigatedOB ? 1 : 0),
g_fuzzy.Score(FZ_STRUCTURE), g_fuzzy.Score(FZ_TREND),
g_fuzzy.Score(FZ_MOMENTUM), g_fuzzy.Score(FZ_VOLATILITY),
g_fuzzy.Score(FZ_AI),
g_fuzzy.Weight(FZ_STRUCTURE), g_fuzzy.Weight(FZ_TREND),
g_fuzzy.Weight(FZ_MOMENTUM), g_fuzzy.Weight(FZ_VOLATILITY),
g_fuzzy.Weight(FZ_AI),
g_fused_last, InpFuzzyThreshold, decision, reason,
(InpEnableTrading ? 1 : 0)));
FileClose(h);
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| FuseAndExecute — keputusan kolektif fuzzy: gabungkan semua skor |
//| agen -> defuzzifikasi -> gate threshold -> eksekusi (anti-veto). |
//+------------------------------------------------------------------+
void FuseAndExecute(const SOrderBlockZone &zone, const string context_json)
{
if(g_fuzzy == NULL || g_enc == NULL || g_sock == NULL || g_exec == NULL || g_ctx == NULL)
return;
const double fused = g_fuzzy.Fuse();
g_last_decision_time = TimeCurrent(); // Gate B evidence: decision reference timestamp
g_fused_last = fused;
g_last_ai_score = fused; // skor fusi dipakai manajemen posisi (bukan veto)
PrintFormat("[CentaurQuant] INFO: FUSION struct=%.1f(w%.2f) trend=%.1f(w%.2f) mom=%.1f(w%.2f) vol=%.1f(w%.2f) ai=%.1f(w%.2f) -> fused=%.1f (thr %.1f)",
g_fuzzy.Score(FZ_STRUCTURE), g_fuzzy.Weight(FZ_STRUCTURE),
g_fuzzy.Score(FZ_TREND), g_fuzzy.Weight(FZ_TREND),
g_fuzzy.Score(FZ_MOMENTUM), g_fuzzy.Weight(FZ_MOMENTUM),
g_fuzzy.Score(FZ_VOLATILITY), g_fuzzy.Weight(FZ_VOLATILITY),
g_fuzzy.Score(FZ_AI), g_fuzzy.Weight(FZ_AI),
fused, InpFuzzyThreshold);
string decision = "PASS";
string reason = "fused_above_threshold";
if(!InpDSOEnable && fused < InpFuzzyThreshold) // EXP-DSO-001 (D3): in diagnostic mode fused<55 is NOT an observation gate
{
decision = "SKIP";
reason = "fused_below_threshold";
PrintFormat("[CentaurQuant] INFO: fused %.1f < threshold %.1f - setup dilewati (keputusan kolektif agen).", fused, InpFuzzyThreshold);
}
if(InpDSOEnable)
{
//--- EXP-DSO-001 candidate census: full score range preserved (observational; D2/D3) ---
const string zid = ZoneID(zone);
if(DSOZoneUsed(zid))
{
AppendDSOCandidate(zone, fused, decision, reason, "NOT_ATTEMPTED", "same_zone_reentry_prohibited");
PrintFormat("[CentaurQuant] EXP-DSO-001: same-zone re-entry PROHIBITED (D14) - candidate NOT_ATTEMPTED (zone %s).", zid);
return;
}
DSOAddZone(zid);
AppendDSOCandidate(zone, fused, decision, reason, "ATTEMPTED", reason);
}
else
AppendSetupCSV(zone, decision, reason); // EXP-003: catat SEMUA kandidat (PASS & SKIP)
if(decision == "SKIP")
return;
if(!InpEnableTrading)
return; // EXP-003 diagnostic mode: log-only, tanpa eksekusi
const string setup_type = zone.is_bullish ? "OB_FVG_BULLISH" : "OB_FVG_BEARISH";
const string payload = g_enc.EncodeSetup(g_norm.Symbol(), g_ctx.Timeframe(),
setup_type, zone.entry, zone.sl, zone.tp,
fused, context_json);
if(StringLen(payload) == 0)
{
PrintFormat("[CentaurQuant] ERROR: failed to encode Setup_Detected payload.");
return;
}
if(!g_sock.Send(payload))
{
PrintFormat("[CentaurQuant] WARNING: Setup_Detected not sent (transport down).");
if(InpExecuteWithoutAi)
TryExecute(zone, fused); // anti-veto: tetap eksekusi dengan skor fusi
return;
}
TryExecute(zone, fused);
}
//+------------------------------------------------------------------+
//| AgentScoreTrend — agen trend: alignment EMA50/EMA200 vs arah zona.|
//+------------------------------------------------------------------+
double AgentScoreTrend(const SOrderBlockZone &zone)
{
double fast[1], slow[1];
if(CopyBuffer(g_ema_fast, 0, 0, 1, fast) < 1 || CopyBuffer(g_ema_slow, 0, 0, 1, slow) < 1)
return 50.0; // data kurang -> netral
const double eps = 1e-9;
const double spread = (fast[0] - slow[0]) / (MathAbs(slow[0]) + eps);
if(zone.is_bullish)
return (fast[0] > slow[0]) ? MathMin(90.0, 65.0 + 25.0 * MathMin(spread * 200.0, 1.0)) : 30.0;
return (fast[0] < slow[0]) ? MathMin(90.0, 65.0 + 25.0 * MathMin(-spread * 200.0, 1.0)) : 30.0;
}
//+------------------------------------------------------------------+
//| AgentScoreMomentum — agen momentum: RSI 14 selaras arah zona. |
//+------------------------------------------------------------------+
double AgentScoreMomentum(const SOrderBlockZone &zone)
{
double r[1];
if(CopyBuffer(g_rsi, 0, 0, 1, r) < 1)
return 50.0;
if(zone.is_bullish)
return (r[0] <= 50.0) ? 30.0 : ((r[0] >= 80.0) ? 40.0 : 55.0 + (r[0] - 50.0) * (30.0 / 25.0));
return (r[0] >= 50.0) ? 30.0 : ((r[0] <= 20.0) ? 40.0 : 55.0 + (50.0 - r[0]) * (30.0 / 25.0));
}
//+------------------------------------------------------------------+
//| AgentScoreVolatility — agen volatilitas: regime ATR14/ATR50. |
//| Volatilitas moderat = peluang eksekusi bagus; ekstrem = rendah. |
//+------------------------------------------------------------------+
double AgentScoreVolatility()
{
double a1[1], a2[1];
if(CopyBuffer(g_atr_short, 0, 0, 1, a1) < 1 || CopyBuffer(g_atr_long, 0, 0, 1, a2) < 1 || a2[0] <= 0.0)
return 50.0;
const double ratio = a1[0] / a2[0];
if(ratio >= 0.8 && ratio <= 1.3)
return 80.0 + 10.0 * (1.0 - MathAbs(ratio - 1.05) / 0.25);
if(ratio >= 0.5 && ratio < 0.8)
return 55.0 + (ratio - 0.5) * 66.6;
if(ratio > 1.3 && ratio <= 2.0)
return 75.0 - (ratio - 1.3) * 44.4;
return 40.0;
}
//+------------------------------------------------------------------+
//| AppendTelemetryFile — file-based telemetry fallback (no socket). |
//| Appends one SDP frame per line to MQL5\Files\telemetry.jsonl |
//| when the TCP transport is unavailable. Consumed by the Python |
//| ingest_telemetry.py script (Track B feedback loop persistence). |
//+------------------------------------------------------------------+
void AppendTelemetryFile(const string payload)
{
const string path = "telemetry.jsonl";
int h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
{
PrintFormat("[CentaurQuant] WARNING: telemetry file fallback open failed (err %d).", GetLastError());
return;
}
if(FileSize(h) > 10 * 1024 * 1024) // 10 MB safety cap — restart the file
{
FileClose(h);
FileDelete(path);
h = FileOpen(path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE)
return;
}
FileSeek(h, 0, SEEK_END);
FileWriteString(h, payload + "\n");
FileClose(h);
}
//+------------------------------------------------------------------+
//| EmitTradeOpened — read the live position and emit SDP telemetry. |
//+------------------------------------------------------------------+
void EmitTradeOpened(const SOrderBlockZone &zone, const ulong ticket, const double ai_score, const ulong attempt_id)
{
if(!PositionSelectByTicket(ticket))
return;
const ENUM_POSITION_TYPE side = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
const double volume = PositionGetDouble(POSITION_VOLUME);
const double price = PositionGetDouble(POSITION_PRICE_OPEN);
//--- Gate B evidence: position-open record (additive; Evidence Protocol §4 Q–U) ---
AppendPositionOpen(zone, ticket, attempt_id, ai_score);
const string payload = g_enc.EncodeTradeOpened(g_norm.Symbol(), g_ctx.Timeframe(),
ticket, side, volume, price,
zone.sl, zone.tp, ai_score);
if(StringLen(payload) > 0 && !g_sock.Send(payload))
AppendTelemetryFile(payload);
}
//+------------------------------------------------------------------+
//| StructuralConfidence — algorithmic confidence proxy for the |
//| setup: baseline 50 + up to 40 from imbalance strength (FVG/ATR). |
//+------------------------------------------------------------------+
double StructuralConfidence(const SOrderBlockZone &zone)
{
const double atr = g_norm.GetATRBuffer(InpAtrPeriod, 1.0);
if(atr <= 0.0)
return 50.0;
return MathMin(90.0, 50.0 + 40.0 * MathMin(zone.fvg_size / atr, 1.0));
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+