SniperGold_ML/MQL5/Experts/AlgoForge_Engine3_UnitTest.mq5

373 lines
14 KiB
MQL5

//+------------------------------------------------------------------+
//| AlgoForge_Engine3_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 3 (display). Jalankan di Strategy |
//| Tester (XAUUSD, model "every tick", Visual=1 agar objek chart |
//| bisa dibuat). Semua hasil dicetak di journal (prefix AFTEST3). |
//| |
//| CEK YANG DIJALANKAN: |
//| T1 - data display valid (struktur/zona/level/bias) |
//| T2 - non-repaint: dalam bar yang sama data identik |
//| T3 - closed-bar lock: semua bar yang dipakai sudah tertutup |
//| T4 - render: AFR_DrawAll menciptakan objek chart (prefix) |
//| T5 - determinisme render: render ulang bar sama -> jumlah sama |
//+------------------------------------------------------------------+
#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 3: display struktur/zona/sinyal/dashboard (hanya baca output Engine 1 & 2). Jalankan di Strategy Tester; hasil di journal (prefix AFTEST3)."
#include <AlgoForge\AF_Engine3_Render.mqh>
input group "===== ENGINE 3 UNIT TEST ====="
// TF agen HARDCODED (AF_E2_TF_S1..S4 = H4/M30/M15/M3) - tanpa input manual
input ENUM_TIMEFRAMES InpDispTF = PERIOD_CURRENT; // TF display (CURRENT = chart)
input int InpMaxBars = 600; // Kapasitas cache per slot
input bool InpVerbose = false; // Log detail
AFEngine1MTF g_e1;
AFEngine2Signals g_eng2;
AFDisplayData g_dd, g_ddChk;
AFSignalOut g_n, g_c, g_e, g_p;
AFAggOut g_agg;
AFRenderCfg g_cfg;
int g_sN, g_sC, g_sE, g_sP, g_sDisp, g_sD1, g_sW1;
long g_lastBar[7];
bool g_repaintPending=false;
int g_pass=0, g_fail=0;
bool g_verbose=false;
bool g_objSupported=false;
bool g_objChecked=false;
int g_lastObjCount=-1;
//+------------------------------------------------------------------+
void Pass(const string what){ g_pass++; if(g_verbose) Print("AFTEST3 PASS: "+what); }
void Fail(const string what){ g_fail++; Print("AFTEST3 FAIL: "+what); }
int SlotIdx(int i)
{
switch(i)
{
case 0: return g_sN;
case 1: return g_sC;
case 2: return g_sE;
case 3: return g_sP;
case 4: return g_sDisp;
case 5: return g_sD1;
case 6: return g_sW1;
}
return -1;
}
string SlotName(int i)
{
switch(i)
{
case 0: return "N";
case 1: return "C";
case 2: return "E";
case 3: return "P";
case 4: return "DISP";
case 5: return "D1";
case 6: return "W1";
}
return "?";
}
int CountObjectsWithPrefix(string pfx)
{
int n=0;
int total=ObjectsTotal(0,0,-1);
for(int i=0;i<total;i++)
{
string nm=ObjectName(0,i,0,-1);
if(StringFind(nm,pfx)==0) n++;
}
return n;
}
//+------------------------------------------------------------------+
//| T1 - validitas data display |
//+------------------------------------------------------------------+
void CheckDisplayData()
{
bool ok=true;
AFDisplayData d=g_dd;
if(d.structure!="Bullish" && d.structure!="Bearish" && d.structure!="Range")
{ ok=false; Fail("T1 struktur invalid: '"+d.structure+"'"); }
if(d.eqPos!="Premium" && d.eqPos!="Discount" && d.eqPos!="Equilibrium")
{ ok=false; Fail("T1 eqPos invalid: '"+d.eqPos+"'"); }
if(d.swHigh<d.swLow-1e-12)
{ ok=false; Fail("T1 range invalid: swHigh<swLow"); }
if(d.swHigh>d.swLow && (d.rangePos<0.0 || d.rangePos>1.0))
{ ok=false; Fail("T1 rangePos di luar [0,1]"); }
if(d.nBias=="" || d.cBias=="" || d.eBias=="" || d.pBias=="")
{ ok=false; Fail("T1 bias agen kosong"); }
int nP=ArraySize(d.pivots);
for(int i=0;i<nP;i++)
{
if(d.pivots[i].tag!="HH" && d.pivots[i].tag!="HL" &&
d.pivots[i].tag!="LH" && d.pivots[i].tag!="LL")
{ ok=false; Fail("T1 tag pivot invalid: "+d.pivots[i].tag); break; }
}
int nL=ArraySize(d.structLines);
for(int i=0;i<nL;i++)
{
if(d.structLines[i].price<=0.0 ||
d.structLines[i].t1>d.structLines[i].t2 ||
(d.structLines[i].lbl!="BOS" && d.structLines[i].lbl!="CHoCH"))
{ ok=false; Fail("T1 garis struktur invalid ("+d.structLines[i].lbl+")"); break; }
}
int nO=ArraySize(d.ob);
for(int i=0;i<nO;i++)
{
if(d.ob[i].top<d.ob[i].bot || d.ob[i].bot<=0.0 || d.ob[i].tag!="OB")
{ ok=false; Fail("T1 zona OB invalid"); break; }
}
int nF=ArraySize(d.fvg);
for(int i=0;i<nF;i++)
{
if(d.fvg[i].top<d.fvg[i].bot || d.fvg[i].bot<=0.0 || d.fvg[i].tag!="FVG")
{ ok=false; Fail("T1 zona FVG invalid"); break; }
}
// level MTF: wajib >0 bila slot D1/W1 punya >=2 bar
int s1=SlotIdx(5);
int s2=SlotIdx(6);
if(g_e1.IsReady(s1) && g_e1.Count(s1)>=2)
{
if(d.pdh<=0.0 || d.pdl<=0.0)
{ ok=false; Fail("T1 PDH/PDL = 0 padahal D1 siap (count="+IntegerToString(g_e1.Count(s1))+")"); }
}
if(g_e1.IsReady(s2) && g_e1.Count(s2)>=2)
{
if(d.pwh<=0.0 || d.pwl<=0.0)
{ ok=false; Fail("T1 PWH/PWL = 0 padahal W1 siap (count="+IntegerToString(g_e1.Count(s2))+")"); }
}
// agregator sanity (dari Engine 2)
AFAggOut a=g_agg;
if(a.buy<0.0 || a.buy>1.0 || a.sell<0.0 || a.sell>1.0) ok=false;
if(MathAbs(a.bias-(a.buy-a.sell))>1e-9) ok=false;
if(ok)
Pass("T1 data display valid (structure="+d.structure+" eqPos="+d.eqPos
+" HH/HL/LH/LL="+IntegerToString(d.HH)+"/"+IntegerToString(d.HL)+"/"
+IntegerToString(d.LH)+"/"+IntegerToString(d.LL)
+" piv="+IntegerToString(nP)+" lines="+IntegerToString(nL)
+" ob="+IntegerToString(nO)+" fvg="+IntegerToString(nF)+")");
}
//+------------------------------------------------------------------+
//| T3 - closed-bar lock |
//+------------------------------------------------------------------+
void CheckClosedBar()
{
datetime now=TimeCurrent();
bool ok=true;
for(int i=0;i<5;i++) // N,C,E,P,DISP
{
int s=SlotIdx(i);
if(s<0 || !g_e1.IsReady(s)) continue;
datetime t0=g_e1.Time(s,0);
if(!g_e1.IsBarClosed(t0,g_e1.TfOf(s),now))
{ ok=false; Fail("T3 "+SlotName(i)+": bar terbaru belum tertutup ("+TimeToString(t0)+")"); }
}
if(ok) Pass("T3 closed-bar lock: semua slot siap memakai bar tertutup");
}
//+------------------------------------------------------------------+
//| T2 - non-repaint (data identik dalam bar sama) |
//+------------------------------------------------------------------+
void CheckNonRepaint()
{
AFDisplayData chk;
chk.Reset();
AF_BuildDisplayData(g_e1,g_sN,g_sC,g_sE,g_sP,g_sDisp,g_sD1,g_sW1,
g_cfg.obCount,g_cfg.fvgCount,
g_n,g_c,g_e,g_p,chk);
if(AF_SameDisplayData(g_dd,chk))
Pass("T2 non-repaint: data display identik dalam bar yang sama");
else
Fail("T2 non-repaint dilanggar (data display berubah dalam bar sama)");
}
//+------------------------------------------------------------------+
//| T4/T5 - render (buat objek, determinisme) |
//+------------------------------------------------------------------+
void CheckRender()
{
// cek dukungan objek chart (sekali)
if(!g_objChecked)
{
g_objChecked=true;
string nm=g_cfg.prefix+"_PROBE";
g_objSupported=ObjectCreate(0,nm,OBJ_TREND,0,TimeCurrent(),0,TimeCurrent(),0);
if(g_objSupported) ObjectDelete(0,nm);
if(!g_objSupported)
Print("AFTEST3 T4 SKIP: pembuatan objek chart tidak didukung (jalankan dengan Visual=1)");
}
if(!g_objSupported) return;
AFR_Clear(g_cfg);
int before=CountObjectsWithPrefix(g_cfg.prefix);
AFR_DrawAll(g_cfg,g_e1,g_sDisp,g_dd,g_n,g_c,g_e,g_p,g_agg);
int after=CountObjectsWithPrefix(g_cfg.prefix);
if(after>before && after>0)
Pass("T4 render: "+IntegerToString(after)+" objek dibuat (prefix "+g_cfg.prefix+")");
else
Fail("T4 render: tidak ada objek dibuat (before="+IntegerToString(before)
+" after="+IntegerToString(after)+")");
// panel dashboard harus ada bila showDashboard
if(g_cfg.showDashboard)
{
if(ObjectFind(0,g_cfg.prefix+"P_BIAS")>=0)
Pass("T4 dashboard: panel P_BIAS ada");
else
Fail("T4 dashboard: panel P_BIAS tidak ditemukan");
}
// T5 - determinisme render: render ulang pada bar sama
AFR_Clear(g_cfg);
AFR_DrawAll(g_cfg,g_e1,g_sDisp,g_dd,g_n,g_c,g_e,g_p,g_agg);
int after2=CountObjectsWithPrefix(g_cfg.prefix);
if(after2==after)
Pass("T5 render deterministik: jumlah objek sama ("+IntegerToString(after2)+")");
else
Fail("T5 render tidak deterministik: "+IntegerToString(after)+" vs "+IntegerToString(after2));
g_lastObjCount=after2;
}
//+------------------------------------------------------------------+
//| Log verbose |
//+------------------------------------------------------------------+
void PrintAllVerbose()
{
PrintFormat("AFTEST3 [DISP] TF=%s structure=%s eqPos=%s HH=%d HL=%d LH=%d LL=%d",
AF_E2_TfName(g_e1.TfOf(g_sDisp)),g_dd.structure,g_dd.eqPos,
g_dd.HH,g_dd.HL,g_dd.LH,g_dd.LL);
PrintFormat("AFTEST3 [DISP] pivots=%d lines=%d ob=%d fvg=%d pdh=%.2f pdl=%.2f pwh=%.2f pwl=%.2f",
ArraySize(g_dd.pivots),ArraySize(g_dd.structLines),
ArraySize(g_dd.ob),ArraySize(g_dd.fvg),
g_dd.pdh,g_dd.pdl,g_dd.pwh,g_dd.pwl);
PrintFormat("AFTEST3 [AGENTS] N=%s C=%s E=%s P=%s",g_dd.nBias,g_dd.cBias,g_dd.eBias,g_dd.pBias);
PrintFormat("AFTEST3 [AGG] %s",g_agg.reason);
}
//+------------------------------------------------------------------+
bool RunAllChecks()
{
for(int i=0;i<4;i++)
{
int s=SlotIdx(i);
if(s<0 || !g_e1.IsReady(s) || g_e1.Count(s)<AF_E2_MIN_BARS)
{
PrintFormat("AFTEST3 SKIP: slot %s belum siap",SlotName(i));
return false;
}
}
if(!g_e1.IsReady(g_sDisp))
{
Print("AFTEST3 SKIP: slot DISP belum siap");
return false;
}
CheckClosedBar(); // T3
// Engine 2 + data display (pipeline lengkap)
g_eng2.Compute(g_e1,g_sN,g_sC,g_sE,g_sP,g_n,g_c,g_e,g_p,g_agg);
AF_BuildDisplayData(g_e1,g_sN,g_sC,g_sE,g_sP,g_sDisp,g_sD1,g_sW1,
g_cfg.obCount,g_cfg.fvgCount,
g_n,g_c,g_e,g_p,g_dd);
CheckDisplayData(); // T1
CheckRender(); // T4 + T5
if(g_verbose) PrintAllVerbose();
return true;
}
//+------------------------------------------------------------------+
int OnInit()
{
Print("AFTEST3 ============================================================");
Print("AFTEST3 Algo Forge Engine3 UnitTest v"+AF_PROJECT_VERSION);
Print("AFTEST3 Atribusi: "+AF_SOURCE_TITLE+" (c) "+AF_SOURCE_AUTHOR+" - "+AF_SOURCE_URL);
g_verbose=InpVerbose;
g_sN = g_e1.Register(AF_E2_TF_S1,InpMaxBars);
g_sC = g_e1.Register(AF_E2_TF_S2,InpMaxBars);
g_sE = g_e1.Register(AF_E2_TF_S3,InpMaxBars);
g_sP = g_e1.Register(AF_E2_TF_S4,InpMaxBars);
ENUM_TIMEFRAMES dispTf=(InpDispTF==PERIOD_CURRENT)? (ENUM_TIMEFRAMES)_Period : InpDispTF;
g_sDisp= g_e1.Register(dispTf,InpMaxBars);
g_sD1 = g_e1.Register(PERIOD_D1,AF_E3_LEVELS_D1_BARS);
g_sW1 = g_e1.Register(PERIOD_W1,AF_E3_LEVELS_W1_BARS);
for(int i=0;i<7;i++) g_lastBar[i]=-1;
if(g_sN<0 || g_sC<0 || g_sE<0 || g_sP<0 || g_sDisp<0)
Fail("Register slot gagal");
AFR_DefaultCfg(g_cfg);
g_cfg.showDashboard=true; // pastikan panel dicek
g_e1.Refresh();
PrintFormat("AFTEST3 slot: N=%d C=%d E=%d P=%d DISP=%d D1=%d W1=%d",
g_sN,g_sC,g_sE,g_sP,g_sDisp,g_sD1,g_sW1);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
void OnTick()
{
g_e1.Refresh();
bool barsChanged=false;
for(int i=0;i<7;i++)
{
int s=SlotIdx(i);
if(s<0 || !g_e1.IsReady(s)) continue;
long lb=g_e1.LastBars(s);
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(); // T2 - bar sama -> data harus identik
g_repaintPending=false;
}
}
//+------------------------------------------------------------------+
double OnTester()
{
Print("AFTEST3 ============================================================");
PrintFormat("AFTEST3 RESULT: PASS=%d FAIL=%d",g_pass,g_fail);
Print("AFTEST3 RESULT: "+(g_fail==0? "SEMUA CEK LULUS" : "ADA CEK GAGAL"));
PrintFormat("AFTEST3 final: DISP TF=%s count=%d historyCalls=%d",
AF_E2_TfName(g_e1.TfOf(g_sDisp)),g_e1.Count(g_sDisp),
g_e1.HistoryCalls(g_sDisp));
PrintFormat("AFTEST3 final: D1 count=%d historyCalls=%d | W1 count=%d historyCalls=%d",
g_e1.Count(g_sD1),g_e1.HistoryCalls(g_sD1),
g_e1.Count(g_sW1),g_e1.HistoryCalls(g_sW1));
PrintFormat("AFTEST3 final: TotalHistoryCalls=%d",g_e1.TotalHistoryCalls());
return (g_fail==0)? 1000.0 : 0.0;
}
//+------------------------------------------------------------------+