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