//+------------------------------------------------------------------+ //| 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)); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+