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
|