SniperGold_ML/MQL5/Experts/AlgoForge_Backtest_Baseline.mq5

798 lines
31 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| AlgoForge_Backtest_Baseline.mq5 |
//| Project : Algo Forge (refactor total) |
//+------------------------------------------------------------------+
//| ATRIBUSI SUMBER (WAJIB - jangan dihapus): |
//| SniperGold SMC Pro+ |
//| https://www.mql5.com/en/code/75466 |
//| Copyright (c) Waseem Shahrukh |
//| Proyek ini adalah refaktor total dengan arsitektur baru; |
//| atribusi sumber wajib dicantumkan (lihat AF_Attribution.mqh). |
//+------------------------------------------------------------------+
//| BACKTEST BASELINE (Tahap 5 Algo Forge): |
//| - Model : SniperGold_ML.mqh (MLP 19->12->2, freeze |
//| v20260821_2head, AUC long 0.6270 / short 0.6207). |
//| - Fitur : 19 fitur SMC dihitung INTERNAL di EA (identik dgn |
//| SniperGold_SMC_ProPlus_v4_4.mq5 ComputeMLFeatures + |
//| dependensinya) -- BUKAN iCustom (rapuh). |
//| - Data : HANYA dari Engine 1 (AFEngine1MTF) -- closed-bar |
//| lock, cache index reusable, anti-freeze. |
//| - Mode 0 : tulis prob long/short per bar M15 tertutup ke CSV |
//| (untuk parity/konsistensi runtime-training + AUC). |
//| - Mode 1 : trading net-of-spread di Strategy Tester |
//| (OrderSend market; spread aktual dari tester). |
//+------------------------------------------------------------------+
#property copyright "Algo Forge (refactor total) - atribusi wajib: https://www.mql5.com/en/code/75466 (Waseem Shahrukh)"
#property version "1.00"
#property description "Backtest baseline MLP 19->12->2 (freeze v20260821_2head): fitur internal (bukan iCustom), data via Engine 1. Mode 0 = CSV prob/bar (AUC/parity), Mode 1 = trade net-of-spread."
#include <AlgoForge\AF_Attribution.mqh>
#include <AlgoForge\AF_Defines.mqh>
#include <AlgoForge\AF_Engine1_MTFData.mqh>
#include <SniperGold_ML.mqh>
//==================================================================
// INPUTS
//==================================================================
input group "===== MODE ====="
input int InpMode = 1; // 0 = CSV prob per bar (parity/AUC), 1 = trade, 2 = CSV 19 fitur (parity), 3 = debug HTF cache
input bool InpLogCsv = true; // Tulis CSV audit (mode 0: prob; mode 1: trades)
input group "===== SINYAL (baseline MLP 0.627/0.621) ====="
input double InpThreshLong = 0.60; // Ambang prob long untuk sinyal BUY
input double InpThreshShort = 0.60; // Ambang prob short untuk sinyal SELL
input group "===== TRADE (mode 1) ====="
input double InpSL_ATR = 1.0; // SL (x ATR) dari entry
input double InpTP_ATR = 1.5; // TP (x ATR) dari entry
input int InpMaxHoldBars = 24; // Exit maksimum (bar M15; = horizon label baseline)
input double InpLot = 0.01; // Volume lot per posisi
input group "===== ENGINE 1 ====="
input int InpMaxBars = 700; // Kapasitas cache M15 (>= 2*SwingLen + Lookback)
//==================================================================
// PARAMETER FITUR (konsisten dgn training v4.4 default -- jangan
// diubah; model dilatih dgn konfigurasi ini)
//==================================================================
#define AF_BT_SWING_LEN 50 // InpSwingLen (v4.4)
#define AF_BT_INTERNAL_LEN 5 // InpInternalLen (v4.4)
#define AF_BT_LOOKBACK 600 // InpLookbackBars (v4.4)
#define AF_BT_GRAB_WIN 8 // InpGrabWindow (v4.4)
#define AF_BT_SEQ_WINDOW 40 // InpSeqWindow (v4.4): sweep -> CHoCH -> entry chain window
// = event validity/expiry utk f7 (P3-S.0 f7 event lifecycle fix)
#define AF_BT_EQ_THR 0.10 // InpEQThreshold (v4.4)
#define AF_BT_EQ_BARS 3 // InpEQBarsConfirm (v4.4)
#define AF_BT_DELTA_BARS 10 // InpDeltaBars (v4.4)
#define AF_BT_HTF1 PERIOD_D1 // InpHTF1 (v4.4)
#define AF_BT_HTF2 PERIOD_H4 // InpHTF2 (v4.4)
#define AF_BT_HTF3 PERIOD_H1 // InpHTF3 (v4.4)
#define AF_BT_MIN_BARS 160 // minimal bar M15 sebelum analisis
#define AF_BT_MAGIC 20260821
//==================================================================
// STATE
//==================================================================
struct BTPivot { int bar; double price; bool isHigh; };
BTPivot g_sp[]; int g_spN=0; // swing pivots (utk EQH/EQL)
BTPivot g_ip[]; int g_ipN=0; // internal pivots (utk liquidity grab)
int g_stBar[]; int g_stVal[]; int g_stN=0; // trend timeline (swing)
double g_swHigh=0,g_swLow=0; int g_swHighBar=-1,g_swLowBar=-1,g_swTrend=0;
int g_inTrend=0,g_inLastBreak=-1,g_swLastBreak=-1,g_bias=0;
int g_swpDir=0; int g_swpBar=-1; double g_swpLevel=0;
int g_chochDir=0; int g_chochBar=-1;
int g_dDir=0; double g_dNorm=0;
bool g_eqhSwept=false,g_eqlSwept=false;
double g_atr=0;
string g_bTF1="Neutral",g_bTF2="Neutral",g_bTF3="Neutral";
double g_feat[SGML_NF];
double g_mlProb=0.5, g_mlProbBear=0.5;
//==================================================================
// F1 EVENT CONTRACT (P3-S.11) — canonical EVENT semantics (frozen
// contract docs/SNIPERGOLD_CANONICAL_SETUP_CONTRACT_v1.md §K/§M,
// adjudicated P3-S.10 OD-3):
// EVENT { ts, dir, valid_until, superseded, source }
// - W_sweep = W_choch = AF_BT_SEQ_WINDOW (40) — NOT optimized.
// - active iff onset <= decisionBar <= onset + W (EXPIRED at W+1).
// - superseded : a newer onset of the same type replaced it (permanent;
// the stored latest onset is by construction the non-superseded one).
// - chain : sweep onset <= CHoCH onset <= sweep onset + W_sweep,
// both fresh at the decision bar, directions equal.
// Consumers (f9/f18) must NOT read raw persistent state without expiry
// (BUG-P3S2-001 / BUG-P3S3-001). Detection semantics
// (DetectLiquidityGrabs / ProcessStructure) are UNCHANGED.
//==================================================================
struct AFEventSweep
{
int bar; // onset bar (M15 feature frame, 0 = oldest)
int dir; // +1 bullish / -1 bearish
int valid_until; // bar + AF_BT_SEQ_WINDOW
bool superseded; // false for the stored latest onset
string source; // "DetectLiquidityGrabs"
};
struct AFEventChoch
{
int bar;
int dir;
int valid_until;
bool superseded;
string source; // "ProcessStructure(internal)"
};
AFEventSweep g_evSweep;
AFEventChoch g_evChoch;
void AFEventSweepReset()
{
g_evSweep.bar=-1; g_evSweep.dir=0; g_evSweep.valid_until=-1;
g_evSweep.superseded=false; g_evSweep.source="DetectLiquidityGrabs";
}
void AFEventChochReset()
{
g_evChoch.bar=-1; g_evChoch.dir=0; g_evChoch.valid_until=-1;
g_evChoch.superseded=false; g_evChoch.source="ProcessStructure(internal)";
}
// Latest sweep EVENT active at the decision bar (contract boundary: age <= W)
bool AFBT_SweepActive(int decisionBar)
{
if(g_evSweep.bar<0) return false;
return (decisionBar-g_evSweep.bar)<=AF_BT_SEQ_WINDOW;
}
// Latest CHoCH EVENT active at the decision bar (contract boundary: age <= W)
bool AFBT_ChochActive(int decisionBar)
{
if(g_evChoch.bar<0) return false;
return (decisionBar-g_evChoch.bar)<=AF_BT_SEQ_WINDOW;
}
// Canonical setup chain (frozen §D/§M + OD-4): latest active sweep AND latest
// active CHoCH, CHoCH after the sweep and within the sweep validity chain,
// directions equal. NO arithmetic voting at the setup layer.
bool AFBT_ChainValid(int decisionBar)
{
if(g_evSweep.dir==0 || g_evChoch.dir==0) return false;
if(!AFBT_SweepActive(decisionBar)) return false;
if(!AFBT_ChochActive(decisionBar)) return false;
if(g_evChoch.bar<g_evSweep.bar) return false; // CHoCH after sweep
if((g_evChoch.bar-g_evSweep.bar)>AF_BT_SEQ_WINDOW) return false; // within sweep chain
if(g_evChoch.dir!=g_evSweep.dir) return false;
return true;
}
AFEngine1MTF e1;
int sM15=-1,sH1=-1,sH4=-1,sD1=-1;
bool g_ready=false;
datetime g_lastM15=0;
// ---- trade state (mode 1) ----
ulong g_ticket=0; int g_posDir=0; double g_entryPrice=0; datetime g_entryTime=0;
// ---- CSV ----
int g_fh=-1;
int g_nBars=0;
// ---- debug HTF (mode 3) : timestamp deterministik utk full-cache dump ----
datetime g_dbgPicks[] =
{
D'2026.01.05 12:00',
D'2026.03.18 01:00',
D'2026.04.01 00:00',
D'2026.06.16 13:30',
D'2026.07.20 15:45'
};
//==================================================================
// INIT / DEINIT
//==================================================================
int OnInit()
{
int mb=MathMax(InpMaxBars, 2*AF_BT_SWING_LEN + AF_BT_LOOKBACK);
sM15=e1.Register(PERIOD_M15,mb);
sH1 =e1.Register(PERIOD_H1,250);
sH4 =e1.Register(PERIOD_H4,250);
sD1 =e1.Register(PERIOD_D1,250);
if(sM15<0||sH1<0||sH4<0||sD1<0){ Print("AFBT: Register gagal"); return(INIT_FAILED); }
if(InpLogCsv)
{
string fn = (InpMode==0) ? "AlgoForge_bt_prob_"+_Symbol+"_M15.csv"
: (InpMode==2) ? "AlgoForge_bt_features_"+_Symbol+"_M15.csv"
: (InpMode==3) ? "AlgoForge_htf_debug_"+_Symbol+"_M15.csv"
: "AlgoForge_bt_trades_"+_Symbol+"_M15.csv";
g_fh=FileOpen(fn,FILE_WRITE|FILE_CSV|FILE_ANSI);
if(g_fh==INVALID_HANDLE){ Print("AFBT: FileOpen gagal ",fn," err=",GetLastError()); }
else
{
if(InpMode==0) FileWrite(g_fh,"time","close","atr","pLong","pShort");
else if(InpMode==2) FileWrite(g_fh,"time","close","atr","f0_htf1","f1_htf2","f2_htf3","f3_swing",
"f4_internal","f5_bias","f6_eqpos","f7_sweep","f8_choch","f9_chochok",
"f10_eqh","f11_eql","f12_dsign","f13_dmag","f14_dhigh","f15_dlow",
"f16_mom20","f17_range","f18_conf");
else if(InpMode==3) FileWrite(g_fh,"time","cntH1","cntH4","cntD1","needH1","needH4","needD1","biasH1","biasH4","biasD1");
else FileWrite(g_fh,"time","dir","entry","sl","tp","exitTime","exitPrice","pnl","reason");
}
}
g_lastM15=0; g_ready=false;
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
if(g_fh!=INVALID_HANDLE) { FileClose(g_fh); g_fh=-1; }
}
//==================================================================
// MAIN
//==================================================================
void OnTick()
{
if(!g_ready)
{
e1.Refresh();
if(!(e1.IsReady(sM15)&&e1.IsReady(sH1)&&e1.IsReady(sH4)&&e1.IsReady(sD1))) return;
if(e1.Count(sM15)<AF_BT_MIN_BARS) return;
g_ready=true;
}
else e1.Refresh();
datetime t=e1.Time(sM15,0);
if(t==g_lastM15) return; // bar M15 belum tertutup baru
g_lastM15=t;
g_nBars++;
ComputeAll();
if(InpMode==0)
{
if(g_fh!=INVALID_HANDLE)
FileWrite(g_fh,TimeToString(t,TIME_DATE|TIME_MINUTES),
DoubleToString(e1.Close(sM15,0),_Digits),
DoubleToString(g_atr,2),
DoubleToString(g_mlProb,5),
DoubleToString(g_mlProbBear,5));
return;
}
if(InpMode==2)
{
if(g_fh!=INVALID_HANDLE)
FileWrite(g_fh,TimeToString(t,TIME_DATE|TIME_MINUTES),
DoubleToString(e1.Close(sM15,0),_Digits),
DoubleToString(g_atr,2),
DoubleToString(g_feat[0],8),DoubleToString(g_feat[1],8),
DoubleToString(g_feat[2],8),DoubleToString(g_feat[3],8),
DoubleToString(g_feat[4],8),DoubleToString(g_feat[5],8),
DoubleToString(g_feat[6],8),DoubleToString(g_feat[7],8),
DoubleToString(g_feat[8],8),DoubleToString(g_feat[9],8),
DoubleToString(g_feat[10],8),DoubleToString(g_feat[11],8),
DoubleToString(g_feat[12],8),DoubleToString(g_feat[13],8),
DoubleToString(g_feat[14],8),DoubleToString(g_feat[15],8),
DoubleToString(g_feat[16],8),DoubleToString(g_feat[17],8),
DoubleToString(g_feat[18],8));
return;
}
if(InpMode==3)
{
if(g_fh!=INVALID_HANDLE)
{
string bh1=BTTFBias(sH1), bh4=BTTFBias(sH4), bd1=BTTFBias(sD1);
FileWrite(g_fh,TimeToString(t,TIME_DATE|TIME_MINUTES),
IntegerToString(e1.Count(sH1)),IntegerToString(e1.Count(sH4)),
IntegerToString(e1.Count(sD1)),
IntegerToString(MathMin(200,e1.Count(sH1))),
IntegerToString(MathMin(200,e1.Count(sH4))),
IntegerToString(MathMin(200,e1.Count(sD1))),
bh1,bh4,bd1);
if(g_nBars==1) DumpHTFCache(t); // cache awal (bar pertama diproses)
for(int p=0;p<ArraySize(g_dbgPicks);p++)
{
if(t==g_dbgPicks[p]) { DumpHTFCache(t); break; }
}
}
return;
}
// mode 1: manage sinyal + max hold
ManageSignal();
ManageMaxHold();
}
//==================================================================
// FITUR (salinan v4.4, sumber data Engine 1)
//==================================================================
bool FillM15(double &o[],double &h[],double &l[],double &c[],long &v[],datetime &tm[])
{
int n=e1.Count(sM15);
ArrayResize(o,n); ArrayResize(h,n); ArrayResize(l,n); ArrayResize(c,n);
ArrayResize(v,n); ArrayResize(tm,n);
for(int i=0;i<n;i++)
{
AFBar b;
if(!e1.GetBar(sM15,n-1-i,b)) return false; // as-series=false: 0 = tertua
o[i]=b.open; h[i]=b.high; l[i]=b.low; c[i]=b.close; v[i]=b.tick_volume; tm[i]=b.time;
}
return true;
}
void AddTrend(int bar,int val)
{
int n=ArraySize(g_stBar);
ArrayResize(g_stBar,n+1); ArrayResize(g_stVal,n+1);
g_stBar[n]=bar; g_stVal[n]=val; g_stN=n+1;
}
int SwingTrendAt(int bar)
{
int t=0;
for(int i=0;i<g_stN;i++){ if(g_stBar[i]<=bar) t=g_stVal[i]; else break; }
return t;
}
bool IsPivotHigh(const double &h[],int idx,int len,int total)
{
if(idx-len<0||idx+len>=total) return false;
double v=h[idx];
for(int k=1;k<=len;k++){ if(h[idx-k]>v) return false; if(h[idx+k]>v) return false; }
return true;
}
bool IsPivotLow(const double &l[],int idx,int len,int total)
{
if(idx-len<0||idx+len>=total) return false;
double v=l[idx];
for(int k=1;k<=len;k++){ if(l[idx-k]<v) return false; if(l[idx+k]<v) return false; }
return true;
}
void AddSwingPivot(int bar,double price,bool isHigh)
{
int n=ArraySize(g_sp);
ArrayResize(g_sp,n+1);
g_sp[n].bar=bar; g_sp[n].price=price; g_sp[n].isHigh=isHigh;
g_spN=n+1;
}
void AddInternalPivot(int bar,double price,bool isHigh)
{
int n=ArraySize(g_ip);
ArrayResize(g_ip,n+1);
g_ip[n].bar=bar; g_ip[n].price=price; g_ip[n].isHigh=isHigh;
g_ipN=n+1;
}
// ProcessStructure: BOS/CHoCH + trend timeline + pivots (tanpa draw/OB/alert)
void ProcessStructure(int len,bool internal,const datetime &time[],const double &open[],
const double &high[],const double &low[],const double &close[],
const long &vol[],int total,int startBar)
{
double upTarget=DBL_MAX,dnTarget=-DBL_MAX;
int upBar=-1,dnBar=-1,trend=0;
int lastLowBar=-1,lastHighBar=-1;
double lastLow=0,lastHigh=0;
int begin=MathMax(startBar,2*len);
for(int i=begin;i<total;i++)
{
int p=i-len;
if(p>=len)
{
if(IsPivotHigh(high,p,len,total)){ upTarget=high[p]; upBar=p; lastHighBar=p; lastHigh=high[p];
if(!internal) AddSwingPivot(p,high[p],true); else AddInternalPivot(p,high[p],true); }
if(IsPivotLow(low,p,len,total)){ dnTarget=low[p]; dnBar=p; lastLowBar=p; lastLow=low[p];
if(!internal) AddSwingPivot(p,low[p],false); else AddInternalPivot(p,low[p],false); }
}
// bullish break
if(upBar>=0 && close[i]>upTarget)
{
bool choch=(trend<0);
bool allow = !internal || SwingTrendAt(i)>=0; // InpConfluenceFilter=true (default v4.4)
trend=1;
if(!internal){ g_swTrend=1; g_swLastBreak=i; AddTrend(i,1); }
else { g_inTrend=1; g_inLastBreak=i; if(allow&&choch){ g_chochDir=1; g_chochBar=i; } }
upTarget=DBL_MAX; upBar=-1;
}
// bearish break
if(dnBar>=0 && close[i]<dnTarget)
{
bool choch=(trend>0);
bool allow = !internal || SwingTrendAt(i)<=0;
trend=-1;
if(!internal){ g_swTrend=-1; g_swLastBreak=i; AddTrend(i,-1); }
else { g_inTrend=-1; g_inLastBreak=i; if(allow&&choch){ g_chochDir=-1; g_chochBar=i; } }
dnTarget=-DBL_MAX; dnBar=-1;
}
}
if(!internal)
{
if(lastHighBar>=0){ g_swHigh=lastHigh; g_swHighBar=lastHighBar; }
if(lastLowBar>=0){ g_swLow=lastLow; g_swLowBar=lastLowBar; }
}
}
// DetectEQ: EQH/EQL swept (fitur 10/11)
void DetectEQ(const datetime &time[],const double &high[],const double &low[],int total)
{
g_eqhSwept=false; g_eqlSwept=false;
double tol=AF_BT_EQ_THR*g_atr;
for(int i=1;i<g_spN;i++)
{
if(MathAbs(g_sp[i].bar-g_sp[i-1].bar)<AF_BT_EQ_BARS) continue;
if(g_sp[i].isHigh&&g_sp[i-1].isHigh&&MathAbs(g_sp[i].price-g_sp[i-1].price)<=tol)
{
for(int b=g_sp[i].bar+1;b<total;b++)
if(high[b]>g_sp[i].price){ g_eqhSwept=true; break; }
}
if(!g_sp[i].isHigh&&!g_sp[i-1].isHigh&&MathAbs(g_sp[i].price-g_sp[i-1].price)<=tol)
{
for(int b=g_sp[i].bar+1;b<total;b++)
if(low[b]<g_sp[i].price){ g_eqlSwept=true; break; }
}
}
}
// DetectLiquidityGrabs: sweep internal high/low (fitur 8)
void DetectLiquidityGrabs(const datetime &time[],const double &high[],const double &low[],
const double &close[],int total)
{
for(int s=0;s<g_ipN;s++)
{
int p=g_ip[s].bar;
double lvl=g_ip[s].price;
int last=MathMin(total-1,p+AF_BT_GRAB_WIN);
for(int b=p+1;b<=last;b++)
{
// bearish grab: wick above internal high, close back below -> buy-side swept (bearish intent)
if(g_ip[s].isHigh && high[b]>lvl && close[b]<lvl)
{
if(b>g_swpBar){ g_swpBar=b; g_swpDir=-1; g_swpLevel=lvl; }
break;
}
// bullish grab: wick below internal low, close back above -> sell-side swept (bullish intent)
if(!g_ip[s].isHigh && low[b]<lvl && close[b]>lvl)
{
if(b>g_swpBar){ g_swpBar=b; g_swpDir=1; g_swpLevel=lvl; }
break;
}
}
}
}
// TFBias: fractal swing (s=3) + break, dari Engine 1 (closed-bar lock)
string BTTFBias(int slot)
{
int need=MathMin(200,e1.Count(slot));
if(need<120) return "Neutral";
double h[],l[],cl[];
ArrayResize(h,need); ArrayResize(l,need); ArrayResize(cl,need);
for(int i=0;i<need;i++)
{
AFBar b;
// P2.1 fix: window = 200 bar TERBARU cache (idxFromRight need-1-i -> 0)
if(!e1.GetBar(slot,need-1-i,b)) return "Neutral";
h[i]=b.high; l[i]=b.low; cl[i]=b.close;
}
int s=3, m=need-1; // m excludes forming bar (Engine 1 sudah closed-bar)
double up=DBL_MAX, dn=-DBL_MAX; int upb=-1, dnb=-1, trend=0;
for(int i=s+1;i<m;i++)
{
int p=i-s;
if(p>=s)
{
bool isH=true,isL=true;
for(int k=1;k<=s;k++){ if(h[p]<=h[p+k]||h[p]<=h[p-k])isH=false; if(l[p]>=l[p+k]||l[p]>=l[p-k])isL=false; }
if(isH){ up=h[p]; upb=p; }
if(isL){ dn=l[p]; dnb=p; }
}
if(upb>=0 && cl[i]>up){ trend=1; up=DBL_MAX; upb=-1; }
if(dnb>=0 && cl[i]<dn){ trend=-1; dn=-DBL_MAX; dnb=-1; }
}
return (trend>0)?"Bullish":(trend<0)?"Bearish":"Neutral";
}
// debug (mode 3): dump full cache H1/H4/D1 (bar time+OHLC) ke CSV
void DumpHTFCache(datetime t)
{
if(g_fh==INVALID_HANDLE) return;
string slots[3]={"H1","H4","D1"};
int ids[3] ={sH1,sH4,sD1};
for(int si=0;si<3;si++)
{
int cnt=e1.Count(ids[si]);
FileWrite(g_fh,"=== FULLDUMP",slots[si],TimeToString(t,TIME_DATE|TIME_MINUTES),"count="+IntegerToString(cnt));
for(int i=0;i<cnt;i++)
{
AFBar b;
if(!e1.GetBar(ids[si],i,b)) break; // idxFromRight: 0 = bar tertutup TERBARU
FileWrite(g_fh,slots[si],IntegerToString(i),
TimeToString(b.time,TIME_DATE|TIME_MINUTES),
DoubleToString(b.open,_Digits),DoubleToString(b.high,_Digits),
DoubleToString(b.low,_Digits),DoubleToString(b.close,_Digits));
}
}
}
int BiasToInt(string s)
{
if(s=="Bullish") return 1;
if(s=="Bearish") return -1;
return 0;
}
// delta proxy (candle anatomy), identik v4.4
void BTDelta(const double &high[],const double &low[],const double &open[],
const double &close[],const long &vol[],int tcv)
{
g_dDir=0; g_dNorm=0;
double sum=0,sumVol=0; int kk=MathMin(AF_BT_DELTA_BARS,tcv-3);
for(int j=tcv-1-kk;j<tcv-1;j++)
{
if(j<1) continue;
double rng=high[j]-low[j]; if(rng<=0) rng=_Point;
double body=MathAbs(close[j]-open[j]);
double uw=high[j]-MathMax(open[j],close[j]);
double lw=MathMin(open[j],close[j])-low[j];
double bb,ss; if(close[j]>=open[j]){ bb=body+lw; ss=uw; } else { ss=body+uw; bb=lw; }
double tt=bb+ss; if(tt<=0) tt=_Point;
sum += (bb-ss)/tt * (double)vol[j];
sumVol += (double)vol[j];
}
g_dDir=(sum>0?1:(sum<0?-1:0));
g_dNorm=(sumVol>0)?MathMax(-1.0,MathMin(1.0,sum/sumVol)):0.0;
}
// ComputeMLFeatures: 19 fitur identik v4.4 / train_model.py
void ComputeMLFeatures(double price,const double &close[],int tc)
{
int b1=(g_bTF1=="Bullish")?1:(g_bTF1=="Bearish"?-1:0);
int b2=(g_bTF2=="Bullish")?1:(g_bTF2=="Bearish"?-1:0);
int b3=(g_bTF3=="Bullish")?1:(g_bTF3=="Bearish"?-1:0);
double rng=(g_swHigh>0&&g_swLow>0)?(g_swHigh-g_swLow):0.0;
double eqPos=(rng>0)?2.0*(price-g_swLow)/rng-1.0:0.0;
double dHigh=(g_swHigh>0)?MathMax(-10.0,MathMin(10.0,(g_swHigh-price)/g_atr)):0.0;
double dLow =(g_swLow >0)?MathMax(-10.0,MathMin(10.0,(price-g_swLow)/g_atr)):0.0;
double mom =(tc>=21)?(price-close[tc-21])/g_atr:0.0; // 20-bar: tc param = tcv = total -> close[total-21] = 20 interval dari price=c[total-1] (parity v4.4 & train_model.py; REVERT SB-06 percobaan, tc-20 ternyata 19-bar)
double rA =(rng>0)?rng/g_atr:0.0;
// F1 (P3-S.11): choch_confirms = canonical chain under the EVENT contract —
// latest active sweep AND latest active CHoCH; CHoCH after the sweep and
// within the sweep validity chain; directions equal. (BUG-P3S2-001 /
// BUG-P3S3-001 fixed: stale sweep/CHoCH no longer confirm. Detection
// semantics unchanged; validity boundary = frozen contract age <= W.)
int decisionBar=tc; // newest closed M15 bar index
bool swpValid =AFBT_SweepActive(decisionBar);
bool chochOK =AFBT_ChainValid(decisionBar);
g_feat[0] =b1; // htf1_bias
g_feat[1] =b2; // htf2_bias
g_feat[2] =b3; // htf3_bias
g_feat[3] =g_swTrend; // swing_trend
g_feat[4] =g_inTrend; // internal_trend
g_feat[5] =g_bias; // chart_bias
g_feat[6] =eqPos; // eq_pos_norm
// P3-S.0 f7 event lifecycle fix: sweep_dir hanya aktif selama event valid
// (onset .. onset+SeqWindow); setelah expired -> NO_SWEEP (0). Tidak mengubah
// trigger condition/close-back/pivot; g_swpDir internal tetap utk f9/f18.
g_feat[7] =(g_swpBar>=0 && (tc-1-g_swpBar)<=AF_BT_SEQ_WINDOW)? g_swpDir : 0; // sweep_dir
g_feat[8] =g_chochDir; // choch_dir
g_feat[9] =chochOK?1.0:0.0; // choch_confirms
g_feat[10]=g_eqhSwept?1.0:0.0; // eqh_swept
g_feat[11]=g_eqlSwept?1.0:0.0; // eql_swept
g_feat[12]=g_dDir; // delta_sign
g_feat[13]=g_dNorm; // delta_mag
g_feat[14]=dHigh; // dist_high_atr
g_feat[15]=dLow; // dist_low_atr
g_feat[16]=mom; // mom20_atr
g_feat[17]=rA; // range_atr
// fitur 18: skor konfluensi heuristik (identik v4.4)
int hb=0,hr=0;
if(b1>0) hb++; else if(b1<0) hr++;
if(b2>0) hb++; else if(b2<0) hr++;
if(b3>0) hb++; else if(b3<0) hr++;
int cb=0;
if(g_bias!=0) cb+=10;
if(hb==3 || hr==3) cb+=25; else if((hb>=2&&hr==0)||(hr>=2&&hb==0)) cb+=12;
if(eqPos<0 && g_bias>0) cb+=10;
if(eqPos>0 && g_bias<0) cb+=10;
if(swpValid && g_swpDir!=0) cb+=15; // F1: valid sweep only
if(chochOK) cb+=15; // F1: valid chain only
if(swpValid && ((g_swpDir==1&&g_bias>0)||(g_swpDir==-1&&g_bias<0))) cb+=15; // F1: valid sweep only
int g_confScore=MathMin(100,cb);
g_feat[18]=g_confScore; // confluence
g_mlProb=SGMLProb(g_feat);
g_mlProbBear=SGMLProbShort(g_feat);
}
// orkestrasi: hitung semua fitur pada bar tertutup terbaru M15
void ComputeAll()
{
int total=e1.Count(sM15); // = N-1 (semua tertutup; forming dibuang Engine 1)
int tcv=total; // setara tc v4.4 (= N-1)
int tc=total-1; // bar tertutup terakhir (setara close[tc-1] v4.4)
double o[],h[],l[],c[]; long v[]; datetime tm[];
if(!FillM15(o,h,l,c,v,tm)) return;
g_spN=0; ArrayResize(g_sp,0);
g_ipN=0; ArrayResize(g_ip,0);
g_stN=0; ArrayResize(g_stBar,0); ArrayResize(g_stVal,0);
g_swTrend=0; g_inTrend=0; g_swLastBreak=-1; g_inLastBreak=-1; g_bias=0;
g_swpDir=0; g_swpBar=-1; g_swpLevel=0; g_chochDir=0; g_chochBar=-1;
g_eqhSwept=false; g_eqlSwept=false;
g_atr=e1.ATR(sM15,14);
int startBar=MathMax(2*AF_BT_SWING_LEN,tcv-AF_BT_LOOKBACK);
ProcessStructure(AF_BT_SWING_LEN,false,tm,o,h,l,c,v,total,startBar);
ProcessStructure(AF_BT_INTERNAL_LEN,true,tm,o,h,l,c,v,total,startBar);
g_bias=(g_inLastBreak>=g_swLastBreak && g_inTrend!=0) ? g_inTrend : g_swTrend;
if(g_bias==0) g_bias=(g_swTrend!=0?g_swTrend:g_inTrend);
DetectEQ(tm,h,l,total);
DetectLiquidityGrabs(tm,h,l,c,total);
BTDelta(h,l,o,c,v,tcv);
// F1 (P3-S.11): maintain the canonical EVENT representation from the
// UNCHANGED detector state. The stored latest onset IS the active
// (non-superseded) event; an older onset is replaced (superseded) by
// construction (DetectLiquidityGrabs: b>g_swpBar; ProcessStructure:
// each qualifying break overwrites). valid_until = onset + W.
AFEventSweepReset();
AFEventChochReset();
if(g_swpBar>=0)
{
g_evSweep.bar=g_swpBar;
g_evSweep.dir=g_swpDir;
g_evSweep.valid_until=g_swpBar+AF_BT_SEQ_WINDOW;
}
if(g_chochBar>=0)
{
g_evChoch.bar=g_chochBar;
g_evChoch.dir=g_chochDir;
g_evChoch.valid_until=g_chochBar+AF_BT_SEQ_WINDOW;
}
g_bTF1=BTTFBias(sD1); g_bTF2=BTTFBias(sH4); g_bTF3=BTTFBias(sH1);
ComputeMLFeatures(c[tc],c,tcv);
}
//==================================================================
// TRADE (mode 1)
//==================================================================
void OpenPosition(bool buy)
{
if(g_ticket>0) return;
if(g_atr<=0) return;
double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
double entry=buy?ask:bid;
double sl =buy?entry-InpSL_ATR*g_atr : entry+InpSL_ATR*g_atr;
double tp =buy?entry+InpTP_ATR*g_atr : entry-InpTP_ATR*g_atr;
MqlTradeRequest req={};
MqlTradeResult res={};
req.action =TRADE_ACTION_DEAL;
req.symbol =_Symbol;
req.volume =InpLot;
req.type =buy?ORDER_TYPE_BUY:ORDER_TYPE_SELL;
req.price =entry;
req.sl =sl;
req.tp =tp;
req.deviation=50;
req.magic =AF_BT_MAGIC;
req.comment =buy?"AFBT_B":"AFBT_S";
if(!OrderSend(req,res))
{
PrintFormat("AFBT OrderSend gagal ret=%d err=%d",res.retcode,GetLastError());
return;
}
if(res.retcode==TRADE_RETCODE_DONE || res.retcode==TRADE_RETCODE_PLACED ||
res.retcode==TRADE_RETCODE_DONE_PARTIAL)
{
g_ticket=res.order; g_posDir=buy?1:-1; g_entryPrice=res.price; g_entryTime=g_lastM15;
if(g_fh!=INVALID_HANDLE)
FileWrite(g_fh,TimeToString(g_entryTime,TIME_DATE|TIME_MINUTES),
buy?"BUY":"SELL",
DoubleToString(g_entryPrice,_Digits),
DoubleToString(sl,_Digits),
DoubleToString(tp,_Digits),"","","","open");
}
}
// Select posisi by ticket tanpa ambiguity overload (build 6093)
bool BTSelectTicket(ulong ticket)
{
int total=PositionsTotal();
for(int i=0;i<total;i++)
{
ulong t=PositionGetTicket(i);
if(t==ticket) return true;
}
return false;
}
void ClosePosition(string reason)
{
if(g_ticket==0) return;
if(!BTSelectTicket(g_ticket)) { g_ticket=0; g_posDir=0; return; }
double vol=PositionGetDouble(POSITION_VOLUME);
double price=(g_posDir>0)?SymbolInfoDouble(_Symbol,SYMBOL_BID):SymbolInfoDouble(_Symbol,SYMBOL_ASK);
double pnl=PositionGetDouble(POSITION_PROFIT);
datetime exTime=TimeCurrent();
MqlTradeRequest req={};
MqlTradeResult res={};
req.action =TRADE_ACTION_DEAL;
req.symbol =_Symbol;
req.volume =vol;
req.type =(g_posDir>0)?ORDER_TYPE_SELL:ORDER_TYPE_BUY;
req.price =price;
req.deviation=50;
req.magic =AF_BT_MAGIC;
req.comment ="AFBT_X";
if(OrderSend(req,res))
{
if(g_fh!=INVALID_HANDLE)
FileWrite(g_fh,TimeToString(g_entryTime,TIME_DATE|TIME_MINUTES),
(g_posDir>0)?"BUY":"SELL",
DoubleToString(g_entryPrice,_Digits),
"","",
TimeToString(exTime,TIME_DATE|TIME_MINUTES),
DoubleToString(price,_Digits),
DoubleToString(pnl,2),
reason);
}
g_ticket=0; g_posDir=0;
}
void ManageSignal()
{
bool buySig =(g_mlProb >=InpThreshLong);
bool sellSig=(g_mlProbBear >=InpThreshShort);
if(buySig && !sellSig)
{
if(g_ticket>0 && g_posDir<0) ClosePosition("reverse");
if(g_ticket==0) OpenPosition(true);
}
else if(sellSig && !buySig)
{
if(g_ticket>0 && g_posDir>0) ClosePosition("reverse");
if(g_ticket==0) OpenPosition(false);
}
}
void ManageMaxHold()
{
if(g_ticket==0) return;
int ib=e1.FindBarIndex(sM15,g_entryTime);
if(ib<0 || ib>=InpMaxHoldBars) ClosePosition("maxhold");
}
//==================================================================
// TESTER
//==================================================================
double OnTester()
{
if(g_fh!=INVALID_HANDLE) { FileClose(g_fh); g_fh=-1; }
if(InpMode==1)
{
double profit=TesterStatistics(STAT_PROFIT);
double dd =TesterStatistics(STAT_BALANCE_DD);
double trades =TesterStatistics(STAT_TRADES);
PrintFormat("AFBT SUMMARY: trades=%.0f profit=%.2f dd=%.2f profitFactor=%.2f",
trades,profit,dd,TesterStatistics(STAT_PROFIT_FACTOR));
PrintFormat("AFBT MODEL: MLP 19->12->2 %s | thresh L=%.2f S=%.2f | SL=%.1fATR TP=%.1fATR hold=%d",
SGML_VER,InpThreshLong,InpThreshShort,InpSL_ATR,InpTP_ATR,InpMaxHoldBars);
}
else
{
PrintFormat("AFBT CSV DONE: bars=%d mode=%d (parity/AUC via Python)",g_nBars,InpMode);
}
return(0.0);
}