1185 lines
57 KiB
MQL5
1185 lines
57 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
|
|
//+------------------------------------------------------------------+
|