EntropyPooling/Scripts/EP/EP_WalkForward.mq5
ayantrader 1f3719940c Add entropy pooling library, scripts, indicator and EA
Five-file library (scenarios, priors, dual solver, views, posterior),
closed-form checks, a demo with replay, a walk-forward forecast test,
the EP_ViewCost indicator and the EP_Sizer expected-shortfall sizing EA,
with a README covering the method, the evidence and the layout.
2026-09-15 14:55:58 +05:00

298 行
11 KiB
MQL5

//+------------------------------------------------------------------+
//| EP_WalkForward.mq5 |
//| MMQ — Muhammad Minhas Qamar |
//| www.mql5.com/en/articles/24757 |
//+------------------------------------------------------------------+
#property copyright "MMQ — Muhammad Minhas Qamar"
#property link "https://www.mql5.com/en/articles/24757"
#property version "1.00"
#property strict
#property script_show_inputs
#property description "Walks forward through a basket's history and forecasts the book's next-bar"
#property description "VaR and ES from each probability vector, using only the bars before it."
#property description "Scores coverage, the joint VaR-ES loss, and the variance forecast."
#include <EntropyPooling\Scenarios.mqh>
#include <EntropyPooling\Probabilities.mqh>
#include <EntropyPooling\Views.mqh>
#include <EntropyPooling\Posterior.mqh>
#include <Math\Stat\ChiSquare.mqh>
#include <Math\Stat\Normal.mqh>
input group "Data"
input string InpSymbols = "EURUSD,GBPUSD,AUDUSD,USDJPY,USDCHF"; // basket
input string InpWeights = "0.2,0.2,0.2,-0.2,-0.2"; // book notional per symbol, same order
input int InpBars = 5000; // D1 bars requested per symbol
input group "Test"
input int InpWarmup = 1000; // rows before the first forecast
input double InpLevel = 0.025; // tail level for VaR and ES
input group "Methods"
input int InpWindow = 250; // long rolling window
input int InpShortWindow = 60; // short rolling window
input double InpHalfLife = 250.0; // decay half-life, bars
input int InpStateWindow = 20; // trailing volatility of the book, bars
input double InpKernelBand = 0.5; // kernel bandwidth, share of the state's std
input double InpMinEns = 250; // scenario budget for conditioning
//--- the forecasters, in table order
enum ENUM_METHOD
{
M_ROLLING, // rolling window, long
M_ROLLING_SHORT, // rolling window, short
M_DECAY, // exponential decay
M_EWMA, // RiskMetrics variance with a normal tail
M_KERNEL, // decay times a Gaussian kernel on the state
M_STATE_FREE, // entropy pooling on the state, no budget
M_STATE, // entropy pooling on the state, ENS budget
M_COUNT
};
//--- running scores of one forecaster
struct SScore
{
int hits; // bars at or below the VaR
int degenerate; // bars whose tail held no loss to average
double ens_sum;
double ens_min;
double fz[]; // joint VaR-ES loss per bar
double ql[]; // QLIKE loss of the variance per bar
};
//+------------------------------------------------------------------+
//| Split a comma list, trimming spaces. |
//+------------------------------------------------------------------+
int SplitList(const string text,string &out[])
{
const int n=StringSplit(text,',',out);
for(int i=0;i<n;i++)
{
StringTrimLeft(out[i]);
StringTrimRight(out[i]);
}
return n;
}
//+------------------------------------------------------------------+
//| FZ0 loss of Patton, Ziegel and Chen, joint for VaR and ES. |
//+------------------------------------------------------------------+
double Fz0(const double y,const double var,const double es,const double level)
{
const double hit=(y<=var ? 1.0 : 0.0);
return -hit*(var-y)/(level*es)+var/es+MathLog(-es)-1.0;
}
//+------------------------------------------------------------------+
//| Kupiec's p-value for the VaR hit rate against the level. |
//+------------------------------------------------------------------+
double KupiecP(const int hits,const int n,const double level)
{
const double pi=(double)hits/n;
double lr=-2.0*((n-hits)*MathLog(1.0-level)+hits*MathLog(level));
if(hits>0 && hits<n)
lr+=2.0*((n-hits)*MathLog(1.0-pi)+hits*MathLog(pi));
int err=0;
return 1.0-MathCumulativeDistributionChiSquare(lr,1.0,err);
}
//+------------------------------------------------------------------+
//| Diebold-Mariano of a - b, Newey-West lag 5; negative favours a. |
//+------------------------------------------------------------------+
double DieboldMariano(const double &a[],const double &b[])
{
const int n=ArraySize(a);
double mu=0.0;
for(int i=0;i<n;i++)
mu+=a[i]-b[i];
mu/=n;
double lr=0.0;
for(int i=0;i<n;i++)
lr+=MathPow(a[i]-b[i]-mu,2);
lr/=n;
for(int lag=1;lag<=5;lag++)
{
double g=0.0;
for(int i=lag;i<n;i++)
g+=(a[i]-b[i]-mu)*(a[i-lag]-b[i-lag]-mu);
lr+=2.0*(1.0-lag/6.0)*g/n;
}
return (lr>0.0 ? mu/MathSqrt(lr/n) : 0.0);
}
//+------------------------------------------------------------------+
//| Probability vectors over the rows before r only. |
//+------------------------------------------------------------------+
void RollingBefore(const int T,const int r,const int window,vector &p)
{
p.Init(T);
p.Fill(0.0);
const int a=MathMax(0,r-window);
for(int i=a;i<r;i++)
p[i]=1.0/(r-a);
}
//+------------------------------------------------------------------+
void DecayBefore(const int T,const int r,const double half_life,vector &p)
{
p.Init(T);
p.Fill(0.0);
const double k=MathLog(2.0)/half_life;
for(int i=0;i<r;i++)
p[i]=MathExp(-k*(r-1-i));
EpNormalise(p);
}
//+------------------------------------------------------------------+
void OnStart(void)
{
string symbols[],wtext[];
const int n=SplitList(InpSymbols,symbols);
if(n<2 || SplitList(InpWeights,wtext)!=n)
{
Print("EP_WalkForward: give at least two symbols and one weight per symbol");
return;
}
int cols[];
double weights[];
ArrayResize(cols,n);
ArrayResize(weights,n);
for(int i=0;i<n;i++)
{
cols[i] =i;
weights[i]=StringToDouble(wtext[i]);
}
CEpScenarios scen;
bool loaded=false;
for(int attempt=0;attempt<40 && !loaded;attempt++)
{
loaded=scen.LoadReturns(symbols,PERIOD_D1,InpBars);
if(!loaded)
Sleep(500);
}
if(!loaded)
return;
vector book;
EpPortfolio(scen,cols,weights,book);
double values[];
ArrayResize(values,scen.Rows());
for(int t=0;t<scen.Rows();t++)
values[t]=book[t];
const int book_col =scen.AddColumn("Book",EP_COLUMN_RETURN,values,EMPTY_VALUE);
const int state_col=scen.AddTrailingVolatility(book_col,InpStateWindow);
const int T=scen.Rows();
if(state_col<0 || T<=InpWarmup+100)
{
Print("EP_WalkForward: too little joint history for this warm-up");
return;
}
scen.Column(book_col,book);
vector z;
scen.Column(state_col,z);
int order[];
EpSortOrder(book,order);
const int N=T-InpWarmup;
SScore score[M_COUNT];
for(int m=0;m<M_COUNT;m++)
{
score[m].hits =0;
score[m].degenerate=0;
score[m].ens_sum =0.0;
score[m].ens_min =DBL_MAX;
ArrayResize(score[m].fz,N);
ArrayResize(score[m].ql,N);
}
//--- EWMA variance seeded over the warm-up, as a desk would run it
double ewma=book[0]*book[0];
for(int i=1;i<InpWarmup;i++)
ewma=0.94*ewma+0.06*book[i-1]*book[i-1];
int err=0;
const double zq =MathQuantileNormal(InpLevel,0.0,1.0,err);
const double phi=MathExp(-0.5*zq*zq)/MathSqrt(2.0*M_PI);
PrintFormat("EP_WalkForward: %d rows %s to %s, forecasting the last %d at the %.1f%% tail",
T,TimeToString(scen.Time(0),TIME_DATE),TimeToString(scen.Time(T-1),TIME_DATE),N,100.0*InpLevel);
const uint t0=GetTickCount();
vector p[M_COUNT];
int bound=0; // forecasts where the budget pulled the target back
for(int s=0;s<N;s++)
{
const int r=InpWarmup+s;
const double y=book[r];
RollingBefore(T,r,InpWindow,p[M_ROLLING]);
RollingBefore(T,r,InpShortWindow,p[M_ROLLING_SHORT]);
DecayBefore(T,r,InpHalfLife,p[M_DECAY]);
ewma=0.94*ewma+0.06*book[r-1]*book[r-1];
//--- the state for row r was known before its bar opened
const double sd=EpVolatility(z,p[M_DECAY]);
EpKernel(z,z[r],InpKernelBand*sd,p[M_DECAY],p[M_KERNEL]);
SEpSolve res;
EpConditionState(scen,p[M_DECAY],state_col,z[r],0.0,p[M_STATE_FREE],res);
if(EpConditionState(scen,p[M_DECAY],state_col,z[r],InpMinEns,p[M_STATE],res)!=z[r])
bound++;
for(int m=0;m<M_COUNT;m++)
{
double var,es,vol;
if(m==M_EWMA)
{
vol=MathSqrt(ewma);
var=zq*vol;
es =-vol*phi/InpLevel;
}
else
{
EpTailOrdered(book,order,p[m],InpLevel,var,es);
vol=EpVolatility(book,p[m]);
const double ens=EpEffectiveScenarios(p[m]);
score[m].ens_sum+=ens;
score[m].ens_min =MathMin(score[m].ens_min,ens);
}
if(y<=var)
score[m].hits++;
if(es>=0.0 || vol<=0.0)
{
score[m].degenerate++;
score[m].fz[s]=0.0;
score[m].ql[s]=0.0;
continue;
}
score[m].fz[s]=Fz0(y,var,es,InpLevel);
score[m].ql[s]=MathLog(vol*vol)+y*y/(vol*vol);
}
}
const string names[M_COUNT]={StringFormat("rolling %d",InpWindow),StringFormat("rolling %d",InpShortWindow),
StringFormat("decay %.0f",InpHalfLife),"EWMA 0.94 normal","decay x kernel",
"state, no budget",StringFormat("state, ENS >= %.0f",InpMinEns)};
PrintFormat("EP_WalkForward: %d forecasts in %u ms",N,GetTickCount()-t0);
PrintFormat("%-18s %6s %6s %7s %8s %9s %8s %8s %8s %9s","method","ENS","minENS","hits",
"Kupiec p","FZ0","DM roll","DM decay","DM kern","QLIKE");
for(int m=0;m<M_COUNT;m++)
{
double fz=0.0,ql=0.0;
for(int s=0;s<N;s++)
{
fz+=score[m].fz[s];
ql+=score[m].ql[s];
}
const bool usable=(score[m].degenerate==0);
const string ens =(m==M_EWMA ? "-" : StringFormat("%.0f",score[m].ens_sum/N));
const string ens_lo=(m==M_EWMA ? "-" : StringFormat("%.0f",score[m].ens_min));
PrintFormat("%-18s %6s %6s %6.2f%% %8.3f %9s %8s %8s %8s %9s",names[m],ens,ens_lo,100.0*score[m].hits/N,
KupiecP(score[m].hits,N,InpLevel),
(usable ? StringFormat("%.4f",fz/N) : "n/a"),
(usable ? StringFormat("%.2f",DieboldMariano(score[m].fz,score[M_ROLLING].fz)) : "n/a"),
(usable ? StringFormat("%.2f",DieboldMariano(score[m].fz,score[M_DECAY].fz)) : "n/a"),
(usable ? StringFormat("%.2f",DieboldMariano(score[m].fz,score[M_KERNEL].fz)) : "n/a"),
(usable ? StringFormat("%.4f",ql/N) : "n/a"));
if(!usable)
PrintFormat("%-18s %d of %d forecasts had a non-negative ES, where FZ0 is undefined",
"",score[m].degenerate,N);
}
PrintFormat("EP_WalkForward: the budget pulled the state target back on %d of %d forecasts",bound,N);
}
//+------------------------------------------------------------------+