SniperGold_ML/MQL5/Include/AlgoForge/AF_Engine2_Agents.mqh

907 lines
33 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| 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;
}
//------------------------------------------------------------------
// 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;
}
//------------------------------------------------------------------
// 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; }
}
}
// Order Block terakhir: bar lawan warna sebelum move kuat.
// return +1 bullish OB, -1 bearish OB, 0 none; zona via zoneHi/zoneLo.
// P3-S.12 F2: hanya zona AKTIF (belum full-mitigasi close-through) yang
// dikembalikan; zona yang sudah dimitigasi dilewati (S-9, BUG-P3S5-001).
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);
int dir=0;
if(upMove)
{
if(e1.Close(slot,i)<e1.Open(slot,i)) dir=1;
}
else
{
if(e1.Close(slot,i)>e1.Open(slot,i)) dir=-1;
}
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;
}
return 0;
}
// FVG (imbalance) terakhir. +1 bullish, -1 bearish, 0 none; zona via out.
// 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).
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);
for(int i=0;i<=maxIdx;i++)
{
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
{
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;
return 1;
}
double h0=e1.High(slot,i);
double l2=e1.Low(slot,i+2);
if(h0<l2) // bearish FVG
{
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;
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