SniperGold_ML/MQL5/Experts/AlgoForge_Engine2_UnitTest.mq5

475 lines
18 KiB
MQL5

//+------------------------------------------------------------------+
//| AlgoForge_Engine2_UnitTest.mq5 |
//| Project : Algo Forge (refactor total) |
//| Sumber inspirasi: SniperGold SMC Pro+ (c) Waseem Shahrukh |
//| https://www.mql5.com/en/code/75466 |
//+------------------------------------------------------------------+
//| Unit test minimal Engine 2 (4 agen sinyal independen + agregator)|
//| Jalankan di Strategy Tester (XAUUSD, model "every tick"). |
//| Semua hasil dicetak di journal dengan prefix "AFTEST2". |
//| |
//| CEK YANG DIJALANKAN: |
//| T1 - validitas output tiap agen (buy/sell/bias/conf/dir) |
//| T2 - independensi: mengubah input agen X TIDAK mengubah output |
//| agen lain; determinisme (input sama -> output sama) |
//| T3 - closed-bar lock: semua bar yang dipakai sudah tertutup |
//| T4 - non-repaint: dalam bar yang sama, output identik |
//| T5 - validitas output agregator (dir/bias/score/levels) |
//| T6 - agregator dengan input sintetis (netral/buy/sell/campur) |
//+------------------------------------------------------------------+
#property copyright "Algo Forge - refactor SniperGold SMC Pro+ (Waseem Shahrukh, https://www.mql5.com/en/code/75466)"
#property version "1.00"
#property strict
#property description "Unit test Engine 2: 4 agen sinyal independen (N/C/E/P) + agregator. Jalankan di Strategy Tester; hasil di journal (prefix AFTEST2)."
#include <AlgoForge\AF_Engine2_Aggregator.mqh>
#include <AlgoForge\AF_Engine2_Setup.mqh>
input group "===== ENGINE 2 UNIT TEST ====="
// TF agen HARDCODED (AF_E2_TF_S1..S4 = H4/M30/M15/M5) - tanpa input manual
input ENUM_TIMEFRAMES InpTamper1 = PERIOD_H1; // Slot tamper N (uji independensi; beda dari base H4)
input ENUM_TIMEFRAMES InpTamper2 = PERIOD_M20; // Slot tamper C (beda dari base M30)
input ENUM_TIMEFRAMES InpTamper3 = PERIOD_M12; // Slot tamper E (beda dari base M15)
input ENUM_TIMEFRAMES InpTamper4 = PERIOD_M6; // Slot tamper P (beda dari base M3)
input int InpMaxBars = 600; // Kapasitas cache per TF
input bool InpVerbose = false; // Log detail
AFEngine1MTF g_e1;
AFEngine2Signals g_eng2;
AFAgentNarrative g_agN;
AFAgentContext g_agC;
AFAgentEntry g_agE;
AFAgentPriceAction g_agP;
AFAggregator g_agg;
int g_s[4]; // slot N,C,E,P
int g_t[4]; // slot tamper
long g_lastBar[4];
AFSignalOut g_out[4];
AFAggOut g_aggOut;
int g_pass=0, g_fail=0;
bool g_verbose=false;
bool g_indepDone=false;
bool g_repaintPending=false;
//+------------------------------------------------------------------+
string AgentName(int i)
{
switch(i)
{
case 0: return "Narrative";
case 1: return "Context";
case 2: return "Entry";
case 3: return "PriceAction";
}
return "?";
}
void Pass(const string what){ g_pass++; if(g_verbose) Print("AFTEST2 PASS: "+what); }
void Fail(const string what){ g_fail++; Print("AFTEST2 FAIL: "+what); }
bool SameSignal(const AFSignalOut &a,const AFSignalOut &b)
{
if(MathAbs(a.buy-b.buy)>1e-12) return false;
if(MathAbs(a.sell-b.sell)>1e-12) return false;
if(a.dir!=b.dir) return false;
if(a.reason!=b.reason) return false;
return true;
}
//+------------------------------------------------------------------+
//| T1 - validitas output agen |
//+------------------------------------------------------------------+
void CheckAgentOut(int i)
{
AFSignalOut o=g_out[i];
bool ok=true;
if(o.buy<0.0 || o.buy>1.0 || o.sell<0.0 || o.sell>1.0) ok=false;
if(o.bias<-1.0-1e-9 || o.bias>1.0+1e-9) ok=false;
if(o.confidence<0.0 || o.confidence>1.0) ok=false;
if(o.dir==1 && o.bias<=AF_E2_DIR_TOL) ok=false;
if(o.dir==-1 && o.bias>=-AF_E2_DIR_TOL) ok=false;
if(o.dir==0 && MathAbs(o.bias)>AF_E2_DIR_TOL) ok=false;
if(o.name!=AgentName(i)) ok=false;
if(ok)
Pass("T1 "+AgentName(i)+" output valid (dir="+IntegerToString(o.dir)
+" bias="+DoubleToString(o.bias,2)+" conf="+DoubleToString(o.confidence,2)+")");
else
Fail("T1 "+AgentName(i)+" output INVALID (buy="+DoubleToString(o.buy,3)
+" sell="+DoubleToString(o.sell,3)+" bias="+DoubleToString(o.bias,3)
+" dir="+IntegerToString(o.dir)+")");
}
//+------------------------------------------------------------------+
//| T5 - validitas output agregator |
//+------------------------------------------------------------------+
void CheckAggOut()
{
AFAggOut o=g_aggOut;
bool ok=true;
if(o.buy<0.0 || o.buy>1.0 || o.sell<0.0 || o.sell>1.0) ok=false;
if(MathAbs(o.bias-(o.buy-o.sell))>1e-9) ok=false;
if(MathAbs(o.score-MathAbs(o.bias))>1e-9) ok=false;
if(o.confidence<0.0 || o.confidence>1.0) ok=false;
if(o.dir==1 && (o.bias<AF_AGG_BUY_TH || o.buy<AF_AGG_MIN_SUP)) ok=false;
if(o.dir==-1 && (o.bias>-AF_AGG_BUY_TH || o.sell<AF_AGG_MIN_SUP)) ok=false;
if(o.dir!=0)
{
if(!o.hasLevels || o.entry<=0.0) ok=false;
if(o.dir==1 && (o.sl>=o.entry || o.tp<=o.entry)) ok=false;
if(o.dir==-1 && (o.sl<=o.entry || o.tp>=o.entry)) ok=false;
}
if(o.reason=="") ok=false;
if(ok)
Pass("T5 agregator valid (dir="+IntegerToString(o.dir)
+" bias="+DoubleToString(o.bias,2)+" score="+DoubleToString(o.score,2)
+" conf="+DoubleToString(o.confidence,2)+")");
else
Fail("T5 agregator INVALID (dir="+IntegerToString(o.dir)
+" buy="+DoubleToString(o.buy,3)+" sell="+DoubleToString(o.sell,3)+")");
}
//+------------------------------------------------------------------+
//| T2 - independensi & determinisme |
//+------------------------------------------------------------------+
bool CheckIndependence()
{
AFSignalOut base[4];
g_agN.Compute(g_e1,g_s[0],base[0]);
g_agC.Compute(g_e1,g_s[1],base[1]);
g_agE.Compute(g_e1,g_s[2],base[2]);
g_agP.Compute(g_e1,g_s[3],base[3]);
bool tamperReady=true;
for(int i=0;i<4;i++)
if(g_t[i]<0 || !g_e1.IsReady(g_t[i])) tamperReady=false;
if(!tamperReady)
{
Print("AFTEST2 T2 SKIP: slot tamper belum siap (akan dicoba di bar berikutnya)");
return false;
}
bool indepOK=true;
for(int t=0;t<4;t++)
{
AFSignalOut alt[4];
g_agN.Compute(g_e1,(t==0)? g_t[0] : g_s[0],alt[0]);
g_agC.Compute(g_e1,(t==1)? g_t[1] : g_s[1],alt[1]);
g_agE.Compute(g_e1,(t==2)? g_t[2] : g_s[2],alt[2]);
g_agP.Compute(g_e1,(t==3)? g_t[3] : g_s[3],alt[3]);
for(int i=0;i<4;i++)
{
if(i==t) continue; // agen yang diubah BOLEH berbeda
if(!SameSignal(base[i],alt[i]))
{
Fail("T2 independensi rusak: ganti input "+AgentName(t)+" mengubah "+AgentName(i));
indepOK=false;
}
}
}
AFSignalOut chk[4];
g_agN.Compute(g_e1,g_s[0],chk[0]);
g_agC.Compute(g_e1,g_s[1],chk[1]);
g_agE.Compute(g_e1,g_s[2],chk[2]);
g_agP.Compute(g_e1,g_s[3],chk[3]);
bool det=true;
for(int i=0;i<4;i++) if(!SameSignal(base[i],chk[i])) det=false;
if(det && indepOK) Pass("T2 independensi & determinisme OK");
if(!det) Fail("T2 determinisme rusak (input sama -> output beda)"); return true;
}
//+------------------------------------------------------------------+
//| T4 - non-repaint (output identik dalam bar yang sama) |
//+------------------------------------------------------------------+
void CheckNonRepaint()
{
AFSignalOut chk[4];
g_agN.Compute(g_e1,g_s[0],chk[0]);
g_agC.Compute(g_e1,g_s[1],chk[1]);
g_agE.Compute(g_e1,g_s[2],chk[2]);
g_agP.Compute(g_e1,g_s[3],chk[3]);
bool ok=true;
for(int i=0;i<4;i++) if(!SameSignal(g_out[i],chk[i])) ok=false;
if(ok) Pass("T4 non-repaint: output identik dalam bar yang sama");
else Fail("T4 non-repaint dilanggar");
}
//+------------------------------------------------------------------+
//| T6 - agregator dengan input sintetis (logika murni) |
//+------------------------------------------------------------------+
void SyntheticAggTests()
{
AFSignalOut z, b, s;
AF_SignalInit(z,"Z","?");
AF_SignalInit(b,"B","?");
b.buy=0.9; b.sell=0.1; b.bias=0.8; b.confidence=0.9; b.dir=1;
AF_SignalInit(s,"S","?");
s.buy=0.1; s.sell=0.9; s.bias=-0.8; s.confidence=0.9; s.dir=-1;
AFAggOut o;
g_agg.Compute(g_e1,0,z,z,z,z,o);
if(o.dir==0) Pass("T6 agregator: semua netral -> WAIT");
else Fail("T6 agregator: semua netral harus WAIT");
g_agg.Compute(g_e1,0,b,b,b,b,o);
if(o.dir==1 && o.buy>0.5) Pass("T6 agregator: semua buy kuat -> BUY");
else Fail("T6 agregator: semua buy kuat harus BUY");
g_agg.Compute(g_e1,0,s,s,s,s,o);
if(o.dir==-1 && o.sell>0.5) Pass("T6 agregator: semua sell kuat -> SELL");
else Fail("T6 agregator: semua sell kuat harus SELL");
g_agg.Compute(g_e1,0,b,z,z,z,o);
if(o.dir==1) Pass("T6 agregator: N buy kuat saja -> BUY (bobot N)");
else Fail("T6 agregator: N buy kuat harus BUY");
g_agg.Compute(g_e1,0,b,s,z,z,o);
if(o.dir==0) Pass("T6 agregator: N buy vs C sell seimbang -> WAIT");
else Fail("T6 agregator: N buy vs C sell seimbang harus WAIT");
}
//+------------------------------------------------------------------+
//| T7 - F3 Candidate Setup layer (sintetis, semantic inputs) |
//| Port test dari spec_tests_candidate_setup_runtime.py oracle: |
//| T7a : rantai lengkap -> CANDIDATE_SETUP (F3-T01) |
//| T7b : H4 conflict -> BLOCKED (F3-T08) |
//| T7c : M3 contrary -> setup valid, M3_CONFIRMED=false (F3-T11) |
//| T7d : entry konsumsi -> CONSUMED, satu entry (F3-T18) |
//| T7e : score rendah tetap ada setup (F3-T25) |
//+------------------------------------------------------------------+
void SyntheticSetupTests()
{
AFSetupEngine eng;
AFSetupInputs inp;
AFSetupOutcome out;
// T7a: rantai lengkap bullish (mirror F3-T01)
eng.Reset();
inp.h4=1; inp.m30=1;
inp.sweep.onset=2; inp.sweep.dir=1;
inp.choch.onset=5; inp.choch.dir=1;
inp.zone.type=AF_SETUP_ZONE_OB; inp.zone.formation=4;
inp.zone.dir=1; inp.zone.mit=AF_ZONE_UNMITIGATED; inp.zone.invalidated=false;
inp.m15=1; inp.m3=0; inp.entry=false; inp.score=0.5;
// bar 6: sweep fresh (age 4), choch fresh (age 1), chain completes
eng.Step(6,inp,out);
if(out.setup_exists && out.setup_id==1 && out.state==AF_SETUP_CANDIDATE && out.dir==1)
Pass("T7a setup layer: rantai lengkap -> CANDIDATE_SETUP id=1");
else
Fail("T7a setup layer: rantai lengkap gagal (state="+EnumToString(out.state)+")");
// T7b: H4 conflict -> BLOCKED (mirror F3-T08)
eng.Reset();
inp.h4=-1; inp.m30=1;
inp.sweep.onset=2; inp.sweep.dir=1;
inp.choch.onset=3; inp.choch.dir=1;
inp.zone.type=AF_SETUP_ZONE_OB; inp.zone.formation=3;
inp.zone.dir=1; inp.zone.mit=AF_ZONE_UNMITIGATED; inp.zone.invalidated=false;
inp.m15=1; inp.m3=0; inp.entry=false; inp.score=0.5;
eng.Step(6,inp,out);
if(out.blocked && !out.setup_exists)
Pass("T7b setup layer: H4/M30 conflict -> BLOCKED");
else
Fail("T7b setup layer: H4/M30 conflict harus BLOCKED");
// T7c: M3 contrary -> setup valid, M3_CONFIRMED=false (mirror F3-T11)
eng.Reset();
inp.h4=1; inp.m30=1;
inp.sweep.onset=2; inp.sweep.dir=1;
inp.choch.onset=5; inp.choch.dir=1;
inp.zone.type=AF_SETUP_ZONE_OB; inp.zone.formation=4;
inp.zone.dir=1; inp.zone.mit=AF_ZONE_UNMITIGATED; inp.zone.invalidated=false;
inp.m15=1; inp.m3=-1; inp.entry=false; inp.score=0.5;
eng.Step(6,inp,out);
eng.Step(7,inp,out);
if(out.setup_exists && out.setup_id==1 && !out.m3_confirmed &&
out.state==AF_SETUP_CANDIDATE)
Pass("T7c setup layer: M3 contrary -> setup valid, M3_CONFIRMED=false");
else
Fail("T7c setup layer: M3 contrary harus tetap valid tanpa konfirmasi");
// T7d: entry konsumsi -> CONSUMED, satu entry per setup (mirror F3-T18)
eng.Reset();
inp.h4=1; inp.m30=1;
inp.sweep.onset=2; inp.sweep.dir=1;
inp.choch.onset=5; inp.choch.dir=1;
inp.zone.type=AF_SETUP_ZONE_OB; inp.zone.formation=4;
inp.zone.dir=1; inp.zone.mit=AF_ZONE_UNMITIGATED; inp.zone.invalidated=false;
inp.m15=1; inp.m3=0; inp.score=0.5;
inp.entry=false;
eng.Step(5,inp,out); // forming
inp.entry=true;
eng.Step(6,inp,out); // entry pada bar yang sama -> CONSUMED
inp.entry=false;
eng.Step(7,inp,out); // setup sudah terminal -> slot bebas
if(eng.EntryBindings()==1 && out.state==AF_SETUP_NONE)
Pass("T7d setup layer: satu entry per setup (CONSUMED)");
else
Fail("T7d setup layer: aturan satu-entry gagal (bindings="
+IntegerToString(eng.EntryBindings())+")");
// T7e: score rendah tetap ada setup (mirror F3-T25)
eng.Reset();
inp.h4=1; inp.m30=1;
inp.sweep.onset=2; inp.sweep.dir=1;
inp.choch.onset=5; inp.choch.dir=1;
inp.zone.type=AF_SETUP_ZONE_OB; inp.zone.formation=4;
inp.zone.dir=1; inp.zone.mit=AF_ZONE_UNMITIGATED; inp.zone.invalidated=false;
inp.m15=1; inp.m3=0; inp.entry=false; inp.score=0.05;
eng.Step(6,inp,out);
if(out.setup_exists && out.setup_id==1 && out.score==0.05)
Pass("T7e setup layer: setup ada walau score rendah (score != setup)");
else
Fail("T7e setup layer: score rendah harus tetap punya setup");
}
//+------------------------------------------------------------------+
//| Log verbose |
//+------------------------------------------------------------------+
void PrintAllVerbose()
{
for(int i=0;i<4;i++)
PrintFormat("AFTEST2 [%s] TF=%s dir=%d buy=%.3f sell=%.3f conf=%.3f | %s",
g_out[i].name,g_out[i].tfName,g_out[i].dir,g_out[i].buy,
g_out[i].sell,g_out[i].confidence,g_out[i].reason);
PrintFormat("AFTEST2 [AGG] %s",g_aggOut.reason);
if(g_aggOut.hasLevels)
PrintFormat("AFTEST2 [AGG] entry=%.2f sl=%.2f tp=%.2f",
g_aggOut.entry,g_aggOut.sl,g_aggOut.tp);
}
//+------------------------------------------------------------------+
//| Semua cek pada bar tertutup baru (atau saat semua slot siap) |
//+------------------------------------------------------------------+
bool RunAllChecks()
{
for(int i=0;i<4;i++)
{
if(g_s[i]<0 || !g_e1.IsReady(g_s[i]))
{
PrintFormat("AFTEST2 SKIP: slot %s belum siap",AgentName(i));
return false;
}
if(g_e1.Count(g_s[i])<AF_E2_MIN_BARS)
{
PrintFormat("AFTEST2 SKIP: data %s kurang (%d bar < %d)",
AgentName(i),g_e1.Count(g_s[i]),AF_E2_MIN_BARS);
return false;
}
}
// T3 - closed-bar lock: bar terbaru tiap slot sudah tertutup
datetime now=TimeCurrent();
for(int i=0;i<4;i++)
{
datetime t0=g_e1.Time(g_s[i],0);
if(!g_e1.IsBarClosed(t0,g_e1.TfOf(g_s[i]),now))
Fail("T3 "+AgentName(i)+": bar terbaru belum tertutup ("+TimeToString(t0)+")");
}
// compute semua agen + agregator (via facade)
g_eng2.Compute(g_e1,g_s[0],g_s[1],g_s[2],g_s[3],
g_out[0],g_out[1],g_out[2],g_out[3],g_aggOut);
for(int i=0;i<4;i++) CheckAgentOut(i);
CheckAggOut();
if(!g_indepDone)
{
g_indepDone=CheckIndependence();
}
if(g_verbose) PrintAllVerbose();
return true;
}
//+------------------------------------------------------------------+
//| Init |
//+------------------------------------------------------------------+
int OnInit()
{
Print("AFTEST2 ============================================================");
Print("AFTEST2 Algo Forge Engine2 UnitTest v"+AF_PROJECT_VERSION);
Print("AFTEST2 Atribusi: "+AF_SOURCE_TITLE+" (c) "+AF_SOURCE_AUTHOR+" - "+AF_SOURCE_URL);
g_verbose=InpVerbose;
ENUM_TIMEFRAMES tfs[4];
tfs[0]=AF_E2_TF_S1; // H4 (N)
tfs[1]=AF_E2_TF_S2; // M30 (C)
tfs[2]=AF_E2_TF_S3; // M15 (E)
tfs[3]=AF_E2_TF_S4; // M3 (P)
ENUM_TIMEFRAMES tft[4];
tft[0]=InpTamper1;
tft[1]=InpTamper2;
tft[2]=InpTamper3;
tft[3]=InpTamper4;
for(int i=0;i<4;i++)
{
g_s[i]=g_e1.Register(tfs[i],InpMaxBars);
g_t[i]=g_e1.Register(tft[i],InpMaxBars);
g_lastBar[i]=-1;
if(g_s[i]<0) Fail("Register slot "+AgentName(i)+" gagal");
}
g_e1.Refresh();
SyntheticAggTests(); // T6 - murni logika, jalan tanpa data
SyntheticSetupTests(); // T7 - F3 setup layer, input semantik sintetis
PrintFormat("AFTEST2 slot: N=%d C=%d E=%d P=%d | tamper N=%d C=%d E=%d P=%d",
g_s[0],g_s[1],g_s[2],g_s[3],g_t[0],g_t[1],g_t[2],g_t[3]);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Tick |
//+------------------------------------------------------------------+
void OnTick()
{
g_e1.Refresh();
bool barsChanged=false;
for(int i=0;i<4;i++)
{
if(g_s[i]<0) continue;
if(!g_e1.IsReady(g_s[i])) continue;
long lb=g_e1.LastBars(g_s[i]);
if(lb!=g_lastBar[i])
{
barsChanged=true;
g_lastBar[i]=lb;
}
}
if(barsChanged)
{
if(RunAllChecks()) g_repaintPending=true;
else g_repaintPending=false;
}
else if(g_repaintPending)
{
CheckNonRepaint(); // bar sama -> output harus identik
g_repaintPending=false;
}
}
//+------------------------------------------------------------------+
//| Tester summary |
//+------------------------------------------------------------------+
double OnTester()
{
Print("AFTEST2 ============================================================");
PrintFormat("AFTEST2 RESULT: PASS=%d FAIL=%d",g_pass,g_fail);
Print("AFTEST2 RESULT: "+(g_fail==0? "SEMUA CEK LULUS" : "ADA CEK GAGAL"));
for(int i=0;i<4;i++)
if(g_s[i]>=0 && g_e1.IsReady(g_s[i]))
PrintFormat("AFTEST2 final [%s]: TF=%s count=%d historyCalls=%d",
AgentName(i),AF_E2_TfName(g_e1.TfOf(g_s[i])),
g_e1.Count(g_s[i]),g_e1.HistoryCalls(g_s[i]));
return (g_fail==0)? 1000.0 : 0.0;
}