2026-08-21 10:01:21 +07:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| AF_Engine2_Agents.mqh |
|
|
|
|
|
//| Project : Algo Forge (refactor total) |
|
|
|
|
|
//| Sumber inspirasi: SniperGold SMC Pro+ (c) Waseem Shahrukh |
|
|
|
|
|
//| https://www.mql5.com/en/code/75466 |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Engine 2 - AGEN SINYAL INDEPENDEN (N/C/E/P) + FUZZY LOGIC |
|
|
|
|
|
//| |
|
|
|
|
|
//| PRINSIP KUNCI: |
|
|
|
|
|
//| 1. Tiap agen (Narrative/Context/Entry/PriceAction) adalah |
|
|
|
|
|
//| komponen INDEPENDEN: hanya membaca Engine 1 (AFEngine1MTF) |
|
|
|
|
|
//| melalui SATU slot (timeframe) miliknya sendiri. |
|
|
|
|
|
//| 2. Agen TIDAK saling tahu: tidak ada pemanggilan/state antar |
|
|
|
|
|
//| agen; tidak ada anggota statis bersama; semua metode murni |
|
|
|
|
|
//| fungsi dari data bar tertutup Engine 1 (stateless). |
|
|
|
|
|
//| 3. Fuzzy logic berbobot dinamis: tiap agen memakai fungsi |
|
|
|
|
|
//| keanggotaan (membership) + aturan berbobot; bobot berubah |
|
|
|
|
|
//| adaptif terhadap kondisi pasar yang diukur dari data agen |
|
|
|
|
|
//| itu sendiri (kekuatan tren, volatilitas, ranging, dll). |
|
|
|
|
|
//| 4. Non-repaint: semua input adalah bar TERTUTUP dari Engine 1 |
|
|
|
|
|
//| (closed-bar lock dijamin Engine 1). |
|
|
|
|
|
//| 5. Output per agen (AFSignalOut) dikonsumsi oleh AGREGATOR |
|
|
|
|
|
//| TERPISAH (AF_Engine2_Aggregator.mqh) - BUKAN antar-agen. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
#ifndef AF_ENGINE2_AGENTS_MQH
|
|
|
|
|
#define AF_ENGINE2_AGENTS_MQH
|
|
|
|
|
|
|
|
|
|
#include "AF_Defines.mqh"
|
|
|
|
|
#include "AF_Engine1_MTFData.mqh"
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Output standar satu agen (untuk agregator & display Engine 3)
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
struct AFSignalOut
|
|
|
|
|
{
|
|
|
|
|
string name; // nama agen
|
|
|
|
|
string tfName; // timeframe yang dipakai (string, untuk display)
|
|
|
|
|
double buy; // dukungan fuzzy BUY [0,1]
|
|
|
|
|
double sell; // dukungan fuzzy SELL [0,1]
|
|
|
|
|
double bias; // buy - sell [-1,1]
|
|
|
|
|
double confidence; // [0,1] kekuatan sinyal agen
|
|
|
|
|
int dir; // +1 buy / -1 sell / 0 wait
|
|
|
|
|
string reason; // alasan ringkas (untuk display)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void AF_SignalInit(AFSignalOut &out,const string name,const string tfName)
|
|
|
|
|
{
|
|
|
|
|
out.name=name;
|
|
|
|
|
out.tfName=tfName;
|
|
|
|
|
out.buy=0.0;
|
|
|
|
|
out.sell=0.0;
|
|
|
|
|
out.bias=0.0;
|
|
|
|
|
out.confidence=0.0;
|
|
|
|
|
out.dir=0;
|
|
|
|
|
out.reason="";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Helper fuzzy (murni, tanpa state - aman dipakai semua agen)
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
double AF_Clamp01(double v)
|
|
|
|
|
{
|
|
|
|
|
return (v<0.0)? 0.0 : (v>1.0)? 1.0 : v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fungsi keanggotaan segitiga (a<b<c)
|
|
|
|
|
double AF_MF_Tri(double x,double a,double b,double c)
|
|
|
|
|
{
|
|
|
|
|
if(x<=a || x>=c) return 0.0;
|
|
|
|
|
if(x==b) return 1.0;
|
|
|
|
|
double lo=(b>a)? (x-a)/(b-a) : 1.0;
|
|
|
|
|
double hi=(c>b)? (c-x)/(c-b) : 1.0;
|
|
|
|
|
if(x<b) return lo;
|
|
|
|
|
return hi;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fungsi keanggotaan trapesium (a<=b<=c<=d); degenerasi aman
|
|
|
|
|
double AF_MF_Trap(double x,double a,double b,double c,double d)
|
|
|
|
|
{
|
|
|
|
|
if(x<=a || x>=d) return 0.0;
|
|
|
|
|
if(x>=b && x<=c) return 1.0;
|
|
|
|
|
double lo=(b>a)? (x-a)/(b-a) : 1.0;
|
|
|
|
|
double hi=(d>c)? (d-x)/(d-c) : 1.0;
|
|
|
|
|
if(x<b) return lo;
|
|
|
|
|
return hi;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Evaluator fuzzy (Mamdani ringan): akumulasi aturan berbobot
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
struct AFFuzzyEval
|
|
|
|
|
{
|
|
|
|
|
double buyAcc; // akumulasi bobot sisi BUY
|
|
|
|
|
double sellAcc; // akumulasi bobot sisi SELL
|
|
|
|
|
double wTot; // total bobot aturan yang menyala
|
|
|
|
|
void Reset() { buyAcc=0.0; sellAcc=0.0; wTot=0.0; }
|
|
|
|
|
void Rule(bool buySide,double fire,double weight)
|
|
|
|
|
{
|
|
|
|
|
if(fire<=0.0 || weight<=0.0) return;
|
|
|
|
|
wTot+=weight;
|
|
|
|
|
if(buySide) buyAcc+=fire*weight; else sellAcc+=fire*weight;
|
|
|
|
|
}
|
|
|
|
|
void Finalize(AFSignalOut &out,const string name,const string tfName) const
|
|
|
|
|
{
|
|
|
|
|
out.name=name;
|
|
|
|
|
out.tfName=tfName;
|
|
|
|
|
double denom=(wTot>0.0)? wTot : 1.0;
|
|
|
|
|
out.buy = AF_Clamp01(buyAcc/denom);
|
|
|
|
|
out.sell = AF_Clamp01(sellAcc/denom);
|
|
|
|
|
out.bias = out.buy-out.sell;
|
|
|
|
|
out.confidence= MathMax(out.buy,out.sell);
|
|
|
|
|
out.dir = (out.bias> AF_E2_DIR_TOL)? 1 : (out.bias< -AF_E2_DIR_TOL)? -1 : 0;
|
|
|
|
|
out.reason = "";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Statistik bar (murni, tanpa state)
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
double AF_AvgBody(const AFEngine1MTF &e1,int slot,int n)
|
|
|
|
|
{
|
|
|
|
|
if(!e1.IsReady(slot)) return 0.0;
|
|
|
|
|
int m=MathMin(n,e1.Count(slot));
|
|
|
|
|
if(m<=0) return 0.0;
|
|
|
|
|
double s=0.0;
|
|
|
|
|
for(int i=0;i<m;i++) s+=MathAbs(e1.Close(slot,i)-e1.Open(slot,i));
|
|
|
|
|
return s/(double)m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double AF_AvgRange(const AFEngine1MTF &e1,int slot,int n)
|
|
|
|
|
{
|
|
|
|
|
if(!e1.IsReady(slot)) return 0.0;
|
|
|
|
|
int m=MathMin(n,e1.Count(slot));
|
|
|
|
|
if(m<=0) return 0.0;
|
|
|
|
|
double s=0.0;
|
|
|
|
|
for(int i=0;i<m;i++) s+=(e1.High(slot,i)-e1.Low(slot,i));
|
|
|
|
|
return s/(double)m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// range min/max dari `lookback` bar tertutup + posisi close [0,1]
|
|
|
|
|
// (0 = terendah range, 1 = tertinggi range)
|
|
|
|
|
void AF_RangeStat(const AFEngine1MTF &e1,int slot,int lookback,double &rmin,double &rmax,double &pos)
|
|
|
|
|
{
|
|
|
|
|
rmin=0.0; rmax=0.0; pos=0.5;
|
|
|
|
|
if(!e1.IsReady(slot)) return;
|
|
|
|
|
int n=MathMin(lookback,e1.Count(slot));
|
|
|
|
|
if(n<2) return;
|
|
|
|
|
rmin=DBL_MAX;
|
|
|
|
|
rmax=-DBL_MAX;
|
|
|
|
|
for(int i=0;i<n;i++)
|
|
|
|
|
{
|
|
|
|
|
double h=e1.High(slot,i), l=e1.Low(slot,i);
|
|
|
|
|
if(h>rmax) rmax=h;
|
|
|
|
|
if(l<rmin) rmin=l;
|
|
|
|
|
}
|
|
|
|
|
if(rmax>rmin) pos=AF_Clamp01((e1.Close(slot,0)-rmin)/(rmax-rmin));
|
|
|
|
|
else pos=0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Analisis struktur (pivot/trend/CHoCH/BOS/sweep) - murni, tanpa state
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
struct AFPivot
|
|
|
|
|
{
|
|
|
|
|
datetime time;
|
|
|
|
|
double price;
|
|
|
|
|
int idx; // index bar di cache Engine 1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct AFSwingData
|
|
|
|
|
{
|
|
|
|
|
AFPivot highs[AF_E2_MAX_PIVOTS];
|
|
|
|
|
AFPivot lows [AF_E2_MAX_PIVOTS];
|
|
|
|
|
int nHigh;
|
|
|
|
|
int nLow;
|
|
|
|
|
void Reset(){ nHigh=0; nLow=0; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Pivot fractal 2-kiri/2-kanan; dikumpulkan dari bar TERBARU (idx kecil).
|
|
|
|
|
void AF_BuildSwing(const AFEngine1MTF &e1,int slot,AFSwingData &sd,int lookback)
|
|
|
|
|
{
|
|
|
|
|
sd.Reset();
|
|
|
|
|
if(!e1.IsReady(slot)) return;
|
|
|
|
|
int cnt=e1.Count(slot);
|
|
|
|
|
if(cnt<5) return;
|
|
|
|
|
int maxIdx=MathMin(cnt-3,(lookback>0)? lookback : cnt);
|
|
|
|
|
for(int i=2;i<=maxIdx;i++)
|
|
|
|
|
{
|
|
|
|
|
double h=e1.High(slot,i);
|
|
|
|
|
if(h>e1.High(slot,i-1) && h>e1.High(slot,i-2) &&
|
|
|
|
|
h>e1.High(slot,i+1) && h>e1.High(slot,i+2))
|
|
|
|
|
{
|
|
|
|
|
if(sd.nHigh<AF_E2_MAX_PIVOTS)
|
|
|
|
|
{
|
|
|
|
|
sd.highs[sd.nHigh].time =e1.Time(slot,i);
|
|
|
|
|
sd.highs[sd.nHigh].price=h;
|
|
|
|
|
sd.highs[sd.nHigh].idx =i;
|
|
|
|
|
sd.nHigh++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
double l=e1.Low(slot,i);
|
|
|
|
|
if(l<e1.Low(slot,i-1) && l<e1.Low(slot,i-2) &&
|
|
|
|
|
l<e1.Low(slot,i+1) && l<e1.Low(slot,i+2))
|
|
|
|
|
{
|
|
|
|
|
if(sd.nLow<AF_E2_MAX_PIVOTS)
|
|
|
|
|
{
|
|
|
|
|
sd.lows[sd.nLow].time =e1.Time(slot,i);
|
|
|
|
|
sd.lows[sd.nLow].price=l;
|
|
|
|
|
sd.lows[sd.nLow].idx =i;
|
|
|
|
|
sd.nLow++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tren dari urutan pivot: +1 bullish, -1 bearish, 0 mixed; clarity [0,1]
|
|
|
|
|
void AF_TrendFromSwing(const AFSwingData &sd,int &trend,double &clarity)
|
|
|
|
|
{
|
|
|
|
|
trend=0; clarity=0.0;
|
|
|
|
|
int up=0, dn=0, pairs=0;
|
|
|
|
|
int nH=MathMin(4,sd.nHigh);
|
|
|
|
|
int nL=MathMin(4,sd.nLow);
|
|
|
|
|
for(int i=0;i+1<nH;i++)
|
|
|
|
|
{
|
|
|
|
|
if(sd.highs[i].price>sd.highs[i+1].price) up++; else dn++;
|
|
|
|
|
pairs++;
|
|
|
|
|
}
|
|
|
|
|
for(int i=0;i+1<nL;i++)
|
|
|
|
|
{
|
|
|
|
|
if(sd.lows[i].price>sd.lows[i+1].price) up++; else dn++;
|
|
|
|
|
pairs++;
|
|
|
|
|
}
|
|
|
|
|
if(pairs<=0) return;
|
|
|
|
|
clarity=(double)MathMax(up,dn)/(double)pairs;
|
|
|
|
|
trend=(up>dn)? 1 : (dn>up)? -1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CHoCH: +1 bullish (break di atas LH pada struktur turun), -1 bearish, 0 none
|
|
|
|
|
int AF_DetectChoch(const AFEngine1MTF &e1,int slot,const AFSwingData &sd,int prevTrend)
|
|
|
|
|
{
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
if(sd.nHigh<1 || sd.nLow<1) return 0;
|
|
|
|
|
double close=e1.Close(slot,0);
|
|
|
|
|
if(prevTrend<0 && close>sd.highs[0].price) return 1;
|
|
|
|
|
if(prevTrend>0 && close<sd.lows[0].price) return -1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BOS: +1 bullish (HH baru pada tren naik), -1 bearish, 0 none
|
|
|
|
|
int AF_DetectBos(const AFEngine1MTF &e1,int slot,const AFSwingData &sd,int trend)
|
|
|
|
|
{
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
double close=e1.Close(slot,0);
|
|
|
|
|
if(trend>0 && sd.nHigh>=1 && close>sd.highs[0].price) return 1;
|
|
|
|
|
if(trend<0 && sd.nLow>=1 && close<sd.lows[0].price) return -1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sweep likuiditas: +1 sell-side tersapu lalu harga kembali (bullish),
|
|
|
|
|
// -1 buy-side tersapu lalu harga kembali (bearish), 0 none
|
|
|
|
|
int AF_DetectSweep(const AFEngine1MTF &e1,int slot,const AFSwingData &sd,int lookback)
|
|
|
|
|
{
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
if(sd.nLow<1 || sd.nHigh<1) return 0;
|
|
|
|
|
int cnt=e1.Count(slot);
|
|
|
|
|
int n=MathMin(lookback,cnt-1);
|
|
|
|
|
if(n<1) return 0;
|
|
|
|
|
double levelLow =(sd.nLow>=2)? MathMin(sd.lows[0].price,sd.lows[1].price) : sd.lows[0].price;
|
|
|
|
|
double levelHigh=(sd.nHigh>=2)? MathMax(sd.highs[0].price,sd.highs[1].price): sd.highs[0].price;
|
|
|
|
|
int bestBull=-1, bestBear=-1;
|
|
|
|
|
for(int i=0;i<n;i++)
|
|
|
|
|
{
|
|
|
|
|
if(bestBull<0 && e1.Low(slot,i)<levelLow && e1.Close(slot,i)>levelLow) bestBull=i;
|
|
|
|
|
if(bestBear<0 && e1.High(slot,i)>levelHigh && e1.Close(slot,i)<levelHigh) bestBear=i;
|
|
|
|
|
}
|
|
|
|
|
if(bestBull>=0 && (bestBear<0 || bestBull<=bestBear)) return 1;
|
|
|
|
|
if(bestBear>=0) return -1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Displacement: body bar terbaru >> rata-rata body (move kuat)
|
|
|
|
|
int AF_DetectDisplacement(const AFEngine1MTF &e1,int slot,int avgN)
|
|
|
|
|
{
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
double avg=AF_AvgBody(e1,slot,avgN);
|
|
|
|
|
if(avg<=0.0) return 0;
|
|
|
|
|
double body=MathAbs(e1.Close(slot,0)-e1.Open(slot,0));
|
|
|
|
|
if(body<1.6*avg) return 0;
|
|
|
|
|
return (e1.Close(slot,0)>e1.Open(slot,0))? 1 : -1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 23:31:22 +07:00
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// F1 EVENT CONTRACT — materialization helpers (P3-S.11 semantics)
|
|
|
|
|
// The stateless detectors (AF_DetectSweep / AF_DetectChoch) return only
|
|
|
|
|
// the direction; these helpers expose the ONSET bar that the detector
|
|
|
|
|
// already computes internally, so the F3 setup layer can consume F1
|
|
|
|
|
// EVENTS {onset, dir, valid_until} instead of raw detector state.
|
|
|
|
|
// Geometry is UNCHANGED — same scans, same conditions; only the onset
|
|
|
|
|
// is reported. (P3-S.7 S-ST: stateless per-bar, no persistent state.)
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Sweep EVENT: same scan as AF_DetectSweep; returns dir (+1/-1/0) and
|
|
|
|
|
// sets onsetBar = reversed cache index of the newest sweep bar (-1 none).
|
|
|
|
|
int AF_DetectSweepEvent(const AFEngine1MTF &e1,int slot,const AFSwingData &sd,
|
|
|
|
|
int lookback,int &onsetBar)
|
|
|
|
|
{
|
|
|
|
|
onsetBar=-1;
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
if(sd.nLow<1 || sd.nHigh<1) return 0;
|
|
|
|
|
int cnt=e1.Count(slot);
|
|
|
|
|
int n=MathMin(lookback,cnt-1);
|
|
|
|
|
if(n<1) return 0;
|
|
|
|
|
double levelLow =(sd.nLow>=2)? MathMin(sd.lows[0].price,sd.lows[1].price) : sd.lows[0].price;
|
|
|
|
|
double levelHigh=(sd.nHigh>=2)? MathMax(sd.highs[0].price,sd.highs[1].price): sd.highs[0].price;
|
|
|
|
|
int bestBull=-1, bestBear=-1;
|
|
|
|
|
for(int i=0;i<n;i++)
|
|
|
|
|
{
|
|
|
|
|
if(bestBull<0 && e1.Low(slot,i)<levelLow && e1.Close(slot,i)>levelLow) bestBull=i;
|
|
|
|
|
if(bestBear<0 && e1.High(slot,i)>levelHigh && e1.Close(slot,i)<levelHigh) bestBear=i;
|
|
|
|
|
}
|
|
|
|
|
if(bestBull>=0 && (bestBear<0 || bestBull<=bestBear)) { onsetBar=bestBull; return 1; }
|
|
|
|
|
if(bestBear>=0) { onsetBar=bestBear; return -1; }
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CHoCH EVENT: same condition as AF_DetectChoch; onset = the CURRENT
|
|
|
|
|
// decision bar (bar 0) when the break is close-confirmed on the newest
|
|
|
|
|
// swing pivot (per-bar stateless, P3-S.7 S-ST).
|
|
|
|
|
int AF_DetectChochEvent(const AFEngine1MTF &e1,int slot,const AFSwingData &sd,
|
|
|
|
|
int prevTrend,int &onsetBar)
|
|
|
|
|
{
|
|
|
|
|
onsetBar=-1;
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
if(sd.nHigh<1 || sd.nLow<1) return 0;
|
|
|
|
|
double close=e1.Close(slot,0);
|
|
|
|
|
if(prevTrend<0 && close>sd.highs[0].price) { onsetBar=0; return 1; }
|
|
|
|
|
if(prevTrend>0 && close<sd.lows[0].price) { onsetBar=0; return -1; }
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 22:32:56 +07:00
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// ZONE STATE CONTRACT (frozen §K; P3-S.12 F2)
|
|
|
|
|
// A zone is a persistent entity identified by its causal formation bar.
|
|
|
|
|
// State is recomputed deterministically from CLOSED bars each call
|
|
|
|
|
// (Engine-2 stateless per-bar, P3-S.7 S-ST) — one formation = one zone,
|
|
|
|
|
// never re-emitted per bar (S-8).
|
|
|
|
|
// UNMITIGATED : ACTIVE — price never entered the zone.
|
|
|
|
|
// PARTIALLY_FILLED : ACTIVE — price entered but NO full fill
|
|
|
|
|
// (S-9: partial fill != mitigation).
|
|
|
|
|
// FULLY_MITIGATED : terminal — full fill; NOT consumable.
|
|
|
|
|
// invalidated : terminal flag; zone-level = full-fill (frozen
|
|
|
|
|
// §M "invalidation = mitigation is the terminal
|
|
|
|
|
// usable-state"); setup-level (F3) may set it.
|
|
|
|
|
// Consumer rule: available iff mit_state in {UNMITIGATED, PARTIALLY_FILLED}
|
|
|
|
|
// and NOT invalidated. Consumers read the NEWEST ACTIVE zone (S-9/S-14).
|
|
|
|
|
// No age expiry (W_zone_age = OPEN NUMERIC PARAMETER, default none).
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
enum AFZoneMitState
|
|
|
|
|
{
|
|
|
|
|
AF_ZONE_UNMITIGATED = 0,
|
|
|
|
|
AF_ZONE_PARTIALLY_FILLED = 1,
|
|
|
|
|
AF_ZONE_FULLY_MITIGATED = 2
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct AFZoneState
|
|
|
|
|
{
|
|
|
|
|
int bar; // formation bar (causal identity; reversed index)
|
|
|
|
|
int dir; // +1 bullish / -1 bearish / 0 none
|
|
|
|
|
double top; // upper bound
|
|
|
|
|
double bot; // lower bound
|
|
|
|
|
AFZoneMitState mit_state; // UNMITIGATED | PARTIALLY_FILLED | FULLY_MITIGATED
|
|
|
|
|
bool partial_filled; // informational: price entered the zone
|
|
|
|
|
bool invalidated; // terminal flag (zone-level = full-fill)
|
|
|
|
|
bool IsActive() const
|
|
|
|
|
{
|
|
|
|
|
return (mit_state==AF_ZONE_UNMITIGATED || mit_state==AF_ZONE_PARTIALLY_FILLED)
|
|
|
|
|
&& !invalidated;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// FVG zone state (S-9): wick full-fill — bull Low(j)<=bot / bear High(j)>=top
|
|
|
|
|
// on any later bar j (j < formation bar in reversed indexing). Partial fill =
|
|
|
|
|
// any later bar whose range overlaps the zone [bot,top].
|
|
|
|
|
void AF_FVGZoneState(const AFEngine1MTF &e1,int slot,int c3,int dir,
|
|
|
|
|
double top,double bot,AFZoneState &zs)
|
|
|
|
|
{
|
|
|
|
|
zs.bar=c3; zs.dir=dir; zs.top=top; zs.bot=bot;
|
|
|
|
|
zs.mit_state=AF_ZONE_UNMITIGATED;
|
|
|
|
|
zs.partial_filled=false;
|
|
|
|
|
zs.invalidated=false;
|
|
|
|
|
if(dir==0) return;
|
|
|
|
|
for(int j=c3-1;j>=0;j--)
|
|
|
|
|
{
|
|
|
|
|
double lo=e1.Low(slot,j);
|
|
|
|
|
double hi=e1.High(slot,j);
|
|
|
|
|
bool full=(dir>0)? (lo<=bot) : (hi>=top);
|
|
|
|
|
if(full)
|
|
|
|
|
{
|
|
|
|
|
zs.mit_state=AF_ZONE_FULLY_MITIGATED;
|
|
|
|
|
zs.partial_filled=true; // the full-fill bar itself overlaps the zone
|
|
|
|
|
zs.invalidated=true; // zone-level terminal (frozen §M)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if(hi>=bot-1e-12 && lo<=top+1e-12) zs.partial_filled=true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OB zone state (S-9): close-through full fill — bull Close(j)<bot /
|
|
|
|
|
// bear Close(j)>top on any later bar j (including the move candle, matching
|
|
|
|
|
// the display collector). Partial fill = any bar strictly AFTER the move
|
|
|
|
|
// candle (j < obBar-1 in reversed indexing) overlapping the zone.
|
|
|
|
|
void AF_OBZoneState(const AFEngine1MTF &e1,int slot,int obBar,int dir,
|
|
|
|
|
double top,double bot,AFZoneState &zs)
|
|
|
|
|
{
|
|
|
|
|
zs.bar=obBar; zs.dir=dir; zs.top=top; zs.bot=bot;
|
|
|
|
|
zs.mit_state=AF_ZONE_UNMITIGATED;
|
|
|
|
|
zs.partial_filled=false;
|
|
|
|
|
zs.invalidated=false;
|
|
|
|
|
if(dir==0) return;
|
|
|
|
|
for(int j=obBar-1;j>=0;j--) // includes the move candle (j=obBar-1)
|
|
|
|
|
{
|
|
|
|
|
double cl=e1.Close(slot,j);
|
|
|
|
|
bool full=(dir>0)? (cl<bot) : (cl>top);
|
|
|
|
|
if(full)
|
|
|
|
|
{
|
|
|
|
|
zs.mit_state=AF_ZONE_FULLY_MITIGATED;
|
|
|
|
|
zs.partial_filled=true;
|
|
|
|
|
zs.invalidated=true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for(int j=obBar-2;j>=0;j--) // strictly after the move candle (re-entry)
|
|
|
|
|
{
|
|
|
|
|
double lo=e1.Low(slot,j);
|
|
|
|
|
double hi=e1.High(slot,j);
|
|
|
|
|
if(hi>=bot-1e-12 && lo<=top+1e-12) { zs.partial_filled=true; break; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 10:01:21 +07:00
|
|
|
// Order Block terakhir: bar lawan warna sebelum move kuat.
|
|
|
|
|
// return +1 bullish OB, -1 bearish OB, 0 none; zona via zoneHi/zoneLo.
|
2026-08-22 22:32:56 +07:00
|
|
|
// P3-S.12 F2: hanya zona AKTIF (belum full-mitigasi close-through) yang
|
|
|
|
|
// dikembalikan; zona yang sudah dimitigasi dilewati (S-9, BUG-P3S5-001).
|
2026-08-21 10:01:21 +07:00
|
|
|
int AF_FindOrderBlock(const AFEngine1MTF &e1,int slot,int avgN,double &zoneHi,double &zoneLo)
|
|
|
|
|
{
|
|
|
|
|
zoneHi=0.0; zoneLo=0.0;
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
int cnt=e1.Count(slot);
|
|
|
|
|
if(cnt<avgN+2) return 0;
|
|
|
|
|
double avg=AF_AvgBody(e1,slot,avgN);
|
|
|
|
|
if(avg<=0.0) return 0;
|
|
|
|
|
for(int i=1;i<cnt-1;i++)
|
|
|
|
|
{
|
|
|
|
|
double bodyPrev=MathAbs(e1.Close(slot,i-1)-e1.Open(slot,i-1));
|
|
|
|
|
if(bodyPrev<1.5*avg) continue;
|
|
|
|
|
bool upMove=e1.Close(slot,i-1)>e1.Open(slot,i-1);
|
2026-08-22 22:32:56 +07:00
|
|
|
int dir=0;
|
2026-08-21 10:01:21 +07:00
|
|
|
if(upMove)
|
|
|
|
|
{
|
2026-08-22 22:32:56 +07:00
|
|
|
if(e1.Close(slot,i)<e1.Open(slot,i)) dir=1;
|
2026-08-21 10:01:21 +07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-08-22 22:32:56 +07:00
|
|
|
if(e1.Close(slot,i)>e1.Open(slot,i)) dir=-1;
|
2026-08-21 10:01:21 +07:00
|
|
|
}
|
2026-08-22 22:32:56 +07:00
|
|
|
if(dir==0) continue;
|
|
|
|
|
double top=e1.High(slot,i);
|
|
|
|
|
double bot=e1.Low(slot,i);
|
|
|
|
|
AFZoneState zs;
|
|
|
|
|
AF_OBZoneState(e1,slot,i,dir,top,bot,zs);
|
|
|
|
|
if(!zs.IsActive()) continue; // skip close-through mitigated (S-9)
|
|
|
|
|
zoneHi=top;
|
|
|
|
|
zoneLo=bot;
|
|
|
|
|
return dir;
|
2026-08-21 10:01:21 +07:00
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FVG (imbalance) terakhir. +1 bullish, -1 bearish, 0 none; zona via out.
|
2026-08-22 22:32:56 +07:00
|
|
|
// P3-S.12 F2: i=0 (bar tertutup terbaru ELIGIBLE sebagai C3, S-6 — tanpa
|
|
|
|
|
// lag 1 bar, tanpa future candle, BUG-P3S4-002); hanya zona AKTIF yang
|
|
|
|
|
// dikembalikan (wick full-fill = mitigated, dilewati, S-9, BUG-P3S4-001).
|
2026-08-21 10:01:21 +07:00
|
|
|
int AF_FindFVG(const AFEngine1MTF &e1,int slot,int lookback,double &zoneHi,double &zoneLo)
|
|
|
|
|
{
|
|
|
|
|
zoneHi=0.0; zoneLo=0.0;
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
int cnt=e1.Count(slot);
|
|
|
|
|
if(cnt<3) return 0;
|
|
|
|
|
int maxIdx=MathMin(cnt-3,(lookback>0)? lookback : cnt);
|
2026-08-22 22:32:56 +07:00
|
|
|
for(int i=0;i<=maxIdx;i++)
|
2026-08-21 10:01:21 +07:00
|
|
|
{
|
|
|
|
|
double l0=e1.Low(slot,i);
|
|
|
|
|
double h2=e1.High(slot,i+2);
|
|
|
|
|
if(l0>h2) // bullish FVG: gap antara candle i dan i+2
|
|
|
|
|
{
|
2026-08-22 22:32:56 +07:00
|
|
|
double bot=h2;
|
|
|
|
|
double top=l0;
|
|
|
|
|
AFZoneState zs;
|
|
|
|
|
AF_FVGZoneState(e1,slot,i,1,top,bot,zs);
|
|
|
|
|
if(!zs.IsActive()) continue; // skip wick-full-filled (S-9)
|
|
|
|
|
zoneLo=bot;
|
|
|
|
|
zoneHi=top;
|
2026-08-21 10:01:21 +07:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
double h0=e1.High(slot,i);
|
|
|
|
|
double l2=e1.Low(slot,i+2);
|
|
|
|
|
if(h0<l2) // bearish FVG
|
|
|
|
|
{
|
2026-08-22 22:32:56 +07:00
|
|
|
double bot=h0;
|
|
|
|
|
double top=l2;
|
|
|
|
|
AFZoneState zs;
|
|
|
|
|
AF_FVGZoneState(e1,slot,i,-1,top,bot,zs);
|
|
|
|
|
if(!zs.IsActive()) continue; // skip wick-full-filled (S-9)
|
|
|
|
|
zoneLo=bot;
|
|
|
|
|
zoneHi=top;
|
2026-08-21 10:01:21 +07:00
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pola price action dari 3 bar terakhir.
|
|
|
|
|
// return +1 bullish, -1 bearish, 0 none; code=arah pola, strength [0,1].
|
|
|
|
|
int AF_PatternPA(const AFEngine1MTF &e1,int slot,int &code,double &strength)
|
|
|
|
|
{
|
|
|
|
|
code=0; strength=0.0;
|
|
|
|
|
if(!e1.IsReady(slot)) return 0;
|
|
|
|
|
int cnt=e1.Count(slot);
|
|
|
|
|
if(cnt<4) return 0;
|
|
|
|
|
double o0=e1.Open(slot,0), h0=e1.High(slot,0), l0=e1.Low(slot,0), c0=e1.Close(slot,0);
|
|
|
|
|
double o1=e1.Open(slot,1), h1=e1.High(slot,1), l1=e1.Low(slot,1), c1=e1.Close(slot,1);
|
|
|
|
|
bool b0=(c0>o0), b1=(c1>o1);
|
|
|
|
|
double body0=MathAbs(c0-o0), rng0=h0-l0;
|
|
|
|
|
int best=0;
|
|
|
|
|
double bestStr=0.0;
|
|
|
|
|
|
|
|
|
|
// 1. engulfing (terkuat)
|
|
|
|
|
if(b0 && !b1 && c0>=o1 && o0<=c1) { best=1; bestStr=1.0; }
|
|
|
|
|
else if(!b0 && b1 && c0<=o1 && o0>=c1) { best=-1; bestStr=1.0; }
|
|
|
|
|
|
|
|
|
|
// 2. pin bar / hammer / shooting star
|
|
|
|
|
if(rng0>0.0)
|
|
|
|
|
{
|
|
|
|
|
double lowerWick=MathMin(o0,c0)-l0;
|
|
|
|
|
double upperWick=h0-MathMax(o0,c0);
|
|
|
|
|
if(body0>0.0 && lowerWick>2.0*body0 && upperWick<0.5*body0 && bestStr<0.85)
|
|
|
|
|
{ best=1; bestStr=0.85; }
|
|
|
|
|
if(body0>0.0 && upperWick>2.0*body0 && lowerWick<0.5*body0 && bestStr<0.85)
|
|
|
|
|
{ best=-1; bestStr=0.85; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. inside bar -> kelanjutan arah bar sebelumnya
|
|
|
|
|
if(h0<=h1+1e-12 && l0>=l1-1e-12)
|
|
|
|
|
{
|
|
|
|
|
if(b1 && bestStr<0.6) { best=1; bestStr=0.6; }
|
|
|
|
|
if(!b1 && bestStr<0.6) { best=-1; bestStr=0.6; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. momentum 2 bar searah
|
|
|
|
|
if(b0 && b1 && bestStr<0.7) { best=1; bestStr=0.7; }
|
|
|
|
|
if(!b0 && !b1 && bestStr<0.7) { best=-1; bestStr=0.7; }
|
|
|
|
|
|
|
|
|
|
// 5. posisi close di range bar terakhir
|
|
|
|
|
if(rng0>0.0)
|
|
|
|
|
{
|
|
|
|
|
double pos=(c0-l0)/rng0;
|
|
|
|
|
if(pos>0.70 && bestStr<0.5) { best=1; bestStr=0.5; }
|
|
|
|
|
if(pos<0.30 && bestStr<0.5) { best=-1; bestStr=0.5; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
code=best;
|
|
|
|
|
strength=bestStr;
|
|
|
|
|
return best;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nama TF ringkas untuk display (hapus prefix "PERIOD_")
|
|
|
|
|
string AF_E2_TfName(ENUM_TIMEFRAMES tf)
|
|
|
|
|
{
|
|
|
|
|
string s=EnumToString(tf);
|
|
|
|
|
int p=StringFind(s,"PERIOD_");
|
|
|
|
|
return (p==0)? StringSubstr(s,7) : s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| AGEN S1 - NARRATIVE (N): "Arah pasar apa?" |
|
|
|
|
|
//| Menilai: struktur HH/HL/LL/LH, CHoCH/MSS, BOS, likuiditas |
|
|
|
|
|
//| tersapu, premium/discount. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
class AFAgentNarrative
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
void Compute(const AFEngine1MTF &e1,int slot,AFSignalOut &out) const;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void AFAgentNarrative::Compute(const AFEngine1MTF &e1,int slot,AFSignalOut &out) const
|
|
|
|
|
{
|
|
|
|
|
string tfName=(e1.IsReady(slot))? AF_E2_TfName(e1.TfOf(slot)) : "?";
|
|
|
|
|
AF_SignalInit(out,"Narrative",tfName);
|
|
|
|
|
if(!e1.IsReady(slot)) { out.reason="engine1 belum siap"; return; }
|
|
|
|
|
if(e1.Count(slot)<AF_E2_MIN_BARS) { out.reason="data kurang"; return; }
|
|
|
|
|
|
|
|
|
|
AFSwingData sd;
|
|
|
|
|
AF_BuildSwing(e1,slot,sd,AF_E2_PIVOT_LOOKBACK);
|
|
|
|
|
int trend=0;
|
|
|
|
|
double clarity=0.0;
|
|
|
|
|
AF_TrendFromSwing(sd,trend,clarity);
|
|
|
|
|
int choch=AF_DetectChoch(e1,slot,sd,trend);
|
|
|
|
|
int bos =AF_DetectBos(e1,slot,sd,trend);
|
|
|
|
|
int sweep=AF_DetectSweep(e1,slot,sd,AF_E2_SWEEP_LOOKBACK);
|
|
|
|
|
double rmin=0.0, rmax=0.0, pos=0.5;
|
|
|
|
|
AF_RangeStat(e1,slot,AF_E2_LOOKBACK_PD,rmin,rmax,pos);
|
|
|
|
|
|
|
|
|
|
// fuzzy variables
|
|
|
|
|
double mBull=(trend>0)? 0.35+0.65*clarity : 0.0;
|
|
|
|
|
double mBear=(trend<0)? 0.35+0.65*clarity : 0.0;
|
|
|
|
|
double mDisc=AF_MF_Trap(pos,0.0,0.0,0.30,0.45); // zona diskon (beli)
|
|
|
|
|
double mPrem=AF_MF_Trap(pos,0.55,0.70,1.0,1.0); // zona premium (jual)
|
|
|
|
|
|
|
|
|
|
// bobot dinamis: tren jelas -> struktur dominan; flat -> zona/likuiditas
|
|
|
|
|
double wStruct=0.40*(0.5+0.5*clarity);
|
|
|
|
|
double wLiq =0.35;
|
|
|
|
|
double wZone =0.25*(1.5-0.5*clarity);
|
|
|
|
|
double wSum =wStruct+wLiq+wZone;
|
|
|
|
|
wStruct/=wSum; wLiq/=wSum; wZone/=wSum;
|
|
|
|
|
|
|
|
|
|
AFFuzzyEval fz;
|
|
|
|
|
fz.Reset();
|
|
|
|
|
fz.Rule(true, mBull, wStruct);
|
|
|
|
|
fz.Rule(false, mBear, wStruct);
|
|
|
|
|
fz.Rule(true, (sweep>0)? 1.0 : 0.0, wLiq);
|
|
|
|
|
fz.Rule(false,(sweep<0)? 1.0 : 0.0, wLiq);
|
|
|
|
|
fz.Rule(true, mDisc, wZone);
|
|
|
|
|
fz.Rule(false, mPrem, wZone);
|
|
|
|
|
if(choch>0) fz.Rule(true, 0.8, wStruct*0.5);
|
|
|
|
|
if(choch<0) fz.Rule(false, 0.8, wStruct*0.5);
|
|
|
|
|
if(bos>0 && trend>0) fz.Rule(true, 0.7, wStruct*0.3);
|
|
|
|
|
if(bos<0 && trend<0) fz.Rule(false, 0.7, wStruct*0.3);
|
|
|
|
|
|
|
|
|
|
fz.Finalize(out,out.name,out.tfName);
|
|
|
|
|
|
|
|
|
|
// reason
|
|
|
|
|
out.reason=(trend>0)? "tren naik" : (trend<0)? "tren turun" : "sideways";
|
|
|
|
|
if(choch>0) out.reason+=" +CHoCH naik";
|
|
|
|
|
if(choch<0) out.reason+=" -CHoCH turun";
|
|
|
|
|
if(bos>0) out.reason+=" +BOS";
|
|
|
|
|
if(bos<0) out.reason+=" -BOS";
|
|
|
|
|
if(sweep>0) out.reason+=" +sweep likuiditas bawah";
|
|
|
|
|
if(sweep<0) out.reason+=" -sweep likuiditas atas";
|
|
|
|
|
if(pos<0.38) out.reason+=" +zona diskon";
|
|
|
|
|
if(pos>0.62) out.reason+=" -zona premium";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| AGEN S2 - CONTEXT (C): "Di zona mana harga sekarang?" |
|
|
|
|
|
//| Menilai: OB, FVG, S/R (supply/demand), premium/discount. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
class AFAgentContext
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
void Compute(const AFEngine1MTF &e1,int slot,AFSignalOut &out) const;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void AFAgentContext::Compute(const AFEngine1MTF &e1,int slot,AFSignalOut &out) const
|
|
|
|
|
{
|
|
|
|
|
string tfName=(e1.IsReady(slot))? AF_E2_TfName(e1.TfOf(slot)) : "?";
|
|
|
|
|
AF_SignalInit(out,"Context",tfName);
|
|
|
|
|
if(!e1.IsReady(slot)) { out.reason="engine1 belum siap"; return; }
|
|
|
|
|
if(e1.Count(slot)<AF_E2_MIN_BARS) { out.reason="data kurang"; return; }
|
|
|
|
|
|
|
|
|
|
double close=e1.Close(slot,0);
|
|
|
|
|
double atr=e1.ATR(slot,14);
|
|
|
|
|
if(atr<=0.0) { out.reason="ATR 0"; return; }
|
|
|
|
|
|
|
|
|
|
double obHi=0.0, obLo=0.0;
|
|
|
|
|
int ob=AF_FindOrderBlock(e1,slot,AF_E2_LOOKBACK_AVG,obHi,obLo);
|
|
|
|
|
double fvHi=0.0, fvLo=0.0;
|
|
|
|
|
int fv=AF_FindFVG(e1,slot,AF_E2_FVG_LOOKBACK,fvHi,fvLo);
|
|
|
|
|
|
|
|
|
|
AFSwingData sd;
|
|
|
|
|
AF_BuildSwing(e1,slot,sd,AF_E2_PIVOT_LOOKBACK);
|
|
|
|
|
double distSup=DBL_MAX, distRes=DBL_MAX;
|
|
|
|
|
int nS=MathMin(12,sd.nLow);
|
|
|
|
|
int nR=MathMin(12,sd.nHigh);
|
|
|
|
|
for(int i=0;i<nS;i++)
|
|
|
|
|
{
|
|
|
|
|
double d=close-sd.lows[i].price;
|
|
|
|
|
if(d>=0.0 && d<distSup) distSup=d;
|
|
|
|
|
}
|
|
|
|
|
for(int i=0;i<nR;i++)
|
|
|
|
|
{
|
|
|
|
|
double d=sd.highs[i].price-close;
|
|
|
|
|
if(d>=0.0 && d<distRes) distRes=d;
|
|
|
|
|
}
|
|
|
|
|
double mAtSup=(distSup<DBL_MAX)? AF_Clamp01(1.0-distSup/atr) : 0.0; // 1 = tepat di support
|
|
|
|
|
double mAtRes=(distRes<DBL_MAX)? AF_Clamp01(1.0-distRes/atr) : 0.0; // 1 = tepat di resistance
|
|
|
|
|
|
|
|
|
|
double rmin=0.0, rmax=0.0, pos=0.5;
|
|
|
|
|
AF_RangeStat(e1,slot,AF_E2_LOOKBACK_PD,rmin,rmax,pos);
|
|
|
|
|
double mDisc=AF_MF_Trap(pos,0.0,0.0,0.30,0.45);
|
|
|
|
|
double mPrem=AF_MF_Trap(pos,0.55,0.70,1.0,1.0);
|
|
|
|
|
|
|
|
|
|
double mInObBull=0.0, mInObBear=0.0;
|
|
|
|
|
if(ob>0) mInObBull=AF_MF_Trap(close,obLo-0.3*atr,obLo,obHi,obHi+0.3*atr);
|
|
|
|
|
if(ob<0) mInObBear=AF_MF_Trap(close,obLo-0.3*atr,obLo,obHi,obHi+0.3*atr);
|
|
|
|
|
double mInFvBull=0.0, mInFvBear=0.0;
|
|
|
|
|
if(fv>0) mInFvBull=AF_MF_Trap(close,fvLo-0.3*atr,fvLo,fvHi,fvHi+0.3*atr);
|
|
|
|
|
if(fv<0) mInFvBear=AF_MF_Trap(close,fvLo-0.3*atr,fvLo,fvHi,fvHi+0.3*atr);
|
|
|
|
|
|
|
|
|
|
// bobot dinamis: volatilitas tinggi -> level S/R & premium/discount
|
|
|
|
|
// kurang andal; OB/FVG (imbalance) lebih relevan
|
|
|
|
|
double vol=atr/close;
|
|
|
|
|
double volFactor=(vol>0.002)? 0.70 : 1.0;
|
|
|
|
|
double wOb=0.30;
|
|
|
|
|
double wFv=0.25;
|
|
|
|
|
double wSr=0.25*volFactor;
|
|
|
|
|
double wPd=0.20*volFactor;
|
|
|
|
|
double wSum2=wOb+wFv+wSr+wPd;
|
|
|
|
|
wOb/=wSum2; wFv/=wSum2; wSr/=wSum2; wPd/=wSum2;
|
|
|
|
|
|
|
|
|
|
AFFuzzyEval fz;
|
|
|
|
|
fz.Reset();
|
|
|
|
|
fz.Rule(true, mInObBull, wOb);
|
|
|
|
|
fz.Rule(false, mInObBear, wOb);
|
|
|
|
|
fz.Rule(true, mInFvBull, wFv);
|
|
|
|
|
fz.Rule(false, mInFvBear, wFv);
|
|
|
|
|
fz.Rule(true, mAtSup, wSr);
|
|
|
|
|
fz.Rule(false, mAtRes, wSr);
|
|
|
|
|
fz.Rule(true, mDisc, wPd);
|
|
|
|
|
fz.Rule(false, mPrem, wPd);
|
|
|
|
|
fz.Finalize(out,out.name,out.tfName);
|
|
|
|
|
|
|
|
|
|
out.reason="";
|
|
|
|
|
if(ob>0 && mInObBull>0.5) out.reason+=" +di OB bullish";
|
|
|
|
|
if(ob<0 && mInObBear>0.5) out.reason+=" -di OB bearish";
|
|
|
|
|
if(fv>0 && mInFvBull>0.5) out.reason+=" +di FVG bullish";
|
|
|
|
|
if(fv<0 && mInFvBear>0.5) out.reason+=" -di FVG bearish";
|
|
|
|
|
if(mAtSup>0.5) out.reason+=" +dekat support";
|
|
|
|
|
if(mAtRes>0.5) out.reason+=" -dekat resistance";
|
|
|
|
|
if(mDisc>0.5) out.reason+=" +zona diskon";
|
|
|
|
|
if(mPrem>0.5) out.reason+=" -zona premium";
|
|
|
|
|
if(out.reason=="") out.reason="tidak di zona signifikan";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| AGEN S3 - ENTRY (E): "Apakah ada konfirmasi masuk?" |
|
|
|
|
|
//| Menilai: liquidity sweep, CHoCH/MSS, displacement, OB/FVG segar. |
|
|
|
|
|
//| Aturan kunci: ZONA + KONFIRMASI = setup valid. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
class AFAgentEntry
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
void Compute(const AFEngine1MTF &e1,int slot,AFSignalOut &out) const;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void AFAgentEntry::Compute(const AFEngine1MTF &e1,int slot,AFSignalOut &out) const
|
|
|
|
|
{
|
|
|
|
|
string tfName=(e1.IsReady(slot))? AF_E2_TfName(e1.TfOf(slot)) : "?";
|
|
|
|
|
AF_SignalInit(out,"Entry",tfName);
|
|
|
|
|
if(!e1.IsReady(slot)) { out.reason="engine1 belum siap"; return; }
|
|
|
|
|
if(e1.Count(slot)<AF_E2_MIN_BARS) { out.reason="data kurang"; return; }
|
|
|
|
|
|
|
|
|
|
AFSwingData sd;
|
|
|
|
|
AF_BuildSwing(e1,slot,sd,AF_E2_PIVOT_LOOKBACK);
|
|
|
|
|
int trend=0;
|
|
|
|
|
double clarity=0.0;
|
|
|
|
|
AF_TrendFromSwing(sd,trend,clarity);
|
|
|
|
|
int sweep=AF_DetectSweep(e1,slot,sd,AF_E2_SWEEP_LOOKBACK);
|
|
|
|
|
int choch=AF_DetectChoch(e1,slot,sd,trend);
|
|
|
|
|
int disp =AF_DetectDisplacement(e1,slot,AF_E2_LOOKBACK_AVG);
|
|
|
|
|
double obHi=0.0, obLo=0.0;
|
|
|
|
|
int ob=AF_FindOrderBlock(e1,slot,AF_E2_LOOKBACK_AVG,obHi,obLo);
|
|
|
|
|
double fvHi=0.0, fvLo=0.0;
|
|
|
|
|
int fv=AF_FindFVG(e1,slot,20,fvHi,fvLo);
|
|
|
|
|
double close=e1.Close(slot,0);
|
|
|
|
|
double atr=e1.ATR(slot,14);
|
|
|
|
|
if(atr<=0.0) { out.reason="ATR 0"; return; }
|
|
|
|
|
|
|
|
|
|
double mSweepBull=(sweep>0)? 1.0 : 0.0;
|
|
|
|
|
double mSweepBear=(sweep<0)? 1.0 : 0.0;
|
|
|
|
|
double mChochBull=(choch>0)? 1.0 : 0.0;
|
|
|
|
|
double mChochBear=(choch<0)? 1.0 : 0.0;
|
|
|
|
|
double mDispBull =(disp>0)? 1.0 : 0.0;
|
|
|
|
|
double mDispBear =(disp<0)? 1.0 : 0.0;
|
|
|
|
|
double mZoneBull=0.0, mZoneBear=0.0;
|
|
|
|
|
if(ob>0) mZoneBull=MathMax(mZoneBull,AF_MF_Trap(close,obLo-0.3*atr,obLo,obHi,obHi+0.3*atr));
|
|
|
|
|
if(ob<0) mZoneBear=MathMax(mZoneBear,AF_MF_Trap(close,obLo-0.3*atr,obLo,obHi,obHi+0.3*atr));
|
|
|
|
|
if(fv>0) mZoneBull=MathMax(mZoneBull,AF_MF_Trap(close,fvLo-0.3*atr,fvLo,fvHi,fvHi+0.3*atr));
|
|
|
|
|
if(fv<0) mZoneBear=MathMax(mZoneBear,AF_MF_Trap(close,fvLo-0.3*atr,fvLo,fvHi,fvHi+0.3*atr));
|
|
|
|
|
|
|
|
|
|
// bobot dinamis: displacement kuat -> bobot konfirmasi dinaikkan
|
|
|
|
|
double wSweep=0.25;
|
|
|
|
|
double wChoch=0.30;
|
|
|
|
|
double wDisp =0.20;
|
|
|
|
|
double wZone =0.15;
|
|
|
|
|
double wSetup=0.30;
|
|
|
|
|
if(MathAbs(disp)>0) wDisp*=1.3;
|
|
|
|
|
double wSum3=wSweep+wChoch+wDisp+wZone+wSetup;
|
|
|
|
|
wSweep/=wSum3; wChoch/=wSum3; wDisp/=wSum3; wZone/=wSum3; wSetup/=wSum3;
|
|
|
|
|
|
|
|
|
|
AFFuzzyEval fz;
|
|
|
|
|
fz.Reset();
|
|
|
|
|
fz.Rule(true, mSweepBull, wSweep);
|
|
|
|
|
fz.Rule(false, mSweepBear, wSweep);
|
|
|
|
|
fz.Rule(true, mChochBull, wChoch);
|
|
|
|
|
fz.Rule(false, mChochBear, wChoch);
|
|
|
|
|
fz.Rule(true, mDispBull, wDisp);
|
|
|
|
|
fz.Rule(false, mDispBear, wDisp);
|
|
|
|
|
fz.Rule(true, mZoneBull, wZone);
|
|
|
|
|
fz.Rule(false, mZoneBear, wZone);
|
|
|
|
|
// aturan setup: ZONA + KONFIRMASI = sinyal kuat
|
|
|
|
|
double confBull=MathMax(mSweepBull,mChochBull);
|
|
|
|
|
double confBear=MathMax(mSweepBear,mChochBear);
|
|
|
|
|
fz.Rule(true, MathMin(mZoneBull,confBull), wSetup);
|
|
|
|
|
fz.Rule(false, MathMin(mZoneBear,confBear), wSetup);
|
|
|
|
|
fz.Finalize(out,out.name,out.tfName);
|
|
|
|
|
|
|
|
|
|
out.reason="";
|
|
|
|
|
if(sweep>0) out.reason+=" +sweep bawah";
|
|
|
|
|
if(sweep<0) out.reason+=" -sweep atas";
|
|
|
|
|
if(choch>0) out.reason+=" +CHoCH naik";
|
|
|
|
|
if(choch<0) out.reason+=" -CHoCH turun";
|
|
|
|
|
if(disp>0) out.reason+=" +displacement naik";
|
|
|
|
|
if(disp<0) out.reason+=" -displacement turun";
|
|
|
|
|
if(mZoneBull>0.5) out.reason+=" +di zona (OB/FVG)";
|
|
|
|
|
if(mZoneBear>0.5) out.reason+=" -di zona (OB/FVG)";
|
|
|
|
|
if(out.reason=="") out.reason="belum ada konfirmasi";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| AGEN S4 - PRICE ACTION (P): "Kapan buka posisi?" |
|
|
|
|
|
//| Menilai: pola candlestick (engulfing, pin bar, inside bar, |
|
|
|
|
|
//| momentum, posisi close di range). |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
class AFAgentPriceAction
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
void Compute(const AFEngine1MTF &e1,int slot,AFSignalOut &out) const;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void AFAgentPriceAction::Compute(const AFEngine1MTF &e1,int slot,AFSignalOut &out) const
|
|
|
|
|
{
|
|
|
|
|
string tfName=(e1.IsReady(slot))? AF_E2_TfName(e1.TfOf(slot)) : "?";
|
|
|
|
|
AF_SignalInit(out,"PriceAction",tfName);
|
|
|
|
|
if(!e1.IsReady(slot)) { out.reason="engine1 belum siap"; return; }
|
|
|
|
|
if(e1.Count(slot)<AF_E2_MIN_BARS) { out.reason="data kurang"; return; }
|
|
|
|
|
|
|
|
|
|
int code=0;
|
|
|
|
|
double strength=0.0;
|
|
|
|
|
int pat=AF_PatternPA(e1,slot,code,strength);
|
|
|
|
|
double rmin=0.0, rmax=0.0, pos=0.5;
|
|
|
|
|
AF_RangeStat(e1,slot,20,rmin,rmax,pos);
|
|
|
|
|
|
|
|
|
|
double mBull=(pat>0)? 0.4+0.6*strength : 0.0;
|
|
|
|
|
double mBear=(pat<0)? 0.4+0.6*strength : 0.0;
|
|
|
|
|
|
|
|
|
|
// bobot dinamis: ranging -> pola reversal lebih berarti;
|
|
|
|
|
// trending -> pola continuation lebih berarti
|
|
|
|
|
double netMove=MathAbs(e1.Close(slot,0)-e1.Close(slot,20));
|
|
|
|
|
double rangeSize=rmax-rmin;
|
|
|
|
|
double mTrend=(rangeSize>0.0)? MathMin(1.0,netMove/(0.5*rangeSize)) : 0.3;
|
|
|
|
|
double mRange=AF_Clamp01(1.0-mTrend);
|
|
|
|
|
double wRev=0.45*(0.6+0.4*mRange);
|
|
|
|
|
double wCont=0.35*(0.6+0.4*mTrend);
|
|
|
|
|
double wPos=0.20;
|
|
|
|
|
double wSum4=wRev+wCont+wPos;
|
|
|
|
|
wRev/=wSum4; wCont/=wSum4; wPos/=wSum4;
|
|
|
|
|
|
|
|
|
|
// continuation: 2 bar terakhir searah
|
|
|
|
|
bool b0=e1.Close(slot,0)>e1.Open(slot,0);
|
|
|
|
|
bool b1=e1.Close(slot,1)>e1.Open(slot,1);
|
|
|
|
|
double mContBull=(b0&&b1)? 1.0 : 0.0;
|
|
|
|
|
double mContBear=(!b0&&!b1)? 1.0 : 0.0;
|
|
|
|
|
double mPosBull=AF_MF_Trap(pos,0.65,0.75,1.0,1.0); // close di atas range
|
|
|
|
|
double mPosBear=AF_MF_Trap(pos,0.0,0.0,0.25,0.35); // close di bawah range
|
|
|
|
|
|
|
|
|
|
AFFuzzyEval fz;
|
|
|
|
|
fz.Reset();
|
|
|
|
|
fz.Rule(true, mBull, wRev);
|
|
|
|
|
fz.Rule(false, mBear, wRev);
|
|
|
|
|
fz.Rule(true, mContBull, wCont);
|
|
|
|
|
fz.Rule(false, mContBear, wCont);
|
|
|
|
|
fz.Rule(true, mPosBull, wPos);
|
|
|
|
|
fz.Rule(false, mPosBear, wPos);
|
|
|
|
|
fz.Finalize(out,out.name,out.tfName);
|
|
|
|
|
|
|
|
|
|
out.reason="";
|
|
|
|
|
if(pat>0)
|
|
|
|
|
{
|
|
|
|
|
if(strength>=0.99) out.reason="+engulfing naik";
|
|
|
|
|
else if(strength>=0.84) out.reason="+pin bar naik";
|
|
|
|
|
else if(strength>=0.69) out.reason="+momentum 2 bar";
|
|
|
|
|
else if(strength>=0.59) out.reason="+inside bar (lanjut naik)";
|
|
|
|
|
else out.reason="+bias candle naik";
|
|
|
|
|
}
|
|
|
|
|
else if(pat<0)
|
|
|
|
|
{
|
|
|
|
|
if(strength>=0.99) out.reason="-engulfing turun";
|
|
|
|
|
else if(strength>=0.84) out.reason="-pin bar turun";
|
|
|
|
|
else if(strength>=0.69) out.reason="-momentum 2 bar";
|
|
|
|
|
else if(strength>=0.59) out.reason="-inside bar (lanjut turun)";
|
|
|
|
|
else out.reason="-bias candle turun";
|
|
|
|
|
}
|
|
|
|
|
else out.reason="pola netral";
|
|
|
|
|
if(mRange>0.6) out.reason+=" (ranging)";
|
|
|
|
|
else if(mTrend>0.6) out.reason+=" (trending)";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif // AF_ENGINE2_AGENTS_MQH
|