305 lines
10 KiB
MQL5
305 lines
10 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| IncrementalInsightEngine.mqh - Real-time Statistics Engine |
| |||
//| Replaces batch InsightsRebuild with O(1) incremental updates |
| |||
//+------------------------------------------------------------------+
| |||
#ifndef __INCREMENTAL_INSIGHT_ENGINE_MQH__
| |||
#define __INCREMENTAL_INSIGHT_ENGINE_MQH__
| |||
| |||
#include <Files\File.mqh>
| |||
| |||
// Running statistics using Welford's algorithm for numerical stability
| |||
struct SRunningStats
| |||
{
| |||
string key;
| |||
string strategy;
| |||
string symbol;
| |||
int timeframe;
| |||
| |||
// Core counters
| |||
int n;
| |||
int wins;
| |||
double sum_r;
| |||
double sum_r_sq;
| |||
| |||
// Running calculations
| |||
double mean_r;
| |||
double m2_r;
| |||
double max_dd_r;
| |||
double peak_equity_r;
| |||
double current_equity_r;
| |||
| |||
// EWMA for responsiveness
| |||
double ewma_win_rate;
| |||
double ewma_alpha;
| |||
| |||
// State
| |||
bool auto_promoted;
| |||
datetime last_update;
| |||
| |||
SRunningStats() : n(0), wins(0), sum_r(0), sum_r_sq(0), mean_r(0), m2_r(0),
| |||
max_dd_r(0), peak_equity_r(0), current_equity_r(0),
| |||
ewma_win_rate(0.5), ewma_alpha(0.05), auto_promoted(false), last_update(0) {}
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Incremental Insight Engine - O(1) Real-time Updates |
| |||
//+------------------------------------------------------------------+
| |||
class CIncrementalInsightEngine
| |||
{
| |||
private:
| |||
SRunningStats m_stats[];
| |||
int m_stat_count;
| |||
string m_state_file;
| |||
datetime m_last_flush;
| |||
int m_flush_interval;
| |||
bool m_dirty;
| |||
| |||
int m_min_trades;
| |||
double m_min_win_rate;
| |||
double m_min_profit_factor;
| |||
double m_max_drawdown_r;
| |||
| |||
int FindOrCreateStats(const string &strategy, const string &symbol, const int timeframe)
| |||
{
| |||
string key = strategy + "|" + symbol + "|" + IntegerToString(timeframe);
| |||
for(int i = 0; i < m_stat_count; i++)
| |||
if(m_stats[i].key == key) return i;
| |||
| |||
ArrayResize(m_stats, m_stat_count + 1);
| |||
m_stats[m_stat_count].key = key;
| |||
m_stats[m_stat_count].strategy = strategy;
| |||
m_stats[m_stat_count].symbol = symbol;
| |||
m_stats[m_stat_count].timeframe = timeframe;
| |||
m_stats[m_stat_count].last_update = TimeCurrent();
| |||
m_stat_count++;
| |||
return m_stat_count - 1;
| |||
}
| |||
| |||
int FindStats(const string &strategy, const string &symbol, const int timeframe)
| |||
{
| |||
string key = strategy + "|" + symbol + "|" + IntegerToString(timeframe);
| |||
for(int i = 0; i < m_stat_count; i++)
| |||
if(m_stats[i].key == key) return i;
| |||
return -1;
| |||
}
| |||
| |||
void UpdateMeanVariance(SRunningStats &s, double r)
| |||
{
| |||
s.n++;
| |||
double delta = r - s.mean_r;
| |||
s.mean_r += delta / s.n;
| |||
double delta2 = r - s.mean_r;
| |||
s.m2_r += delta * delta2;
| |||
}
| |||
| |||
void UpdateDrawdown(SRunningStats &s, double r)
| |||
{
| |||
s.current_equity_r += r;
| |||
if(s.current_equity_r > s.peak_equity_r)
| |||
s.peak_equity_r = s.current_equity_r;
| |||
double dd = s.peak_equity_r - s.current_equity_r;
| |||
if(dd > s.max_dd_r) s.max_dd_r = dd;
| |||
}
| |||
| |||
void CheckPromotion(int idx)
| |||
{
| |||
if(m_stats[idx].auto_promoted) return;
| |||
if(m_stats[idx].n < m_min_trades) return;
| |||
| |||
double win_rate = (double)m_stats[idx].wins / m_stats[idx].n;
| |||
double pf = CalculateProfitFactor(m_stats[idx]);
| |||
| |||
if(win_rate >= m_min_win_rate && pf >= m_min_profit_factor && m_stats[idx].max_dd_r <= m_max_drawdown_r)
| |||
{
| |||
m_stats[idx].auto_promoted = true;
| |||
PrintFormat("[AUTO-PROMOTE] %s on %s M%d: WR=%.1f%% PF=%.2f DD=%.2fR",
| |||
m_stats[idx].strategy, m_stats[idx].symbol, m_stats[idx].timeframe, win_rate*100, pf, m_stats[idx].max_dd_r);
| |||
WritePromotionFlag(m_stats[idx]);
| |||
}
| |||
}
| |||
| |||
double CalculateProfitFactor(SRunningStats &s)
| |||
{
| |||
if(s.n == 0) return 0;
| |||
double gross_profit = s.wins * (s.mean_r > 0 ? s.mean_r * 1.5 : 1.0);
| |||
double gross_loss = (s.n - s.wins) * (s.mean_r < 0 ? MathAbs(s.mean_r) * 0.5 : 0.5);
| |||
if(gross_loss == 0) return gross_profit > 0 ? 999 : 0;
| |||
return gross_profit / gross_loss;
| |||
}
| |||
| |||
void WritePromotionFlag(SRunningStats &s)
| |||
{
| |||
string flag_file = "DualEA\\promotions\\" + s.strategy + "_" + s.symbol + "_" +
| |||
IntegerToString(s.timeframe) + ".promo";
| |||
FolderCreate("DualEA\\promotions", FILE_COMMON);
| |||
int h = FileOpen(flag_file, FILE_WRITE|FILE_TXT|FILE_COMMON);
| |||
if(h != INVALID_HANDLE)
| |||
{
| |||
FileWriteString(h, TimeToString(TimeCurrent()) + ",APPROVED");
| |||
FileClose(h);
| |||
}
| |||
}
| |||
| |||
public:
| |||
CIncrementalInsightEngine()
| |||
{
| |||
m_stat_count = 0;
| |||
m_state_file = "DualEA\\engine_state.bin";
| |||
m_last_flush = 0;
| |||
m_flush_interval = 300;
| |||
m_dirty = false;
| |||
m_min_trades = 30;
| |||
m_min_win_rate = 0.52;
| |||
m_min_profit_factor = 1.2;
| |||
m_max_drawdown_r = 10.0;
| |||
LoadState();
| |||
}
| |||
| |||
~CIncrementalInsightEngine() { SaveState(); }
| |||
| |||
// O(1) update - call from OnTradeTransaction
| |||
void RecordTradeOutcome(const string &strategy, const string &symbol,
| |||
const int timeframe, const double r_multiple)
| |||
{
| |||
int idx = FindOrCreateStats(strategy, symbol, timeframe);
| |||
| |||
m_stats[idx].sum_r += r_multiple;
| |||
m_stats[idx].sum_r_sq += r_multiple * r_multiple;
| |||
if(r_multiple > 0) m_stats[idx].wins++;
| |||
| |||
UpdateMeanVariance(m_stats[idx], r_multiple);
| |||
UpdateDrawdown(m_stats[idx], r_multiple);
| |||
| |||
bool is_win = r_multiple > 0;
| |||
m_stats[idx].ewma_win_rate = m_stats[idx].ewma_alpha * (is_win ? 1.0 : 0.0) + (1.0 - m_stats[idx].ewma_alpha) * m_stats[idx].ewma_win_rate;
| |||
m_stats[idx].last_update = TimeCurrent();
| |||
m_dirty = true;
| |||
| |||
CheckPromotion(idx);
| |||
}
| |||
| |||
// Instant query for LiveEA (no file I/O)
| |||
bool GetStrategyMetrics(const string &strategy, const string &symbol, const int timeframe,
| |||
int &trade_count, double &win_rate, double &avg_r,
| |||
double &profit_factor, double &max_dd_r)
| |||
{
| |||
int idx = FindStats(strategy, symbol, timeframe);
| |||
if(idx < 0) return false;
| |||
| |||
trade_count = m_stats[idx].n;
| |||
win_rate = m_stats[idx].n > 0 ? (double)m_stats[idx].wins / m_stats[idx].n : 0;
| |||
avg_r = m_stats[idx].mean_r;
| |||
profit_factor = CalculateProfitFactor(m_stats[idx]);
| |||
max_dd_r = m_stats[idx].max_dd_r;
| |||
return true;
| |||
}
| |||
| |||
bool IsStrategyApproved(const string &strategy, const string &symbol, const int timeframe)
| |||
{
| |||
int idx = FindStats(strategy, symbol, timeframe);
| |||
if(idx < 0) return false;
| |||
return m_stats[idx].auto_promoted;
| |||
}
| |||
| |||
void MaybeFlush()
| |||
{
| |||
if(!m_dirty) return;
| |||
if(TimeCurrent() - m_last_flush < m_flush_interval) return;
| |||
SaveState();
| |||
m_last_flush = TimeCurrent();
| |||
m_dirty = false;
| |||
}
| |||
| |||
void FlushNow()
| |||
{
| |||
if(!m_dirty && m_stat_count <= 0)
| |||
return;
| |||
SaveState();
| |||
m_last_flush = TimeCurrent();
| |||
m_dirty = false;
| |||
}
| |||
| |||
void SaveState()
| |||
{
| |||
FolderCreate("DualEA", FILE_COMMON);
| |||
int h = FileOpen(m_state_file, FILE_WRITE|FILE_BIN|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE);
| |||
if(h == INVALID_HANDLE)
| |||
{
| |||
PrintFormat("[InsightEngine] SaveState: cannot open %s (Common). err=%d", m_state_file, GetLastError());
| |||
return;
| |||
}
| |||
| |||
FileWriteInteger(h, m_stat_count);
| |||
for(int i = 0; i < m_stat_count; i++)
| |||
{
| |||
FileWriteString(h, m_stats[i].key);
| |||
FileWriteString(h, m_stats[i].strategy);
| |||
FileWriteString(h, m_stats[i].symbol);
| |||
FileWriteInteger(h, m_stats[i].timeframe);
| |||
FileWriteInteger(h, m_stats[i].n);
| |||
FileWriteInteger(h, m_stats[i].wins);
| |||
FileWriteDouble(h, m_stats[i].mean_r);
| |||
FileWriteDouble(h, m_stats[i].m2_r);
| |||
FileWriteDouble(h, m_stats[i].max_dd_r);
| |||
FileWriteDouble(h, m_stats[i].peak_equity_r);
| |||
FileWriteDouble(h, m_stats[i].ewma_win_rate);
| |||
FileWriteInteger(h, m_stats[i].auto_promoted ? 1 : 0);
| |||
FileWriteLong(h, m_stats[i].last_update);
| |||
}
| |||
FileClose(h);
| |||
PrintFormat("[InsightEngine] Saved %d strategy records to %s (Common=%s)",
| |||
m_stat_count, m_state_file, TerminalInfoString(TERMINAL_COMMONDATA_PATH));
| |||
}
| |||
| |||
void LoadState()
| |||
{
| |||
FolderCreate("DualEA", FILE_COMMON);
| |||
if(FileGetInteger(m_state_file, FILE_EXISTS, true) <= 0)
| |||
{
| |||
PrintFormat("[InsightEngine] State file NOT FOUND: %s (Common=%s) - cold start", m_state_file, TerminalInfoString(TERMINAL_COMMONDATA_PATH));
| |||
return;
| |||
}
| |||
int h = FileOpen(m_state_file, FILE_READ|FILE_BIN|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE);
| |||
if(h == INVALID_HANDLE)
| |||
{
| |||
PrintFormat("[InsightEngine] LoadState: cannot open %s (Common). err=%d", m_state_file, GetLastError());
| |||
return;
| |||
}
| |||
| |||
m_stat_count = FileReadInteger(h);
| |||
ArrayResize(m_stats, m_stat_count);
| |||
| |||
for(int i = 0; i < m_stat_count; i++)
| |||
{
| |||
m_stats[i].key = FileReadString(h);
| |||
m_stats[i].strategy = FileReadString(h);
| |||
m_stats[i].symbol = FileReadString(h);
| |||
m_stats[i].timeframe = FileReadInteger(h);
| |||
m_stats[i].n = FileReadInteger(h);
| |||
m_stats[i].wins = FileReadInteger(h);
| |||
m_stats[i].mean_r = FileReadDouble(h);
| |||
m_stats[i].m2_r = FileReadDouble(h);
| |||
m_stats[i].max_dd_r = FileReadDouble(h);
| |||
m_stats[i].peak_equity_r = FileReadDouble(h);
| |||
m_stats[i].ewma_win_rate = FileReadDouble(h);
| |||
m_stats[i].auto_promoted = (FileReadInteger(h) == 1);
| |||
m_stats[i].last_update = (datetime)FileReadLong(h);
| |||
}
| |||
FileClose(h);
| |||
PrintFormat("[InsightEngine] Loaded %d strategy records from %s (Common=%s)",
| |||
m_stat_count, m_state_file, TerminalInfoString(TERMINAL_COMMONDATA_PATH));
| |||
}
| |||
| |||
void SetPromotionCriteria(int min_trades, double min_wr, double min_pf, double max_dd)
| |||
{
| |||
m_min_trades = min_trades;
| |||
m_min_win_rate = min_wr;
| |||
m_min_profit_factor = min_pf;
| |||
m_max_drawdown_r = max_dd;
| |||
}
| |||
};
| |||
| |||
// Global singleton access - extern declaration (defined in EA files)
| |||
extern CIncrementalInsightEngine* g_insight_engine;
| |||
| |||
#endif
|