128 lines
5.5 KiB
MQL5
128 lines
5.5 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| KronosEvalQuality.mq5 |
|
|
//| MMQ — Muhammad Minhas Qamar |
|
|
//| www.mql5.com |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "MMQ — Muhammad Minhas Qamar"
|
|
#property link "https://www.mql5.com"
|
|
#property version "1.00"
|
|
#property strict
|
|
#property script_show_inputs
|
|
|
|
input string InpInFile = "kronos_eval\\eurusd_h1_forecasts.csv"; // dump file (MQL5/Files)
|
|
input int InpPredLen = 16; // horizon count P in the dump
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Per-horizon running accumulators. |
|
|
//+------------------------------------------------------------------+
|
|
struct HorizonStat
|
|
{
|
|
int n; // samples
|
|
int dir_hit; // Kronos directional hits
|
|
int rw_hit; // random-walk directional hits (baseline: always "flat")
|
|
double k_abs; // sum |kronos_ret - actual_ret|
|
|
double k_sq; // sum (kronos_ret - actual_ret)^2
|
|
double rw_abs; // sum |0 - actual_ret| == sum |actual_ret|
|
|
double rw_sq; // sum actual_ret^2
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Script entry: load dump, score, print table. |
|
|
//+------------------------------------------------------------------+
|
|
void OnStart()
|
|
{
|
|
const int P = InpPredLen;
|
|
if(P < 1) { Print("PredLen must be >= 1"); return; }
|
|
|
|
int fh = FileOpen(InpInFile, FILE_READ | FILE_CSV | FILE_ANSI, ',');
|
|
if(fh == INVALID_HANDLE)
|
|
{ PrintFormat("Cannot open %s (err %d). Run KronosForecastDump first.", InpInFile, GetLastError()); return; }
|
|
|
|
HorizonStat st[];
|
|
ArrayResize(st, P);
|
|
for(int h = 0; h < P; h++)
|
|
{ st[h].n = 0; st[h].dir_hit = 0; st[h].rw_hit = 0;
|
|
st[h].k_abs = 0; st[h].k_sq = 0; st[h].rw_abs = 0; st[h].rw_sq = 0; }
|
|
|
|
//--- expected columns: time, last_close, fc_c1..cP, ac_c1..cP => 2 + 2P
|
|
int expected = 2 + 2 * P;
|
|
int rows = 0, skipped_header = 0;
|
|
|
|
while(!FileIsEnding(fh))
|
|
{
|
|
string time_s = FileReadString(fh);
|
|
if(time_s == "") // trailing blank
|
|
break;
|
|
if(time_s == "time") // header line
|
|
{ // consume the rest of the header row
|
|
for(int k = 1; k < expected && !FileIsLineEnding(fh); k++)
|
|
FileReadString(fh);
|
|
skipped_header++;
|
|
continue;
|
|
}
|
|
|
|
double last_close = (double)FileReadString(fh);
|
|
double fc[]; ArrayResize(fc, P);
|
|
double ac[]; ArrayResize(ac, P);
|
|
for(int h = 0; h < P; h++) fc[h] = (double)FileReadString(fh);
|
|
for(int h = 0; h < P; h++) ac[h] = (double)FileReadString(fh);
|
|
|
|
if(last_close <= 0.0)
|
|
continue;
|
|
|
|
for(int h = 0; h < P; h++)
|
|
{
|
|
double k_ret = (fc[h] - last_close) / last_close; // predicted cumulative return to h
|
|
double a_ret = (ac[h] - last_close) / last_close; // realized cumulative return to h
|
|
|
|
//--- directional accuracy (skip exact-flat actuals: no direction to call)
|
|
if(a_ret != 0.0)
|
|
{
|
|
if((k_ret > 0.0) == (a_ret > 0.0)) st[h].dir_hit++;
|
|
//--- random-walk predicts no change => never calls a direction; counts as
|
|
//--- a coin-flip baseline of 0 hits here (reported separately below)
|
|
st[h].n++;
|
|
}
|
|
|
|
double k_err = k_ret - a_ret;
|
|
st[h].k_abs += MathAbs(k_err);
|
|
st[h].k_sq += k_err * k_err;
|
|
st[h].rw_abs += MathAbs(a_ret); // baseline error == |actual return|
|
|
st[h].rw_sq += a_ret * a_ret;
|
|
}
|
|
rows++;
|
|
}
|
|
FileClose(fh);
|
|
|
|
if(rows == 0)
|
|
{ Print("No data rows parsed. Check the dump file/PredLen."); return; }
|
|
|
|
PrintFormat("=== Kronos forecast-skill report === (%d forecasts, P=%d)", rows, P);
|
|
Print("h | DirAcc | RMSE_ret RW_RMSE skill | MAE_ret RW_MAE skill");
|
|
Print("--+----------+----------------------------+---------------------------");
|
|
|
|
double agg_dir = 0; int agg_n = 0;
|
|
for(int h = 0; h < P; h++)
|
|
{
|
|
int n = st[h].n;
|
|
double diracc = (n > 0) ? (double)st[h].dir_hit / n : 0.0;
|
|
double k_rmse = MathSqrt(st[h].k_sq / rows);
|
|
double rw_rmse= MathSqrt(st[h].rw_sq / rows);
|
|
double k_mae = st[h].k_abs / rows;
|
|
double rw_mae = st[h].rw_abs / rows;
|
|
//--- skill ratio < 1 means Kronos beats random-walk on that error metric
|
|
double rmse_skill = (rw_rmse > 0) ? k_rmse / rw_rmse : 0.0;
|
|
double mae_skill = (rw_mae > 0) ? k_mae / rw_mae : 0.0;
|
|
|
|
PrintFormat("%2d| %6.2f%% | %.6f %.6f %5.3f | %.6f %.6f %5.3f",
|
|
h + 1, diracc * 100.0, k_rmse, rw_rmse, rmse_skill,
|
|
k_mae, rw_mae, mae_skill);
|
|
agg_dir += st[h].dir_hit; agg_n += n;
|
|
}
|
|
Print("--+----------+----------------------------+---------------------------");
|
|
PrintFormat("Overall directional accuracy across all horizons: %.2f%% (n=%d)",
|
|
(agg_n > 0 ? 100.0 * agg_dir / agg_n : 0.0), agg_n);
|
|
Print("Reading: DirAcc > 50%% = some directional skill; error skill < 1.000 =");
|
|
Print("Kronos beats the random-walk (no-change) baseline on that horizon.");
|
|
}
|
|
//+------------------------------------------------------------------+
|