forked from chiki2bum2/SniperGold_ML
366 lines
13 KiB
MQL5
366 lines
13 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| AF_Engine1_MTFData.mqh |
| |||
//| Project : Algo Forge (refactor total) |
| |||
//| Sumber inspirasi: SniperGold SMC Pro+ (c) Waseem Shahrukh |
| |||
//| https://www.mql5.com/en/code/75466 |
| |||
//+------------------------------------------------------------------+
| |||
//| Engine 1 - Pengumpul Data Bar MTF + Cache Index Reusable |
| |||
//| |
| |||
//| TUGAS: |
| |||
//| 1. Mengumpulkan bar MTF (OHLCV) dari beberapa timeframe |
| |||
//| sesuai kebutuhan agen N/C/E/P (Engine 2) dan display |
| |||
//| (Engine 3). |
| |||
//| 2. Cache internal berbasis INDEX BAR: CopyRates hanya dipanggil |
| |||
//| ketika Bars(_Symbol,tf) berubah. Ini pola yang sudah |
| |||
//| terbukti di SniperGold v4.5 (RefreshMTFCache/BuildMTFStruct)|
| |||
//| dan mencegah pembacaan History berulang (anti-freeze). |
| |||
//| 3. Independen dari timeframe chart: semua akses memakai symbol |
| |||
//| eksplisit + ENUM_TIMEFRAMES eksplisit. PERIOD_CURRENT |
| |||
//| DILARANG (ditolak saat Register). |
| |||
//| 4. Closed-bar lock: cache HANYA berisi bar yang sudah tertutup.|
| |||
//| Bar pembentuk (bar terbaru yang masih berjalan) dibuang |
| |||
//| saat Build, sehingga analisis tidak pernah repaint. |
| |||
//| 5. Reusable & aman: guard reentrancy + statistik panggilan |
| |||
//| History (untuk unit test dan audit). |
| |||
//| |
| |||
//| INDEKS: cache disimpan seri (series): bars[0] = bar tertutup |
| |||
//| TERBARU, bars[n-1] = bar tertua. Konsumen memakai |
| |||
//| idxFromRight: 0 = bar tertutup terakhir. |
| |||
//+------------------------------------------------------------------+
| |||
#ifndef AF_ENGINE1_MTFDATA_MQH
| |||
#define AF_ENGINE1_MTFDATA_MQH
| |||
| |||
#include "AF_Defines.mqh"
| |||
| |||
//------------------------------------------------------------------
| |||
// Struct bar ringkas (subset MqlRates - cukup untuk agen sinyal)
| |||
//------------------------------------------------------------------
| |||
struct AFBar
| |||
{
| |||
datetime time;
| |||
double open;
| |||
double high;
| |||
double low;
| |||
double close;
| |||
long tick_volume;
| |||
long real_volume;
| |||
int spread;
| |||
};
| |||
| |||
//------------------------------------------------------------------
| |||
// Engine 1 - MTF Bar Collector + Cache
| |||
//------------------------------------------------------------------
| |||
class AFEngine1MTF
| |||
{
| |||
private:
| |||
struct AFSlot
| |||
{
| |||
string symbol; // symbol eksplisit (default _Symbol)
| |||
ENUM_TIMEFRAMES tf; // timeframe eksplisit
| |||
int maxBars; // kapasitas cache
| |||
// --- state cache ---
| |||
long lastBars; // Bars(symbol,tf) saat terakhir dibangun (cache index)
| |||
datetime lastRefresh; // waktu refresh terakhir (untuk throttle retry)
| |||
int count; // jumlah bar TERTUTUP di cache
| |||
AFBar bars[]; // [0] = bar tertutup TERBARU
| |||
bool ready;
| |||
// --- statistik (unit test / audit) ---
| |||
int historyCalls; // jumlah CopyRates yang benar-benar dieksekusi
| |||
int refreshCount; // jumlah rebuild
| |||
};
| |||
AFSlot m_slots[]; // daftar slot terdaftar (dynamic array)
| |||
int m_nSlots;
| |||
bool m_inRefresh; // guard reentrancy
| |||
| |||
int m_find(ENUM_TIMEFRAMES tf,const string symbol) const;
| |||
void Build(int idx); // isi ulang cache slot (HANYA saat bar baru TF itu)
| |||
| |||
public:
| |||
AFEngine1MTF() { m_nSlots=0; m_inRefresh=false; }
| |||
| |||
// --- lifecycle ---
| |||
int Register(ENUM_TIMEFRAMES tf,int maxBars=AF_E1_DEFAULT_BARS,const string symbol="");
| |||
bool Refresh(); // panggil tiap OnCalculate/OnTick; skip otomatis bila tak ada bar baru
| |||
| |||
// --- read API (idxFromRight: 0 = bar tertutup terakhir) ---
| |||
bool IsReady(int slot) const;
| |||
int Count(int slot) const;
| |||
bool GetBar(int slot,int idxFromRight,AFBar &out) const;
| |||
bool GetBarByTime(int slot,datetime t,AFBar &out) const;
| |||
int FindBarIndex(int slot,datetime t) const; // -1 bila tidak ada
| |||
double Open (int slot,int idxFromRight) const;
| |||
double High (int slot,int idxFromRight) const;
| |||
double Low (int slot,int idxFromRight) const;
| |||
double Close(int slot,int idxFromRight) const;
| |||
datetime Time (int slot,int idxFromRight) const;
| |||
long TickVolume(int slot,int idxFromRight) const;
| |||
| |||
// --- diagnostik / statistik ---
| |||
int HistoryCalls(int slot) const;
| |||
int RefreshCount(int slot) const;
| |||
int TotalHistoryCalls() const;
| |||
int SlotCount() const { return m_nSlots; }
| |||
long LastBars(int slot) const;
| |||
ENUM_TIMEFRAMES TfOf(int slot) const;
| |||
string SymbolOf(int slot) const;
| |||
| |||
// --- utilitas ---
| |||
static int PeriodSecondsOf(ENUM_TIMEFRAMES tf); // detik per TF (tanpa PERIOD_CURRENT)
| |||
static bool IsBarClosed(datetime barTime,ENUM_TIMEFRAMES tf,datetime now);
| |||
double ATR(int slot,int period=14) const; // ATR sederhana dari cache (bar tertutup)
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Implementasi |
| |||
//+------------------------------------------------------------------+
| |||
int AFEngine1MTF::m_find(ENUM_TIMEFRAMES tf,const string symbol) const
| |||
{
| |||
for(int i=0;i<m_nSlots;i++)
| |||
if(m_slots[i].tf==tf && m_slots[i].symbol==symbol)
| |||
return i;
| |||
return -1;
| |||
}
| |||
| |||
int AFEngine1MTF::Register(ENUM_TIMEFRAMES tf,int maxBars,const string symbol)
| |||
{
| |||
if(tf==PERIOD_CURRENT) return AF_E1_ERR_SLOT; // TF eksplisit WAJIB
| |||
if(maxBars<=0) maxBars=AF_E1_DEFAULT_BARS;
| |||
if(maxBars>AF_E1_MAX_BARS) maxBars=AF_E1_MAX_BARS;
| |||
string sym=(symbol=="" ? _Symbol : symbol);
| |||
int existing=m_find(tf,sym);
| |||
if(existing>=0) return existing; // idempoten
| |||
if(m_nSlots>=AF_E1_MAX_SLOTS) return AF_E1_ERR_SLOT; // slot penuh
| |||
int idx=m_nSlots;
| |||
ArrayResize(m_slots,idx+1);
| |||
m_slots[idx].symbol=sym;
| |||
m_slots[idx].tf=tf;
| |||
m_slots[idx].maxBars=maxBars;
| |||
m_slots[idx].lastBars=-1;
| |||
m_slots[idx].lastRefresh=0;
| |||
m_slots[idx].count=0;
| |||
m_slots[idx].ready=false;
| |||
m_slots[idx].historyCalls=0;
| |||
m_slots[idx].refreshCount=0;
| |||
ArrayResize(m_slots[idx].bars,maxBars);
| |||
m_nSlots++;
| |||
return idx;
| |||
}
| |||
| |||
bool AFEngine1MTF::Refresh()
| |||
{
| |||
if(m_inRefresh) return false; // guard reentrancy
| |||
m_inRefresh=true;
| |||
bool any=false;
| |||
datetime now=TimeCurrent();
| |||
for(int i=0;i<m_nSlots;i++)
| |||
{
| |||
long barsNow=Bars(m_slots[i].symbol,m_slots[i].tf);
| |||
if(barsNow<1) continue; // history TF belum siap -> coba lagi nanti
| |||
if(m_slots[i].ready && barsNow==m_slots[i].lastBars) continue; // TIDAK ada bar baru -> TANPA baca History
| |||
if(!m_slots[i].ready && (now-m_slots[i].lastRefresh)<AF_E1_RETRY_SEC) continue; // throttle retry
| |||
m_slots[i].historyCalls++;
| |||
Build(i);
| |||
if(m_slots[i].ready) m_slots[i].lastBars=barsNow;
| |||
m_slots[i].lastRefresh=now;
| |||
any=true;
| |||
}
| |||
m_inRefresh=false;
| |||
return any;
| |||
}
| |||
| |||
void AFEngine1MTF::Build(int idx)
| |||
{
| |||
m_slots[idx].count=0;
| |||
m_slots[idx].ready=false;
| |||
MqlRates rates[];
| |||
ArraySetAsSeries(rates,true); // [0] = bar terbaru
| |||
// Closed-bar lock: ambil maxBars+1 bar, lalu BUANG bar pembentuk
| |||
// (bar yang belum tertutup) bila ada. Dengan ini SEMUA bar di
| |||
// cache dijamin tertutup -> non-repaint.
| |||
int got=CopyRates(m_slots[idx].symbol,m_slots[idx].tf,0,m_slots[idx].maxBars+1,rates);
| |||
if(got<=0) return;
| |||
int skip=0;
| |||
if(!IsBarClosed(rates[0].time,m_slots[idx].tf,TimeCurrent())) skip=1;
| |||
int n=MathMin(got-skip,m_slots[idx].maxBars);
| |||
if(n<=0) return;
| |||
for(int i=0;i<n;i++)
| |||
{
| |||
m_slots[idx].bars[i].time = rates[i+skip].time;
| |||
m_slots[idx].bars[i].open = rates[i+skip].open;
| |||
m_slots[idx].bars[i].high = rates[i+skip].high;
| |||
m_slots[idx].bars[i].low = rates[i+skip].low;
| |||
m_slots[idx].bars[i].close = rates[i+skip].close;
| |||
m_slots[idx].bars[i].tick_volume = rates[i+skip].tick_volume;
| |||
m_slots[idx].bars[i].real_volume = rates[i+skip].real_volume;
| |||
m_slots[idx].bars[i].spread = rates[i+skip].spread;
| |||
}
| |||
m_slots[idx].count=n;
| |||
m_slots[idx].ready=true; m_slots[idx].refreshCount++;
| |||
}
| |||
| |||
//--- read API ---
| |||
bool AFEngine1MTF::IsReady(int slot) const
| |||
{
| |||
return (slot>=0 && slot<m_nSlots && m_slots[slot].ready);
| |||
}
| |||
| |||
int AFEngine1MTF::Count(int slot) const
| |||
{
| |||
return (slot>=0 && slot<m_nSlots) ? m_slots[slot].count : 0;
| |||
}
| |||
| |||
bool AFEngine1MTF::GetBar(int slot,int idxFromRight,AFBar &out) const
| |||
{
| |||
if(slot<0 || slot>=m_nSlots) return false;
| |||
if(!m_slots[slot].ready) return false;
| |||
if(idxFromRight<0 || idxFromRight>=m_slots[slot].count) return false;
| |||
out=m_slots[slot].bars[idxFromRight];
| |||
return true;
| |||
}
| |||
| |||
int AFEngine1MTF::FindBarIndex(int slot,datetime t) const
| |||
{
| |||
if(slot<0 || slot>=m_nSlots) return -1;
| |||
if(!m_slots[slot].ready) return -1;
| |||
for(int i=0;i<m_slots[slot].count;i++)
| |||
if(m_slots[slot].bars[i].time==t) return i;
| |||
return -1;
| |||
}
| |||
| |||
bool AFEngine1MTF::GetBarByTime(int slot,datetime t,AFBar &out) const
| |||
{
| |||
int idx=FindBarIndex(slot,t);
| |||
if(idx<0) return false;
| |||
return GetBar(slot,idx,out);
| |||
}
| |||
| |||
double AFEngine1MTF::Open(int slot,int idxFromRight) const
| |||
{
| |||
AFBar b;
| |||
return GetBar(slot,idxFromRight,b) ? b.open : 0.0;
| |||
}
| |||
| |||
double AFEngine1MTF::High(int slot,int idxFromRight) const
| |||
{
| |||
AFBar b;
| |||
return GetBar(slot,idxFromRight,b) ? b.high : 0.0;
| |||
}
| |||
| |||
double AFEngine1MTF::Low(int slot,int idxFromRight) const
| |||
{
| |||
AFBar b;
| |||
return GetBar(slot,idxFromRight,b) ? b.low : 0.0;
| |||
}
| |||
| |||
double AFEngine1MTF::Close(int slot,int idxFromRight) const
| |||
{
| |||
AFBar b;
| |||
return GetBar(slot,idxFromRight,b) ? b.close : 0.0;
| |||
}
| |||
| |||
datetime AFEngine1MTF::Time(int slot,int idxFromRight) const
| |||
{
| |||
AFBar b;
| |||
return GetBar(slot,idxFromRight,b) ? b.time : (datetime)0;
| |||
}
| |||
| |||
long AFEngine1MTF::TickVolume(int slot,int idxFromRight) const
| |||
{
| |||
AFBar b;
| |||
return GetBar(slot,idxFromRight,b) ? b.tick_volume : 0;
| |||
}
| |||
| |||
//--- diagnostik ---
| |||
int AFEngine1MTF::HistoryCalls(int slot) const
| |||
{
| |||
return (slot>=0 && slot<m_nSlots) ? m_slots[slot].historyCalls : 0;
| |||
}
| |||
| |||
int AFEngine1MTF::RefreshCount(int slot) const
| |||
{
| |||
return (slot>=0 && slot<m_nSlots) ? m_slots[slot].refreshCount : 0;
| |||
}
| |||
| |||
int AFEngine1MTF::TotalHistoryCalls() const
| |||
{
| |||
int total=0;
| |||
for(int i=0;i<m_nSlots;i++) total+=m_slots[i].historyCalls;
| |||
return total;
| |||
}
| |||
| |||
long AFEngine1MTF::LastBars(int slot) const
| |||
{
| |||
return (slot>=0 && slot<m_nSlots) ? m_slots[slot].lastBars : -1;
| |||
}
| |||
| |||
ENUM_TIMEFRAMES AFEngine1MTF::TfOf(int slot) const
| |||
{
| |||
return (slot>=0 && slot<m_nSlots) ? m_slots[slot].tf : PERIOD_CURRENT;
| |||
}
| |||
| |||
string AFEngine1MTF::SymbolOf(int slot) const
| |||
{
| |||
return (slot>=0 && slot<m_nSlots) ? m_slots[slot].symbol : "";
| |||
}
| |||
| |||
//--- utilitas ---
| |||
int AFEngine1MTF::PeriodSecondsOf(ENUM_TIMEFRAMES tf)
| |||
{
| |||
switch(tf)
| |||
{
| |||
case PERIOD_M1: return 60;
| |||
case PERIOD_M2: return 120;
| |||
case PERIOD_M3: return 180;
| |||
case PERIOD_M4: return 240;
| |||
case PERIOD_M5: return 300;
| |||
case PERIOD_M6: return 360;
| |||
case PERIOD_M10: return 600;
| |||
case PERIOD_M12: return 720;
| |||
case PERIOD_M15: return 900;
| |||
case PERIOD_M20: return 1200;
| |||
case PERIOD_M30: return 1800;
| |||
case PERIOD_H1: return 3600;
| |||
case PERIOD_H2: return 7200;
| |||
case PERIOD_H3: return 10800;
| |||
case PERIOD_H4: return 14400;
| |||
case PERIOD_H6: return 21600;
| |||
case PERIOD_H8: return 28800;
| |||
case PERIOD_H12: return 43200;
| |||
case PERIOD_D1: return 86400;
| |||
case PERIOD_W1: return 604800;
| |||
case PERIOD_MN1: return 2592000;
| |||
default: return 0; // PERIOD_CURRENT dkk -> invalid (0)
| |||
}
| |||
}
| |||
| |||
bool AFEngine1MTF::IsBarClosed(datetime barTime,ENUM_TIMEFRAMES tf,datetime now)
| |||
{
| |||
int sec=PeriodSecondsOf(tf);
| |||
if(sec<=0) return false;
| |||
return (barTime + (datetime)sec) <= now;
| |||
}
| |||
| |||
double AFEngine1MTF::ATR(int slot,int period) const
| |||
{
| |||
if(slot<0 || slot>=m_nSlots) return 0.0;
| |||
if(!m_slots[slot].ready || period<1) return 0.0;
| |||
int n=MathMin(period,m_slots[slot].count);
| |||
if(n==0) return 0.0;
| |||
double sum=0.0;
| |||
for(int i=0;i<n;i++)
| |||
{
| |||
double tr=m_slots[slot].bars[i].high-m_slots[slot].bars[i].low;
| |||
int j=i+1;
| |||
if(j<m_slots[slot].count)
| |||
{
| |||
double pc=m_slots[slot].bars[j].close;
| |||
double t1=MathAbs(m_slots[slot].bars[i].high-pc);
| |||
double t2=MathAbs(m_slots[slot].bars[i].low-pc);
| |||
tr=MathMax(tr,MathMax(t1,t2));
| |||
}
| |||
sum+=tr;
| |||
}
| |||
return sum/(double)n;
| |||
}
| |||
| |||
#endif // AF_ENGINE1_MTFDATA_MQH
|