2026-08-12 19:54:12 +07:00
//+------------------------------------------------------------------+
//| 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"
2026-08-12 20:15:22 +07:00
# include "..\Include\Models\CLLMClient.mqh"
2026-08-13 10:44:30 +07:00
# include "..\Include\Models\CFuzzyFusion.mqh"
2026-08-12 19:54:12 +07:00
//--- 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)
2026-08-12 20:15:22 +07:00
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
2026-08-13 08:57:03 +07:00
sinput bool InpEnableLLM = true ; // use "The Brain" via MCP bridge (satu-satunya jalur)
2026-08-12 20:50:58 +07:00
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
2026-08-12 21:18:02 +07:00
sinput int InpLLMTimeoutMs = 15000 ; // LLM fallback timeout (ms)
2026-08-12 20:15:22 +07:00
2026-08-13 10:44:30 +07:00
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
2026-08-13 11:37:53 +07:00
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)
2026-08-16 21:27:41 +07:00
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)
2026-08-13 10:44:30 +07:00
2026-08-12 19:54:12 +07:00
//--- 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 ;
2026-08-12 20:15:22 +07:00
CLLMClient * g_llm = NULL ;
2026-08-13 10:44:30 +07:00
CFuzzyFusion * g_fuzzy = NULL ;
2026-08-12 21:18:02 +07:00
SOrderBlockZone g_pending_zone ; // zona setup menunggu skor LLM asinkron
2026-08-13 10:44:30 +07:00
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 ;
2026-08-12 19:54:12 +07:00
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
double g_last_ai_score = 0.0 ; // last AI confidence received (fallback for mgmt)
ulong g_last_connect_attempt = 0 ; // reconnect throttle
2026-08-16 21:27:41 +07:00
//--- 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)
2026-08-12 19:54:12 +07:00
2026-08-16 11:12:59 +07:00
//--- 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)
2026-08-12 19:54:12 +07:00
//--- helpers ------------------------------------------------------------
2026-08-12 20:50:58 +07:00
2026-08-12 21:18:02 +07:00
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
2026-08-12 19:54:12 +07:00
string TfName ( ) ;
bool HasOpenPosition ( ) ;
void EnsureConnection ( ) ;
void HarvestAndFlush ( ) ;
void ScanAndTrade ( ) ;
ulong TryExecute ( const SOrderBlockZone & zone , const double ai_score ) ;
2026-08-16 11:12:59 +07:00
void EmitTradeOpened ( const SOrderBlockZone & zone , const ulong ticket , const double ai_score , const ulong attempt_id ) ;
2026-08-12 19:54:12 +07:00
double StructuralConfidence ( const SOrderBlockZone & zone ) ;
2026-08-13 10:44:30 +07:00
double AgentScoreTrend ( const SOrderBlockZone & zone ) ;
double AgentScoreMomentum ( const SOrderBlockZone & zone ) ;
double AgentScoreVolatility ( ) ;
void FuseAndExecute ( const SOrderBlockZone & zone , const string context_json ) ;
2026-08-13 11:37:53 +07:00
void AppendSetupCSV ( const SOrderBlockZone & zone , const string decision , const string reason ) ;
2026-08-16 21:27:41 +07:00
//--- 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 ) ;
2026-08-16 11:12:59 +07:00
//--- 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 ) ;
2026-08-12 19:54:12 +07:00
//+------------------------------------------------------------------+
//| 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 ) ;
2026-08-12 20:15:22 +07:00
if ( InpEnableLLM )
2026-08-12 20:21:07 +07:00
{
2026-08-12 20:50:58 +07:00
g_llm = new CLLMClient ( InpLLMTimeoutMs ) ;
2026-08-12 20:21:07 +07:00
}
2026-08-13 10:44:30 +07:00
//--- 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. " ) ;
2026-08-12 19:54:12 +07:00
//--- runtime tuning from inputs ---
g_harv . SetHarvestInterval ( InpHarvestIntervalMs ) ;
g_harv . SetHeartbeatInterval ( InpHeartbeatIntervalMs ) ;
g_exec . SetDeviationPoints ( InpDeviationPoints ) ;
2026-08-16 21:27:41 +07:00
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 ) ;
}
2026-08-12 19:54:12 +07:00
//--- transport: non-fatal; auto-reconnect runs inside OnTimer ---
2026-08-13 10:25:04 +07:00
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. " ) ;
2026-08-13 11:37:53 +07:00
if ( ! InpEnableTrading )
FileDelete ( " exp003_setups.csv " ) ; // EXP-003: run diagnostic mulai clean slate
2026-08-12 19:54:12 +07:00
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 )
{
2026-08-16 11:12:59 +07:00
g_exec . FlushManagementEvents ( ) ; // Gate B evidence: ordered flush of buffered V/W records (zero-loss)
2026-08-12 19:54:12 +07:00
delete g_exec ;
g_exec = NULL ;
}
2026-08-12 20:15:22 +07:00
if ( g_llm ! = NULL )
{
delete g_llm ;
g_llm = NULL ;
}
2026-08-12 19:54:12 +07:00
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 ;
}
2026-08-13 10:44:30 +07:00
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 ;
}
2026-08-12 19:54:12 +07:00
PrintFormat ( " [CentaurQuant] INFO: deinitialized (reason %d). " , reason ) ;
2026-08-16 21:27:41 +07:00
if ( InpDSOEnable )
AppendDSOCensoredPositions ( ) ; // EXP-DSO-001 (D17): censor open-at-end positions
2026-08-16 11:12:59 +07:00
WriteRunCompleteMarker ( reason ) ; // Gate B evidence: ordered-shutdown completion marker
2026-08-12 19:54:12 +07:00
}
//+------------------------------------------------------------------+
//| 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 ) )
{
2026-08-13 10:44:30 +07:00
//--- feedback: adaptasi bobot dinamis agen dari outcome trade ---
2026-08-16 21:27:41 +07:00
if ( g_fuzzy ! = NULL & & g_has_last_scores & & ! InpDSOEnable ) // EXP-DSO-001 (D5): weights FROZEN during diagnostic run
2026-08-13 10:44:30 +07:00
{
g_fuzzy . UpdateWeights ( closed . profit > 0.0 , g_last_trade_scores ) ;
g_has_last_scores = false ;
if ( InpPersistWeights )
g_fuzzy . Save ( " centaur_weights.txt " ) ;
}
2026-08-16 11:12:59 +07:00
//--- Gate B evidence: exit + round-trip records (additive; Evidence Protocol §4 X–AG) ---
AppendExitRecord ( closed ) ;
AppendRoundTripRecord ( closed ) ;
2026-08-12 19:54:12 +07:00
const string payload = g_enc . EncodeTradeClosed ( g_norm . Symbol ( ) , closed . ticket ,
closed . profit , closed . r_multiple , closed . initial_ai_score ) ;
2026-08-13 08:57:03 +07:00
if ( StringLen ( payload ) > 0 & & ! g_sock . Send ( payload ) )
AppendTelemetryFile ( payload ) ;
2026-08-12 19:54:12 +07:00
}
}
//+------------------------------------------------------------------+
//| OnTimer — orchestration pipeline (bounded, non-blocking): |
//| reconnect -> harvest flush -> scan/AI/execute -> manage positions.|
//+------------------------------------------------------------------+
2026-08-12 20:21:07 +07:00
//+------------------------------------------------------------------+
//| 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.|
//+------------------------------------------------------------------+
2026-08-12 21:18:02 +07:00
//+------------------------------------------------------------------+
//| CheckPendingLLM — baca response LLM (file bridge) atau fallback. |
//+------------------------------------------------------------------+
2026-08-13 06:46:37 +07:00
//+------------------------------------------------------------------+
//| 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 ;
2026-08-13 11:37:53 +07:00
const double buf = g_norm . GetATRBuffer ( InpAtrPeriod , InpProximityMult ) ;
2026-08-13 06:46:37 +07:00
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. |
//+------------------------------------------------------------------+
2026-08-12 21:18:02 +07:00
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 ) )
{
2026-08-13 10:44:30 +07:00
g_last_ai_score = llm_score ; // S5: skor AI masuk sebagai satu agen
2026-08-13 06:46:37 +07:00
if ( ZoneStillValid ( g_pending_zone ) ) // S1: zona masih valid?
{
2026-08-13 10:44:30 +07:00
if ( g_fuzzy ! = NULL )
g_fuzzy . SetScore ( FZ_AI , llm_score ) ; // kontribusi agen AI
FuseAndExecute ( g_pending_zone , g_pending_context ) ; // keputusan kolektif fuzzy
2026-08-13 06:46:37 +07:00
}
else
{
PrintFormat ( " [CentaurQuant] WARNING: zona basi saat response tiba — dilewati. " ) ;
g_llm . ResetPending ( ) ;
}
2026-08-12 21:18:02 +07:00
PrintFormat ( " [CentaurQuant] INFO: LLM score %.1f — %s " , llm_score , llm_reason ) ;
}
else
if ( g_llm . DeadlinePassed ( ) )
{
2026-08-13 10:44:30 +07:00
PrintFormat ( " [CentaurQuant] WARNING: LLM response timeout; fallback AI=40 (anti-veto). " ) ;
2026-08-12 21:18:02 +07:00
g_llm . ResetPending ( ) ;
g_last_ai_score = 40.0 ;
2026-08-13 06:46:37 +07:00
if ( ZoneStillValid ( g_pending_zone ) ) // S1: revalidasi pada timeout juga
2026-08-13 10:44:30 +07:00
{
if ( g_fuzzy ! = NULL )
g_fuzzy . SetScore ( FZ_AI , 40.0 ) ;
FuseAndExecute ( g_pending_zone , g_pending_context ) ;
}
2026-08-13 06:46:37 +07:00
else
PrintFormat ( " [CentaurQuant] WARNING: zona basi pada timeout — dilewati. " ) ;
2026-08-12 21:18:02 +07:00
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
2026-08-12 19:54:12 +07:00
void OnTimer ( )
{
EnsureConnection ( ) ;
HarvestAndFlush ( ) ;
ScanAndTrade ( ) ;
2026-08-12 21:18:02 +07:00
CheckPendingLLM ( ) ;
2026-08-12 19:54:12 +07:00
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 ( ) ;
2026-08-12 21:18:02 +07:00
CheckPendingLLM ( ) ;
2026-08-12 19:54:12 +07:00
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). |
//+------------------------------------------------------------------+
2026-08-16 11:12:59 +07:00
//+------------------------------------------------------------------+
//| 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 ) ;
}
//+------------------------------------------------------------------+
2026-08-16 21:27:41 +07:00
//+------------------------------------------------------------------+
//| 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 ) ;
}
//+------------------------------------------------------------------+
2026-08-16 11:12:59 +07:00
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 )
{
2026-08-16 21:27:41 +07:00
const string path = ( InpDSOEnable ? " exp_dso_001_attempts.csv " : " exp003b_attempts.csv " ) ; // EXP-DSO-001 (D6)
2026-08-16 11:12:59 +07:00
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 ;
2026-08-16 21:27:41 +07:00
const string path = ( InpDSOEnable ? " exp_dso_001_positions.csv " : " exp003b_positions.csv " ) ; // EXP-DSO-001 (D6)
2026-08-16 11:12:59 +07:00
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 )
{
2026-08-16 21:27:41 +07:00
const string path = ( InpDSOEnable ? " exp_dso_001_exits.csv " : " exp003b_exits.csv " ) ; // EXP-DSO-001 (D6)
2026-08-16 11:12:59 +07:00
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 ;
2026-08-16 21:27:41 +07:00
const string path = ( InpDSOEnable ? " exp_dso_001_roundtrips.csv " : " exp003b_roundtrips.csv " ) ; // EXP-DSO-001 (D6)
2026-08-16 11:12:59 +07:00
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, "
2026-08-16 21:27:41 +07:00
" 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 " ) ) ;
2026-08-16 11:12:59 +07:00
FileWriteString ( h , StringFormat (
2026-08-16 21:27:41 +07:00
" %I64u,%I64u,%s,%s,%.5f,%.5f,%.5f,%.5f,%.4f,%.2f,%.2f,%.4f,%.4f,%.2f,%s,%d,%d " + ( InpDSOEnable ? " ,%s \r \n " : " \r \n " ) ,
2026-08-16 11:12:59 +07:00
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 ,
2026-08-16 21:27:41 +07:00
closed . profit , recon , ( zero_risk ? 1 : 0 ) , ( missing_sl ? 1 : 0 ) ,
( InpDSOEnable ? ( ( zero_risk | | missing_sl ) ? " INVALID " : " COMPLETED " ) : " " ) ) ) ;
2026-08-16 11:12:59 +07:00
FileClose ( h ) ;
}
//+------------------------------------------------------------------+
//| WriteRunCompleteMarker — ordered-shutdown completion marker. |
//| Absence of this marker at run end indicates interruption (AD-17). |
//+------------------------------------------------------------------+
void WriteRunCompleteMarker ( const int reason )
{
2026-08-16 21:27:41 +07:00
const string path = ( InpDSOEnable ? " exp_dso_001_run_complete.marker " : " exp003b_run_complete.marker " ) ; // EXP-DSO-001 (D6)
2026-08-16 11:12:59 +07:00
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 ) ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
2026-08-12 19:54:12 +07:00
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 ( )
{
2026-08-13 10:25:04 +07:00
if ( MQLInfoInteger ( MQL_TESTER ) )
return ; // tester: no socket transport (SocketCreate=4014); file fallback aktif
2026-08-12 19:54:12 +07:00
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 ;
2026-08-13 10:25:04 +07:00
g_sock . Connect ( InpHost , InpPort , 3000 ) ; // tester guard sudah di atas (MQL_TESTER -> return)
2026-08-12 19:54:12 +07:00
}
}
//+------------------------------------------------------------------+
//| 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 )
2026-08-13 10:25:04 +07:00
if ( ! g_sock . IsConnected ( ) )
AppendTelemetryFile ( payload ) ; // tester/transport down -> file fallback (harvest tetap terekam)
else
g_sock . Send ( payload ) ; // bounded; logs its own failures
2026-08-12 19:54:12 +07:00
}
}
//+------------------------------------------------------------------+
//| 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 ) ;
2026-08-13 11:37:53 +07:00
if ( ! g_scan . Scan ( zone , InpScanDepth , InpExpansionMin , 0.0 , InpAtrPeriod , InpProximityMult , 0.25 , 2.0 , InpAllowMitigatedOB ) )
2026-08-12 19:54:12 +07:00
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 ( ) ;
2026-08-13 10:44:30 +07:00
//--- multi-agent: skor kontributor (indikator & struktur pasar) ---
if ( g_fuzzy = = NULL )
2026-08-12 19:54:12 +07:00
return ;
2026-08-13 10:44:30 +07:00
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) ---
2026-08-12 20:15:22 +07:00
if ( InpEnableLLM & & g_llm ! = NULL & & g_llm . IsReady ( ) )
{
2026-08-13 06:46:37 +07:00
if ( g_llm . IsPending ( ) )
g_llm . ResetPending ( ) ; // S2: zona baru lebih fresh — batalkan request lama
2026-08-13 10:44:30 +07:00
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()
2026-08-12 19:54:12 +07:00
}
2026-08-13 10:44:30 +07:00
//--- LLM off: AI netral 50, keputusan kolektif langsung ---
g_fuzzy . SetScore ( FZ_AI , 50.0 ) ;
FuseAndExecute ( zone , context_json ) ;
2026-08-12 19:54:12 +07:00
}
//+------------------------------------------------------------------+
//| TryExecute — Anti-Veto execution + Trade_Opened telemetry. |
//+------------------------------------------------------------------+
ulong TryExecute ( const SOrderBlockZone & zone , const double ai_score )
{
2026-08-16 11:12:59 +07:00
//--- Gate B evidence: attempt identity (Evidence Protocol §4/§5.1) ---
const ulong attempt_id = + + g_attempt_seq ;
2026-08-13 10:25:04 +07:00
//--- market connection guard (internet/broker outage): never execute on stale data ---
2026-08-13 09:54:00 +07:00
if ( ! TerminalInfoInteger ( TERMINAL_CONNECTED ) | | ( TimeCurrent ( ) - ( datetime ) SymbolInfoInteger ( _Symbol , SYMBOL_TIME ) > 120 ) )
{
2026-08-16 21:27:41 +07:00
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
2026-08-13 09:54:00 +07:00
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 ;
}
2026-08-12 19:54:12 +07:00
const ulong ticket = g_exec . ExecuteSetup ( zone , InpRiskPercent , ai_score ) ;
2026-08-16 11:12:59 +07:00
//--- Gate B evidence: every order attempt is recorded (zero-loss) ---
2026-08-16 21:27:41 +07:00
if ( InpDSOEnable ) AppendDSOAttempt ( zone , ai_score , ticket , attempt_id , " " ) ; else AppendAttemptRecord ( zone , ai_score , ticket , attempt_id , " " ) ;
2026-08-12 19:54:12 +07:00
if ( ticket ! = 0 )
2026-08-13 10:44:30 +07:00
{
2026-08-16 11:12:59 +07:00
EmitTradeOpened ( zone , ticket , ai_score , attempt_id ) ;
2026-08-13 10:44:30 +07:00
//--- 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 ;
}
}
2026-08-12 19:54:12 +07:00
return ticket ;
}
//+------------------------------------------------------------------+
2026-08-13 11:37:53 +07:00
//| 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 ) ;
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
2026-08-13 10:44:30 +07:00
//| 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 ( ) ;
2026-08-16 11:12:59 +07:00
g_last_decision_time = TimeCurrent ( ) ; // Gate B evidence: decision reference timestamp
2026-08-13 10:44:30 +07:00
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 ) ;
2026-08-13 11:37:53 +07:00
string decision = " PASS " ;
string reason = " fused_above_threshold " ;
2026-08-16 21:27:41 +07:00
if ( ! InpDSOEnable & & fused < InpFuzzyThreshold ) // EXP-DSO-001 (D3): in diagnostic mode fused<55 is NOT an observation gate
2026-08-13 10:44:30 +07:00
{
2026-08-13 11:37:53 +07:00
decision = " SKIP " ;
reason = " fused_below_threshold " ;
2026-08-13 10:44:30 +07:00
PrintFormat ( " [CentaurQuant] INFO: fused %.1f < threshold %.1f - setup dilewati (keputusan kolektif agen). " , fused , InpFuzzyThreshold ) ;
}
2026-08-16 21:27:41 +07:00
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)
2026-08-13 11:37:53 +07:00
if ( decision = = " SKIP " )
return ;
if ( ! InpEnableTrading )
return ; // EXP-003 diagnostic mode: log-only, tanpa eksekusi
2026-08-13 10:44:30 +07:00
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 ;
}
//+------------------------------------------------------------------+
2026-08-13 08:57:03 +07:00
//| 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 ) ;
}
//+------------------------------------------------------------------+
2026-08-12 19:54:12 +07:00
//| EmitTradeOpened — read the live position and emit SDP telemetry. |
//+------------------------------------------------------------------+
2026-08-16 11:12:59 +07:00
void EmitTradeOpened ( const SOrderBlockZone & zone , const ulong ticket , const double ai_score , const ulong attempt_id )
2026-08-12 19:54:12 +07:00
{
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 ) ;
2026-08-16 11:12:59 +07:00
//--- Gate B evidence: position-open record (additive; Evidence Protocol §4 Q–U) ---
AppendPositionOpen ( zone , ticket , attempt_id , ai_score ) ;
2026-08-12 19:54:12 +07:00
const string payload = g_enc . EncodeTradeOpened ( g_norm . Symbol ( ) , g_ctx . Timeframe ( ) ,
ticket , side , volume , price ,
zone . sl , zone . tp , ai_score ) ;
2026-08-13 08:57:03 +07:00
if ( StringLen ( payload ) > 0 & & ! g_sock . Send ( payload ) )
AppendTelemetryFile ( payload ) ;
2026-08-12 19:54:12 +07:00
}
//+------------------------------------------------------------------+
//| 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 ) ) ;
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+