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.
386 lines
15 KiB
MQL5
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);
|
|
}
|
|
//+------------------------------------------------------------------+
|