//+------------------------------------------------------------------+ //| 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 #include //--- 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;i0.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)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;iInpMaxLeverage*equity) scale=InpMaxLeverage*equity/gross_w; string lots=""; bool done=true; for(int i=0;i0.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;iInpBudget/100.0) beyond++; } ArraySort(r); const int tail=MathMax(1,(int)MathFloor(InpLevel*n)); double worst=0.0; for(int i=0;iInpBudget/100.0) breaches++; } for(int i=0;i