2026-08-21 15:21:30 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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 ====="
|
2026-08-22 22:03:54 +07:00 | | | input int InpMode = 1; // 0 = CSV prob per bar (parity/AUC), 1 = trade, 2 = CSV 19 fitur (parity), 3 = debug HTF cache
|
2026-08-21 15:21:30 +07:00 | | | 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)
|
2026-08-22 07:46:00 +07:00 | | | #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)
|
2026-08-21 15:21:30 +07:00 | | | #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;
|
| | |
|
2026-08-22 22:03:54 +07:00 | | | //==================================================================
|
| | | // 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;
|
| | | }
|
| | |
|
2026-08-21 15:21:30 +07:00 | | | 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;
|
| | |
|
2026-08-22 22:03:54 +07:00 | | | // ---- 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'
|
| | | };
|
| | |
|
2026-08-21 15:21:30 +07:00 | | | //==================================================================
|
| | | // 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"
|
2026-08-21 19:33:40 +07:00 | | | : (InpMode==2) ? "AlgoForge_bt_features_"+_Symbol+"_M15.csv"
|
2026-08-22 22:03:54 +07:00 | | | : (InpMode==3) ? "AlgoForge_htf_debug_"+_Symbol+"_M15.csv"
|
2026-08-21 19:33:40 +07:00 | | | : "AlgoForge_bt_trades_"+_Symbol+"_M15.csv";
|
2026-08-21 15:21:30 +07:00 | | | 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");
|
2026-08-21 19:33:40 +07:00 | | | 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");
|
2026-08-22 22:03:54 +07:00 | | | else if(InpMode==3) FileWrite(g_fh,"time","cntH1","cntH4","cntD1","needH1","needH4","needD1","biasH1","biasH4","biasD1");
|
2026-08-21 15:21:30 +07:00 | | | 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;
|
| | | }
|
| | |
|
2026-08-21 19:33:40 +07:00 | | | 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;
|
| | | }
|
| | |
|
2026-08-22 22:03:54 +07:00 | | | 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;
|
| | | }
|
| | |
|
2026-08-21 15:21:30 +07:00 | | | // 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;
|
2026-08-22 22:03:54 +07:00 | | | // P2.1 fix: window = 200 bar TERBARU cache (idxFromRight need-1-i -> 0)
|
| | | if(!e1.GetBar(slot,need-1-i,b)) return "Neutral";
|
2026-08-21 15:21:30 +07:00 | | | 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";
|
| | | }
|
| | |
|
2026-08-22 22:03:54 +07:00 | | | // 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;
|
| | | }
|
| | |
|
2026-08-21 15:21:30 +07:00 | | | // 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;
|
2026-08-21 19:33:40 +07:00 | | | 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)
|
2026-08-21 15:21:30 +07:00 | | | double rA =(rng>0)?rng/g_atr:0.0;
|
2026-08-22 22:03:54 +07:00 | | | // 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);
|
2026-08-21 15:21:30 +07:00 | | |
|
| | | 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
|
2026-08-22 07:46:00 +07:00 | | | // 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
|
2026-08-21 15:21:30 +07:00 | | | 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;
|
2026-08-22 22:03:54 +07:00 | | | 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
|
2026-08-21 15:21:30 +07:00 | | | 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);
|
| | |
|
2026-08-22 22:03:54 +07:00 | | | // 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;
|
| | | }
|
| | |
|
2026-08-21 15:21:30 +07:00 | | | 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
|
| | | {
|
2026-08-21 19:33:40 +07:00 | | | PrintFormat("AFBT CSV DONE: bars=%d mode=%d (parity/AUC via Python)",g_nBars,InpMode);
|
2026-08-21 15:21:30 +07:00 | | | }
|
| | | return(0.0);
|
| | | }
|