EntropyPooling/Indicators/EP/EP_ViewCost.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

616 lignes
22 Kio
MQL5

//+------------------------------------------------------------------+
//| EP_ViewCost.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 description "A basket's next-bar risk from its whole history, tilted so that the"
#property description "average volatility state matches today's. Each bar is forecast from"
#property description "the bars before it; red bars broke the forecast VaR. The panel adds"
#property description "the views to the forming bar and shows what they cost in scenarios."
#property indicator_separate_window
#property indicator_buffers 9
#property indicator_plots 4
#property indicator_label1 "Book return"
#property indicator_type1 DRAW_COLOR_HISTOGRAM
#property indicator_color1 clrSilver,clrTomato
#property indicator_width1 2
#property indicator_label2 "VaR"
#property indicator_type2 DRAW_LINE
#property indicator_color2 clrSteelBlue
#property indicator_style2 STYLE_DOT
#property indicator_label3 "ES"
#property indicator_type3 DRAW_COLOR_LINE
#property indicator_color3 clrDodgerBlue,clrOrange
#property indicator_width3 2
#property indicator_label4 "ES rolling window"
#property indicator_type4 DRAW_LINE
#property indicator_color4 clrGray
#property indicator_style4 STYLE_DASH
#include <Canvas\Canvas.mqh>
#include <EntropyPooling\Scenarios.mqh>
#include <EntropyPooling\Probabilities.mqh>
#include <EntropyPooling\MinRelEntropy.mqh>
#include <EntropyPooling\Views.mqh>
#include <EntropyPooling\Posterior.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; // bars loaded per symbol
input int InpHistory = 750; // chart bars to forecast
input group "Probabilities"
input int InpWindow = 250; // rolling window drawn for comparison
input double InpHalfLife = 250.0; // exponential decay half-life, bars
input int InpStateWindow = 20; // trailing volatility of the book, bars
input double InpMinEns = 250; // fewest effective scenarios conditioning may leave
input double InpLevel = 0.025; // tail level for VaR and ES
input group "Views on the forming bar"
input bool InpUseViews = true; // pool the views into the panel
input string InpVolSymbol = "USDJPY"; // volatility view on
input double InpVolScale = 1.5; // times its conditioned volatility
input string InpTailSymbol = "USDCHF"; // tail view on
input double InpTailMove = -0.02; // log return at or below
input double InpTailProb = 0.01; // has at least this probability
input string InpCorrA = "EURUSD"; // correlation view between
input string InpCorrB = "GBPUSD"; // and
input double InpCorr = 0.40; // at most this correlation
input group "Display"
input bool InpPanel = true; // draw the forming-bar panel on the chart
input color InpPanelBack = clrWhite; // panel background
input color InpPanelText = clrBlack; // panel text
double BufReturn[];
double BufReturnColor[];
double BufVar[];
double BufEs[];
double BufEsColor[];
double BufEsWindow[];
double BufEns[];
double BufSize[];
double BufEsViews[];
//--- one row of the panel
struct SPanelRow
{
string name;
double es;
double ens;
};
#define EP_EVENT_SLICE 4711 // custom chart event that resumes the history pass
string g_symbols[];
double g_weights[];
CEpScenarios g_scen;
CEpSolver g_solver;
vector g_book; // book return per scenario row
int g_order[]; // rows by book return, sorted once per reload
vector g_state; // trailing volatility per row
int g_state_col=-1;
bool g_reload =true; // rebuild the scenarios before the next forecast
int g_cursor =-1; // next chart bar to forecast
datetime g_bar_time =0; // open time of the forming bar
datetime g_time[]; // chart bar times, copied from OnCalculate
int g_rates =0;
bool g_panel_due=false;
int g_panel_try=0;
datetime g_tried_bar=0; // bar of the last reload attempt
uint g_tried_ms =0;
bool g_warned =false; // history warning printed once
string g_short; // short name, used to find the indicator on the chart
CCanvas g_canvas;
bool g_canvas_ok=false;
//+------------------------------------------------------------------+
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;
}
//+------------------------------------------------------------------+
//| Bind buffers and read the basket; the work runs in slices. |
//+------------------------------------------------------------------+
int OnInit(void)
{
SetIndexBuffer(EP_BUF_RETURN,BufReturn,INDICATOR_DATA);
SetIndexBuffer(EP_BUF_RETURN_COLOR,BufReturnColor,INDICATOR_COLOR_INDEX);
SetIndexBuffer(EP_BUF_VAR,BufVar,INDICATOR_DATA);
SetIndexBuffer(EP_BUF_ES,BufEs,INDICATOR_DATA);
SetIndexBuffer(EP_BUF_ES_COLOR,BufEsColor,INDICATOR_COLOR_INDEX);
SetIndexBuffer(EP_BUF_ES_WINDOW,BufEsWindow,INDICATOR_DATA);
SetIndexBuffer(EP_BUF_ENS,BufEns,INDICATOR_CALCULATIONS);
SetIndexBuffer(EP_BUF_SIZE,BufSize,INDICATOR_CALCULATIONS);
SetIndexBuffer(EP_BUF_ES_VIEWS,BufEsViews,INDICATOR_CALCULATIONS);
for(int k=0;k<4;k++)
PlotIndexSetDouble(k,PLOT_EMPTY_VALUE,EMPTY_VALUE);
string wtext[];
const int n=SplitList(InpSymbols,g_symbols);
if(n<2 || SplitList(InpWeights,wtext)!=n)
{
Print("EP_ViewCost: give at least two symbols and one weight per symbol");
return INIT_PARAMETERS_INCORRECT;
}
ArrayResize(g_weights,n);
for(int i=0;i<n;i++)
g_weights[i]=StringToDouble(wtext[i]);
if(InpLevel<=0.0 || InpLevel>=0.5 || InpMinEns<1.0 || InpHistory<1)
return INIT_PARAMETERS_INCORRECT;
g_short=StringFormat("EP_ViewCost (%.1f%% ES, ENS >= %.0f)",100.0*InpLevel,InpMinEns);
IndicatorSetString(INDICATOR_SHORTNAME,g_short);
IndicatorSetInteger(INDICATOR_DIGITS,3);
IndicatorSetInteger(INDICATOR_LEVELS,1);
IndicatorSetDouble(INDICATOR_LEVELVALUE,0,0.0);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Create the panel once the indicator is found on this chart. |
//+------------------------------------------------------------------+
bool PanelReady(void)
{
if(g_canvas_ok)
return true;
if(!InpPanel || MQLInfoInteger(MQL_TESTER))
return false;
if(ChartWindowFind(0,g_short)<1)
return false;
g_canvas_ok=g_canvas.CreateBitmapLabel(0,0,"EP_ViewCost_Panel",8,24,400,200,
COLOR_FORMAT_ARGB_NORMALIZE);
if(g_canvas_ok)
g_canvas.FontSet("Consolas",-80);
return g_canvas_ok;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_canvas_ok)
g_canvas.Destroy();
}
//+------------------------------------------------------------------+
//| True once every symbol's history is fully synchronised. |
//+------------------------------------------------------------------+
bool BasketReady(void)
{
for(int i=0;i<ArraySize(g_symbols);i++)
{
if(!SymbolSelect(g_symbols[i],true))
return false;
if(!SeriesInfoInteger(g_symbols[i],_Period,SERIES_SYNCHRONIZED) || Bars(g_symbols[i],_Period)<2)
{
MqlRates probe[];
CopyRates(g_symbols[i],_Period,0,1,probe);
return false;
}
}
return true;
}
//+------------------------------------------------------------------+
//| Joint history of closed bars, the book, and its state column. |
//+------------------------------------------------------------------+
bool BuildScenarios(void)
{
if(!g_scen.LoadReturns(g_symbols,_Period,InpBars))
return false;
const int n=ArraySize(g_symbols);
int cols[];
ArrayResize(cols,n);
for(int i=0;i<n;i++)
cols[i]=i;
vector book;
EpPortfolio(g_scen,cols,g_weights,book);
double values[];
ArrayResize(values,g_scen.Rows());
for(int t=0;t<g_scen.Rows();t++)
values[t]=book[t];
const int book_col=g_scen.AddColumn("Book",EP_COLUMN_RETURN,values,EMPTY_VALUE);
const int state_col=g_scen.AddTrailingVolatility(book_col,InpStateWindow);
if(state_col<0 || g_scen.Rows()<2*InpWindow)
{
if(!g_warned)
PrintFormat("EP_ViewCost: %d joint bars so far, %d needed before the first forecast",
g_scen.Rows(),2*InpWindow);
g_warned=true;
return false;
}
g_scen.Column(book_col,g_book);
g_scen.Column(state_col,g_state);
EpSortOrder(g_book,g_order);
g_state_col=state_col;
return true;
}
//+------------------------------------------------------------------+
//| Row of the bar at t; Rows() past the last row, -1 if not shared. |
//+------------------------------------------------------------------+
int RowOf(const datetime t)
{
const int T=g_scen.Rows();
if(T==0 || t>g_scen.Time(T-1))
return T;
int lo=0,hi=T-1;
while(lo<=hi)
{
const int mid=(lo+hi)/2;
const datetime m=g_scen.Time(mid);
if(m==t)
return mid;
if(m<t)
lo=mid+1;
else
hi=mid-1;
}
return -1;
}
//+------------------------------------------------------------------+
//| Decay prior over the rows before r, zero from r on. |
//+------------------------------------------------------------------+
void DecayBefore(const int r,vector &p)
{
const int T=g_scen.Rows();
const double k=MathLog(2.0)/InpHalfLife;
p.Init(T);
p.Fill(0.0);
for(int i=0;i<r;i++)
p[i]=MathExp(-k*(r-1-i));
EpNormalise(p);
}
//+------------------------------------------------------------------+
//| Forecast row r from earlier rows: VaR, ES, window ES, ENS, bound.|
//+------------------------------------------------------------------+
bool Forecast(const int r,double &var,double &es,double &es_window,double &ens,bool &bound,
vector &p_decay,vector &p_state)
{
const int T=g_scen.Rows();
if(r<InpWindow || r>T)
return false;
const double target=(r<T ? g_state[r] : g_scen.Next(g_state_col));
DecayBefore(r,p_decay);
SEpSolve res;
const double used=EpConditionState(g_scen,p_decay,g_state_col,target,InpMinEns,p_state,res);
bound=(used!=target);
ens =EpEffectiveScenarios(p_state);
if(!EpTailOrdered(g_book,g_order,p_state,InpLevel,var,es))
return false;
vector p_roll;
p_roll.Init(T);
p_roll.Fill(0.0);
for(int i=r-InpWindow;i<r;i++)
p_roll[i]=1.0/InpWindow;
double var_window;
return EpTailOrdered(g_book,g_order,p_roll,InpLevel,var_window,es_window);
}
//+------------------------------------------------------------------+
void ClearBar(const int i)
{
BufReturn[i] =EMPTY_VALUE;
BufReturnColor[i]=0.0;
BufVar[i] =EMPTY_VALUE;
BufEs[i] =EMPTY_VALUE;
BufEsColor[i] =0.0;
BufEsWindow[i] =EMPTY_VALUE;
BufEns[i] =EMPTY_VALUE;
BufSize[i] =EMPTY_VALUE;
BufEsViews[i] =EMPTY_VALUE;
}
//+------------------------------------------------------------------+
void ForecastBar(const int i)
{
ClearBar(i);
const int r=RowOf(g_time[i]);
if(r<0)
return;
double var,es,es_window,ens;
bool bound;
vector p_decay,p_state;
if(!Forecast(r,var,es,es_window,ens,bound,p_decay,p_state))
return;
if(r<g_scen.Rows())
{
BufReturn[i] =100.0*g_book[r];
BufReturnColor[i]=(g_book[r]<=var ? 1.0 : 0.0);
}
BufVar[i] =100.0*var;
BufEs[i] =100.0*es;
BufEsColor[i] =(bound ? 1.0 : 0.0);
BufEsWindow[i]=100.0*es_window;
BufEns[i] =ens;
BufSize[i] =(es<0.0 ? es_window/es : EMPTY_VALUE);
}
//+------------------------------------------------------------------+
//| Forecast bars until the time slice runs out; 0 means no limit. |
//+------------------------------------------------------------------+
void Work(const uint budget_ms)
{
if(g_rates==0)
return;
//--- a failed reload waits for the next bar, or a second on a live chart
if(g_reload)
{
if(g_tried_bar==g_bar_time && (MQLInfoInteger(MQL_TESTER) || GetTickCount()-g_tried_ms<1000))
return;
g_tried_bar=g_bar_time;
g_tried_ms =GetTickCount();
if(!BasketReady() || !BuildScenarios())
return;
g_reload=false;
}
const uint t0=GetTickCount();
while(g_cursor<g_rates)
{
ForecastBar(g_cursor);
g_cursor++;
if(budget_ms>0 && GetTickCount()-t0>=budget_ms)
break;
}
if(g_cursor>=g_rates && g_panel_due)
{
g_panel_due=false;
LiveBar();
}
ChartRedraw();
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,const int prev_calculated,const datetime &time[],
const double &open[],const double &high[],const double &low[],const double &close[],
const long &tick_volume[],const long &volume[],const int &spread[])
{
if(rates_total<2)
return 0;
if(prev_calculated==0)
{
for(int i=0;i<rates_total;i++)
ClearBar(i);
g_cursor =MathMax(0,rates_total-InpHistory);
g_bar_time=0;
}
ArrayCopy(g_time,time,0,0,rates_total);
g_rates=rates_total;
//--- a new bar closes the last one: reload, redo it, forecast the new one
if(time[rates_total-1]!=g_bar_time)
{
g_bar_time =time[rates_total-1];
g_reload =true;
g_panel_due=true;
if(prev_calculated>0)
g_cursor=MathMin(g_cursor,rates_total-2);
}
//--- the tester runs it all at once; on a chart the rest follows in slices
if(MQLInfoInteger(MQL_TESTER))
{
Work(0);
return rates_total;
}
const bool on_chart=(ChartWindowFind(0,g_short)>0);
Work(on_chart ? 100 : 250);
if(on_chart)
NextSlice();
return rates_total;
}
//+------------------------------------------------------------------+
//| Queue the next slice as a chart event; a refresh would restart. |
//+------------------------------------------------------------------+
void NextSlice(void)
{
if(!g_reload && (g_cursor<g_rates || g_panel_due))
EventChartCustom(0,EP_EVENT_SLICE,0,0.0,g_short);
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
if(id!=CHARTEVENT_CUSTOM+EP_EVENT_SLICE || sparam!=g_short)
return;
Work(100);
NextSlice();
}
//+------------------------------------------------------------------+
//| The forming bar: conditioned and view-pooled ES, ENS and panel. |
//+------------------------------------------------------------------+
void LiveBar(void)
{
const int T=g_scen.Rows();
double var,es,es_window,ens;
bool bound;
vector p_decay,p_state;
if(!Forecast(T,var,es,es_window,ens,bound,p_decay,p_state))
return;
double v_decay,es_decay;
EpTailOrdered(g_book,g_order,p_decay,InpLevel,v_decay,es_decay);
vector p_views=p_state;
double es_views=es,ens_views=ens;
bool have_views=false;
if(InpUseViews)
{
const int vol_col =g_scen.Find(InpVolSymbol);
const int tail_col=g_scen.Find(InpTailSymbol);
const int ca=g_scen.Find(InpCorrA),cb=g_scen.Find(InpCorrB);
if(vol_col<0 || tail_col<0 || ca<0 || cb<0)
Print("EP_ViewCost: every view symbol must be in the basket");
else
{
vector z,x;
g_scen.Column(g_state_col,z);
CEpViews views;
views.AddMean(g_state_col,EP_EQUAL,EpMean(z,p_state));
g_scen.Column(vol_col,x);
views.AddVolatility(vol_col,EP_EQUAL,InpVolScale*EpVolatility(x,p_state));
views.AddTail(tail_col,InpTailMove,EP_AT_LEAST,InpTailProb);
views.AddCorrelation(ca,cb,EP_AT_MOST,InpCorr);
for(int k=0;k<2;k++)
{
const int col=(k==0 ? ca : cb);
if(col==vol_col)
continue;
g_scen.Column(col,x);
views.AddVolatility(col,EP_EQUAL,EpVolatility(x,p_state));
}
SEpSolve rv;
if(views.Pool(g_scen,p_decay,g_solver,p_views,rv))
{
double v_views;
EpTailOrdered(g_book,g_order,p_views,InpLevel,v_views,es_views);
ens_views =rv.ens;
have_views=true;
}
else
{
p_views=p_state;
Print("EP_ViewCost: the views cannot be met by reweighting these scenarios");
}
}
}
BufEsViews[g_rates-1]=(have_views ? 100.0*es_views : EMPTY_VALUE);
if(!PanelReady())
{
if(InpPanel && ++g_panel_try<50)
g_panel_due=true;
return;
}
SPanelRow rows[4];
rows[0].name="rolling";
rows[0].es =es_window;
rows[0].ens =InpWindow;
rows[1].name="decay";
rows[1].es =es_decay;
rows[1].ens =EpEffectiveScenarios(p_decay);
rows[2].name="conditioned";
rows[2].es =es;
rows[2].ens =ens;
rows[3].name="+ views";
rows[3].es =es_views;
rows[3].ens =ens_views;
DrawPanel(rows,(have_views ? 4 : 3),p_state,p_views,have_views,bound,T);
}
//+------------------------------------------------------------------+
//| Opaque colour between a and b; the canvas does not blend. |
//+------------------------------------------------------------------+
uint Mix(const color a,const color b,const double share)
{
const int r=(int)MathRound((1.0-share)*(a&0xFF)+share*(b&0xFF));
const int g=(int)MathRound((1.0-share)*((a>>8)&0xFF)+share*((b>>8)&0xFF));
const int u=(int)MathRound((1.0-share)*((a>>16)&0xFF)+share*((b>>16)&0xFF));
return ColorToARGB((color)(r|(g<<8)|(u<<16)),255);
}
//+------------------------------------------------------------------+
//| Panel: ES, size and ENS per vector, and the book's distribution. |
//+------------------------------------------------------------------+
void DrawPanel(const SPanelRow &rows[],const int n,const vector &p_state,const vector &p_views,
const bool have_views,const bool bound,const int T)
{
const color bg =InpPanelBack;
const color fg =InpPanelText;
const uint ink =ColorToARGB(fg,255);
const uint dim =Mix(bg,fg,0.55);
const uint grid=Mix(bg,fg,0.15);
const uint blue=ColorToARGB(clrRoyalBlue,255);
const uint orng=ColorToARGB(clrDarkOrange,255);
int cw=0,ch=0;
g_canvas.TextSize("0",cw,ch);
const int pad=cw,lh=ch+4;
const int gx=pad+30*cw,gw=8*cw;
const int hx=gx+gw+7*cw,hw=26*cw,bins=26;
const int W=hx+hw+pad;
const int H=pad+(n+3)*lh+pad;
if(g_canvas.Width()!=W || g_canvas.Height()!=H)
g_canvas.Resize(W,H);
g_canvas.Erase(ColorToARGB(bg,255));
g_canvas.Rectangle(0,0,W-1,H-1,grid);
g_canvas.TextOut(pad,pad,StringFormat("Next bar ES %.1f%%",100.0*InpLevel),ink);
g_canvas.TextOut(pad,pad+lh,StringFormat("%-12s %8s %5s","","ES","size"),dim);
g_canvas.TextOut(gx,pad+lh,"ENS",dim);
for(int i=0;i<n;i++)
{
const int y =pad+(i+2)*lh;
const uint clr=(i==3 ? orng : (i==2 ? blue : ink));
const string size=(rows[i].es<0.0 ? StringFormat("%.2fx",rows[0].es/rows[i].es) : "-");
g_canvas.TextOut(pad,y,StringFormat("%-12s %7.3f%% %5s",rows[i].name,100.0*rows[i].es,size),clr);
const int w=(int)MathRound(gw*MathMin(rows[i].ens/T,1.0));
g_canvas.FillRectangle(gx,y+ch/4,gx+gw,y+ch-ch/4,grid);
g_canvas.FillRectangle(gx,y+ch/4,gx+MathMax(w,1),y+ch-ch/4,(i>=2 ? clr : dim));
g_canvas.TextOut(gx+gw+cw,y,StringFormat("%.0f",rows[i].ens),dim);
}
//--- floor on the gauges; the conditioned row is marked when it binds
const int mark=gx+(int)MathRound(gw*MathMin(InpMinEns/T,1.0));
g_canvas.LineVertical(mark,pad+2*lh,pad+(n+2)*lh,(bound ? orng : ink));
double lo,hi,unused;
vector p_all;
EpUniform(T,p_all);
EpTailOrdered(g_book,g_order,p_all,0.005,lo,unused);
EpTailOrdered(g_book,g_order,p_all,0.995,hi,unused);
const int hy=pad+lh/2,hh=H-hy-lh-pad;
if(hi>lo && hh>10)
{
double hs[],hv[];
ArrayResize(hs,bins);
ArrayResize(hv,bins);
ArrayInitialize(hs,0.0);
ArrayInitialize(hv,0.0);
for(int t=0;t<T;t++)
{
const int b=MathMax(0,MathMin(bins-1,(int)((g_book[t]-lo)/(hi-lo)*bins)));
hs[b]+=p_state[t];
hv[b]+=p_views[t];
}
const double top=MathMax(hs[ArrayMaximum(hs)],hv[ArrayMaximum(hv)]);
const int bw=MathMax(hw/bins,1);
for(int b=0;b<bins;b++)
{
const int x =hx+b*bw;
const int hS=(int)MathRound(hh*hs[b]/top);
const int hV=(int)MathRound(hh*hv[b]/top);
if(hS>0)
g_canvas.FillRectangle(x,hy+hh-hS,x+bw-2,hy+hh,Mix(bg,clrRoyalBlue,0.45));
if(have_views && hV>0)
g_canvas.Rectangle(x,hy+hh-hV,x+bw-2,hy+hh,orng);
}
g_canvas.LineHorizontal(hx,hx+bins*bw,hy+hh,dim);
const int zero=hx+(int)MathRound(-lo/(hi-lo)*bins*bw);
if(zero>hx && zero<hx+bins*bw)
g_canvas.LineVertical(zero,hy,hy+hh,dim);
g_canvas.TextOut(hx,hy+hh+2,StringFormat("%.2f%%",100.0*lo),dim);
g_canvas.TextOut(hx+bins*bw,hy+hh+2,StringFormat("%.2f%%",100.0*hi),dim,TA_RIGHT);
}
g_canvas.Update();
}
//+------------------------------------------------------------------+