//+------------------------------------------------------------------+ //| 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 #include 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.sell=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]) 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; }