EntropyPooling/Experts/EP/EP_Sizer.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

386 lines
15 KiB
MQL5

//+------------------------------------------------------------------+
//| EP_Sizer.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 "Holds a basket at a fixed daily expected-shortfall budget. The book"
#property description "is yours; its size comes from EP_ViewCost's next-bar forecast, by"
#property description "rolling window, conditioned history, or conditioned history plus views."
#include <Trade\Trade.mqh>
#include <EntropyPooling\Posterior.mqh>
//--- which forecast sets the size
enum ENUM_EP_SIZING
{
EP_SIZING_WINDOW, // rolling window
EP_SIZING_CONDITIONED, // decay prior conditioned on the state
EP_SIZING_VIEWS // conditioned, with the views pooled in
};
input group "Book"
input string InpSymbols = "EURUSD,GBPUSD,AUDUSD,USDJPY,USDCHF"; // basket
input string InpWeights = "0.2,0.2,0.2,-0.2,-0.2"; // notional share per symbol, sign = side
input ENUM_TIMEFRAMES InpTimeframe = PERIOD_D1; // one scenario per bar of this size
input int InpBars = 5000; // bars loaded per symbol
input group "Risk"
input ENUM_EP_SIZING InpSizing = EP_SIZING_CONDITIONED; // forecast that sets the size
input double InpBudget = 1.0; // ES budget per bar, percent of equity
input double InpMaxLeverage = 5.0; // gross notional cap, multiple of equity
input double InpBand = 0.10; // leave a leg alone within this share of target
input ulong InpMagic = 20260915; // magic number
input group "Probabilities"
input int InpWindow = 250; // rolling window, bars
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 of the ES
input group "Views"
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
CTrade g_trade;
string g_symbols[];
double g_weights[];
int g_ind =INVALID_HANDLE;
datetime g_last_bar =0;
bool g_pending =false; // new bar seen, size not set yet
bool g_described =false;
//--- one row per rebalance, kept for the end-of-test summary
double g_ret[]; // equity change over the bar just ended
datetime g_when[]; // that bar's open time
double g_lev[]; // gross leverage set at the start of that bar
double g_eq_prev =0.0;
datetime g_bar_prev =0;
//+------------------------------------------------------------------+
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;
}
//+------------------------------------------------------------------+
//| Read the basket and open EP_ViewCost, panel off, two bars. |
//+------------------------------------------------------------------+
int OnInit(void)
{
string wtext[];
const int n=SplitList(InpSymbols,g_symbols);
if(n<2 || SplitList(InpWeights,wtext)!=n || InpBudget<=0.0 || InpMaxLeverage<=0.0)
{
Print("EP_Sizer: give at least two symbols, one weight each, and a positive budget");
return INIT_PARAMETERS_INCORRECT;
}
ArrayResize(g_weights,n);
for(int i=0;i<n;i++)
{
g_weights[i]=StringToDouble(wtext[i]);
if(!SymbolSelect(g_symbols[i],true))
{
PrintFormat("EP_Sizer: symbol %s is not available",g_symbols[i]);
return INIT_FAILED;
}
}
g_ind=iCustom(g_symbols[0],InpTimeframe,"EP\\EP_ViewCost",
"",InpSymbols,InpWeights,InpBars,2,
"",InpWindow,InpHalfLife,InpStateWindow,InpMinEns,InpLevel,
"",(InpSizing==EP_SIZING_VIEWS),InpVolSymbol,InpVolScale,InpTailSymbol,InpTailMove,
InpTailProb,InpCorrA,InpCorrB,InpCorr,
"",false);
if(g_ind==INVALID_HANDLE)
{
Print("EP_Sizer: EP_ViewCost could not be loaded");
return INIT_FAILED;
}
g_trade.SetExpertMagicNumber(InpMagic);
PrintFormat("EP_Sizer: %s sizing, ES budget %.2f%% at the %.1f%% tail, leverage cap %.1f, book %s = %s",
EnumToString(InpSizing),InpBudget,100.0*InpLevel,InpMaxLeverage,InpSymbols,InpWeights);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_ind!=INVALID_HANDLE)
IndicatorRelease(g_ind);
}
//+------------------------------------------------------------------+
//| One lot of a symbol in account currency, through its base. |
//+------------------------------------------------------------------+
double LotNotional(const string symbol)
{
const string acc =AccountInfoString(ACCOUNT_CURRENCY);
const string base=SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE);
const double contract=SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
if(base==acc)
return contract;
if(SymbolInfoString(symbol,SYMBOL_CURRENCY_PROFIT)==acc)
return contract*SymbolInfoDouble(symbol,SYMBOL_BID);
double bid=0.0;
if(SymbolSelect(base+acc,true) && (bid=SymbolInfoDouble(base+acc,SYMBOL_BID))>0.0)
return contract*bid;
if(SymbolSelect(acc+base,true) && (bid=SymbolInfoDouble(acc+base,SYMBOL_BID))>0.0)
return contract/bid;
return 0.0;
}
//+------------------------------------------------------------------+
//| Net lots this EA holds on a symbol, long positive. |
//+------------------------------------------------------------------+
double NetLots(const string symbol)
{
double net=0.0;
for(int i=PositionsTotal()-1;i>=0;i--)
{
if(PositionGetSymbol(i)!=symbol || (ulong)PositionGetInteger(POSITION_MAGIC)!=InpMagic)
continue;
const double v=PositionGetDouble(POSITION_VOLUME);
net+=(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ? v : -v);
}
return net;
}
//+------------------------------------------------------------------+
//| Move one leg to its target, closing opposite positions first. |
//+------------------------------------------------------------------+
bool SetLeg(const string symbol,const double target)
{
const double step=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP);
const double vmin=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN);
const double vmax=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MAX);
const double net =NetLots(symbol);
double diff=target-net;
if(MathAbs(diff)<=InpBand*MathMax(MathAbs(target),MathAbs(net)) || MathAbs(diff)<vmin*0.5)
return true;
diff=(diff>0.0 ? 1.0 : -1.0)*MathFloor(MathAbs(diff)/step+1e-9)*step;
if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
{
for(int i=PositionsTotal()-1;i>=0 && MathAbs(diff)>=vmin;i--)
{
if(PositionGetSymbol(i)!=symbol || (ulong)PositionGetInteger(POSITION_MAGIC)!=InpMagic)
continue;
const bool long_pos=(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY);
if(long_pos==(diff>0.0))
continue;
const ulong ticket=(ulong)PositionGetInteger(POSITION_TICKET);
const double v =PositionGetDouble(POSITION_VOLUME);
const double cut =MathMin(v,MathAbs(diff));
if(!(cut>=v-step*0.5 ? g_trade.PositionClose(ticket) : g_trade.PositionClosePartial(ticket,cut)))
return false;
diff+=(diff>0.0 ? -cut : cut);
}
}
double left=MathAbs(diff);
while(left>=vmin)
{
const double v=MathMin(left,vmax);
const bool ok=(diff>0.0 ? g_trade.Buy(v,symbol) : g_trade.Sell(v,symbol));
if(!ok)
{
if(g_trade.ResultRetcode()!=TRADE_RETCODE_MARKET_CLOSED)
PrintFormat("EP_Sizer: %s %.2f lots on %s failed, %s",(diff>0.0 ? "buy" : "sell"),v,symbol,
g_trade.ResultRetcodeDescription());
return false;
}
left-=v;
}
return true;
}
//+------------------------------------------------------------------+
//| The forming bar's ES under the chosen sizing, as a fraction. |
//+------------------------------------------------------------------+
bool ForecastEs(double &es,double &ens)
{
double v[1];
const int buf=(InpSizing==EP_SIZING_WINDOW ? EP_BUF_ES_WINDOW :
(InpSizing==EP_SIZING_VIEWS ? EP_BUF_ES_VIEWS : EP_BUF_ES));
if(CopyBuffer(g_ind,buf,0,1,v)!=1)
return false;
if(v[0]==EMPTY_VALUE && InpSizing==EP_SIZING_VIEWS)
{
if(CopyBuffer(g_ind,EP_BUF_ES,0,1,v)!=1)
return false;
Print("EP_Sizer: the views were not met on this bar; sizing on the conditioned forecast");
}
if(v[0]==EMPTY_VALUE || v[0]>=0.0)
return false;
es=v[0]/100.0;
double e[1];
ens=(CopyBuffer(g_ind,EP_BUF_ENS,0,1,e)==1 ? e[0] : 0.0);
return true;
}
//+------------------------------------------------------------------+
//| Size the book so its forecast ES equals the budget, then trade. |
//+------------------------------------------------------------------+
bool Rebalance(void)
{
double es,ens;
if(!ForecastEs(es,ens))
return false;
const double equity=AccountInfoDouble(ACCOUNT_EQUITY);
double gross_w=0.0;
for(int i=0;i<ArraySize(g_weights);i++)
gross_w+=MathAbs(g_weights[i]);
double scale=InpBudget/100.0*equity/(-es);
if(scale*gross_w>InpMaxLeverage*equity)
scale=InpMaxLeverage*equity/gross_w;
string lots="";
bool done=true;
for(int i=0;i<ArraySize(g_symbols);i++)
{
const double unit=LotNotional(g_symbols[i]);
if(unit<=0.0)
{
PrintFormat("EP_Sizer: cannot value a lot of %s in %s",g_symbols[i],AccountInfoString(ACCOUNT_CURRENCY));
continue;
}
const double target=scale*g_weights[i]/unit;
if(!SetLeg(g_symbols[i],target))
done=false;
lots+=StringFormat(" %s %+.2f",g_symbols[i],target);
}
//--- a closed market or a refused order: try again on the next tick
if(!done)
return false;
if(g_eq_prev>0.0)
{
const int n=ArraySize(g_ret);
ArrayResize(g_ret,n+1,4096);
ArrayResize(g_when,n+1,4096);
g_ret[n] =equity/g_eq_prev-1.0;
g_when[n]=g_bar_prev;
}
const int m=ArraySize(g_lev);
ArrayResize(g_lev,m+1,4096);
g_lev[m]=scale*gross_w/equity;
g_eq_prev =equity;
g_bar_prev=g_last_bar;
if(!g_described)
{
g_described=true;
PrintFormat("EP_Sizer: equity %.2f, forecast ES %.3f%% from %.0f scenarios, gross %.2fx, lots%s",
equity,100.0*es,ens,scale*gross_w/equity,lots);
}
return true;
}
//+------------------------------------------------------------------+
void OnTick(void)
{
const datetime bar=iTime(g_symbols[0],InpTimeframe,0);
if(bar==0)
return;
if(bar!=g_last_bar)
{
g_last_bar=bar;
g_pending =true;
}
if(g_pending && Rebalance())
g_pending=false;
}
//+------------------------------------------------------------------+
//| Realised ES over returns [a,b] and the count beyond the budget. |
//+------------------------------------------------------------------+
double RealisedEs(const int a,const int b,int &beyond)
{
const int n=b-a+1;
double r[];
ArrayResize(r,n);
beyond=0;
for(int i=0;i<n;i++)
{
r[i]=g_ret[a+i];
if(-r[i]>InpBudget/100.0)
beyond++;
}
ArraySort(r);
const int tail=MathMax(1,(int)MathFloor(InpLevel*n));
double worst=0.0;
for(int i=0;i<tail;i++)
worst+=r[i];
return -worst/tail;
}
//+------------------------------------------------------------------+
//| Criterion: -|realised ES / budget - 1|, best when on budget. |
//+------------------------------------------------------------------+
double OnTester(void)
{
const int n=ArraySize(g_ret);
if(n<50)
return 0.0;
double r[];
ArrayCopy(r,g_ret);
ArraySort(r);
const int tail=MathMax(1,(int)MathFloor(InpLevel*n));
double worst=0.0,m1=0.0,m2=0.0,lev=0.0;
int breaches=0;
for(int i=0;i<tail;i++)
worst+=r[i];
for(int i=0;i<n;i++)
{
m1+=g_ret[i];
m2+=g_ret[i]*g_ret[i];
if(-g_ret[i]>InpBudget/100.0)
breaches++;
}
for(int i=0;i<ArraySize(g_lev);i++)
lev+=g_lev[i];
const double es_real=-worst/tail;
const double vol =MathSqrt(MathMax(m2/n-(m1/n)*(m1/n),0.0))*MathSqrt(252.0);
const double ratio =es_real/(InpBudget/100.0);
//--- the same measure one calendar year at a time
int a=0;
while(a<n)
{
MqlDateTime d;
TimeToStruct(g_when[a],d);
int b=a;
MqlDateTime e;
while(b+1<n && TimeToStruct(g_when[b+1],e) && e.year==d.year)
b++;
int beyond;
const double es_year=RealisedEs(a,b,beyond);
double lev_year=0.0;
for(int i=a;i<=b && i<ArraySize(g_lev);i++)
lev_year+=g_lev[i];
PrintFormat("EP_Sizer YEAR %d bars %d realised ES %.3f%% ratio %.3f beyond budget %d gross %.2fx",
d.year,b-a+1,100.0*es_year,es_year/(InpBudget/100.0),beyond,lev_year/(b-a+1));
a=b+1;
}
PrintFormat("EP_Sizer RESULT %s bars %d realised ES %.3f%% ratio %.3f losses beyond budget %.2f%% "
"vol %.2f%% worst %.2f%% avg gross %.2fx profit %.2f dd %.2f%% trades %.0f",
EnumToString(InpSizing),n,100.0*es_real,ratio,100.0*breaches/n,100.0*vol,100.0*r[0],
lev/MathMax(ArraySize(g_lev),1),TesterStatistics(STAT_PROFIT),
TesterStatistics(STAT_EQUITY_DDREL_PERCENT),TesterStatistics(STAT_TRADES));
return -MathAbs(ratio-1.0);
}
//+------------------------------------------------------------------+