//+------------------------------------------------------------------+ //| VtCore.mqh | //| Copyright 2026, Allan Niñal | //| https://www.allanninal.dev/mql5 | //+------------------------------------------------------------------+ //| Vitals engine. Pure computation on a deal list: no terminal | //| calls, so tests/reference.py (its Python twin) can be compared | //| with it number for number (tests/check_core.py). | //| Trade = deals grouped by position id; net = profit + | //| commission + swap + fee of all its deals; it belongs | //| to the day of its closing deal. | //| Cash = balance deals (deposits, withdrawals, charges ...): | //| never a trade, but part of the rebuilt balance. | //| Credit = never a trade, not part of the balance. | //| Gain % = period net / balance at the period start (current | //| balance minus every deal since, cash flow included). | //| Floating drawdown of a day: the journal (VtJournal.mqh) when | //| it has the day, else the peak-to-trough of that day's | //| closed-trade curve, flagged approximate. | //| Stats follow the usual prop-firm formulas (see the README). | //| Day = server calendar day (seconds / 86400); week starts Monday. | //+------------------------------------------------------------------+ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE) #ifndef VTCORE_MQH #define VTCORE_MQH #define VT_T_BUY 0 #define VT_T_SELL 1 #define VT_T_CASH 2 #define VT_T_CREDIT 3 #define VT_IN 0 #define VT_OUT 1 #define VT_INOUT 2 #define VT_OUTBY 3 #define VT_EPS 1e-8 #define VT_DAY 86400 #define VT_MAXROWS 8 struct VtDeal { long time,pos,magic; int type,entry; double volume,price,profit,swap,commission,fee,sl,risk; string symbol,comment; }; struct VtTrade { long pos,magic; string symbol,comment; int dir; // 0 buy, 1 sell (the opening side) long open_time,close_time; double volume,open_price,close_price,sl,net,comm,swap,fee,gross,risk,lo,hi; }; struct VtOpenPos { long magic; string symbol,comment; int dir; long open_time,pos; double price,sl,risk,vol,tvol,net,comm,swap,fee,gross,close_price; }; struct VtDay { long day; int n,w,l; double net,lots,cum,fb,fbp,c,pk,bal; }; struct VtRow { int kind; // 0 one day, 1 week, 2 month, 3 year long day; // first calendar day int n,w,l,gain_ok,dd_ok,dd_approx; double net,lots,gain,dd_money,dd_pct; }; struct VtStats { int n,w,l,pf_ok,sharpe_ok,best_ok; double gp,gl,winrate,avgwin,avgloss,rr,expct,pf,comm,swap,fee,lots; double maxdd_money,maxdd_pct,rel_pct,rel_money,sharpe; long maxdd_time,rel_time,first_day; double best_pct,best_net,worst_pct,worst_net; long best_day,worst_day; }; //--- the pip size table, verbatim from SetPipMultiplier() of trade-info/Vitals.mq4 (MT5 digits instead of MarketInfo) int VtPipMult(const string sym,const int digit) { if(digit==5 || (digit==3 && StringFind(sym,"JPY")>-1) || (digit==2 && StringFind(sym,"XAU")>-1) || (digit==2 && StringFind(sym,"GOLD")>-1) || (digit==3 && StringFind(sym,"XAG")>-1) || (digit==3 && StringFind(sym,"SILVER")>-1) || (digit==1)) return(10); if(digit==6 || (digit==4 && StringFind(sym,"JPY")>-1) || (digit==3 && StringFind(sym,"XAU")>-1) || (digit==3 && StringFind(sym,"GOLD")>-1) || (digit==4 && StringFind(sym,"XAG")>-1) || (digit==4 && StringFind(sym,"SILVER")>-1) || (digit==2)) return(100); return(1); } //--- calendar of the server clock (whole seconds since 1970, no zones): days <-> y/m/d void VtCivil(long z,int &y,int &m,int &d) { z+=719468; long era=(z>=0 ? z : z-146096)/146097; long doe=z-era*146097; long yoe=(doe-doe/1460+doe/36524-doe/146096)/365; long yy=yoe+era*400; long doy=doe-(365*yoe+yoe/4-yoe/100); long mp=(5*doy+2)/153; d=(int)(doy-(153*mp+2)/5+1); m=(int)(mp<10 ? mp+3 : mp-9); y=(int)(yy+(m<=2 ? 1 : 0)); } long VtDaysFromCivil(int y,const int m,const int d) { if(m<=2) y--; long era=(y>=0 ? y : y-399)/400; long yoe=y-era*400; long doy=(153*(m>2 ? m-3 : m+9)+2)/5+d-1; long doe=yoe*365+yoe/4-yoe/100+doy; return(era*146097+doe-719468); } int VtWeekday(const long day) { return((int)((day+3)%7)); } // Monday = 0 class CVtEngine { public: //--- filters (apply to trades and positions; the balance never is filtered) string f_symbol; long f_magic; string f_comment; // lower case long f_start; //--- the book VtOpenPos op[]; int nop,hint; VtTrade tr[]; int ntr; long ct[]; // time of every balance-affecting deal double cc[]; // running sum of their pnl int ncc; //--- the report VtRow rows[VT_MAXROWS]; int nrows; VtStats st; double s0; VtDay days[]; int ndays; double avg_day,avg_month; int avg_day_ok,avg_month_ok; int njournal; //--- open positions int o_n[3]; // long, short, total double o_lots[3],o_pl[3],o_pips[3]; double o_risk,o_risk_pct; int o_nostop,o_nex; string o_nostop_first; string ex_name[]; double ex_net[]; CVtEngine() { Filter("",-1,"",0); Reset(); } void Filter(const string symbol,const long magic,const string comment,const long start) { f_symbol=symbol; f_magic=magic; f_start=start; f_comment=comment; StringToLower(f_comment); } void Reset() { ArrayResize(op,0,64); nop=0; hint=0; ArrayResize(tr,0,1024); ntr=0; ArrayResize(ct,0,4096); ArrayResize(cc,0,4096); ncc=0; ArrayResize(days,0,256); ndays=0; nrows=0; njournal=0; } bool PassPos(const string symbol,const long magic,const string comment) const { if(f_symbol!="" && symbol!=f_symbol) return(false); if(f_magic>=0 && magic!=f_magic) return(false); if(f_comment!="") { string c=comment; StringToLower(c); if(StringFind(c,f_comment)<0) return(false); } return(true); } int OpIdx(const long pos) { if(hint=0; i--) if(op[i].pos==pos) { hint=i; return(i); } return(-1); } int NewOp(const long pos) { int k=nop++; ArrayResize(op,nop,64); op[k].pos=pos; hint=k; return(k); } //--- one deal, in time order; true when it completed a trade that passes the filters bool Add(const VtDeal &d); void SetExt(const double lo,const double hi) { if(ntr>0) { tr[ntr-1].lo=lo; tr[ntr-1].hi=hi; } } double BalBefore(const long t,const double bal_now) const; void Report(const long now,const double bal_now,const long &jd[],const double &jm[],const double &jp[],const int jn); //--- helpers for the views int DayIdx(const long day) const; bool DdOf(const long dn,const long &jd[],const double &jm[],const double &jp[],const int jn, double &money,double &pct,bool &approx) const; }; bool CVtEngine::Add(const VtDeal &d) { double pnl=d.profit+d.swap+d.commission+d.fee; if(d.type==VT_T_CREDIT) return(false); double total=(ncc>0 ? cc[ncc-1] : 0.0)+pnl; ArrayResize(ct,ncc+1,4096); ArrayResize(cc,ncc+1,4096); ct[ncc]=d.time; cc[ncc]=total; ncc++; if(d.type==VT_T_CASH) return(false); int pi=OpIdx(d.pos); if(d.entry==VT_IN) { if(pi<0) { pi=NewOp(d.pos); op[pi].magic=d.magic; op[pi].symbol=d.symbol; op[pi].comment=d.comment; op[pi].dir=(d.type==VT_T_BUY ? 0 : 1); op[pi].open_time=d.time; op[pi].price=d.price; op[pi].sl=d.sl; op[pi].risk=0; op[pi].vol=0; op[pi].tvol=0; op[pi].net=0; op[pi].comm=0; op[pi].swap=0; op[pi].fee=0; op[pi].gross=0; op[pi].close_price=0; } op[pi].vol+=d.volume; op[pi].tvol+=d.volume; op[pi].risk+=d.risk; } else if(pi<0) // the opening deal is not in the history { pi=NewOp(d.pos); op[pi].magic=d.magic; op[pi].symbol=d.symbol; op[pi].comment=d.comment; op[pi].dir=(d.type==VT_T_BUY ? 1 : 0); op[pi].open_time=d.time; op[pi].price=0; op[pi].sl=0; op[pi].risk=0; op[pi].vol=d.volume; op[pi].tvol=d.volume; op[pi].net=0; op[pi].comm=0; op[pi].swap=0; op[pi].fee=0; op[pi].gross=0; op[pi].close_price=0; } op[pi].net+=pnl; op[pi].comm+=d.commission; op[pi].swap+=d.swap; op[pi].fee+=d.fee; op[pi].gross+=d.profit; bool closed=false; double rest=0; if(d.entry==VT_OUT || d.entry==VT_OUTBY) { op[pi].vol-=d.volume; closed=(op[pi].vol<=VT_EPS); } else if(d.entry==VT_INOUT) { rest=d.volume-op[pi].vol; // the part beyond the open volume opens the other way closed=true; } if(!closed) return(false); op[pi].close_price=d.price; bool ok=(PassPos(op[pi].symbol,op[pi].magic,op[pi].comment) && d.time>=f_start); if(ok) { int k=ntr++; ArrayResize(tr,ntr,1024); tr[k].pos=d.pos; tr[k].symbol=op[pi].symbol; tr[k].magic=op[pi].magic; tr[k].comment=op[pi].comment; tr[k].dir=op[pi].dir; tr[k].open_time=op[pi].open_time; tr[k].close_time=d.time; tr[k].volume=op[pi].tvol; tr[k].open_price=op[pi].price; tr[k].close_price=op[pi].close_price; tr[k].sl=op[pi].sl; tr[k].net=op[pi].net; tr[k].comm=op[pi].comm; tr[k].swap=op[pi].swap; tr[k].fee=op[pi].fee; tr[k].gross=op[pi].gross; tr[k].risk=op[pi].risk; tr[k].lo=0; tr[k].hi=0; } if(d.entry==VT_INOUT && rest>VT_EPS) // the rest opens the other way { op[pi].dir=1-op[pi].dir; op[pi].open_time=d.time; op[pi].price=d.price; op[pi].sl=d.sl; op[pi].risk=0; op[pi].vol=rest; op[pi].tvol=rest; op[pi].net=0; op[pi].comm=0; op[pi].swap=0; op[pi].fee=0; op[pi].gross=0; op[pi].close_price=0; } else { nop--; if(pi= t while(lo0 ? cc[lo-1] : 0.0); double total=(ncc>0 ? cc[ncc-1] : 0.0); return(bal_now-(total-before)); } int CVtEngine::DayIdx(const long day) const { int lo=0,hi=ndays; while(lo=0) { money=days[i].fb; pct=days[i].fbp; approx=true; return(true); } money=0; pct=0; approx=false; return(false); } void CVtEngine::Report(const long now,const double bal_now,const long &jd[],const double &jm[],const double &jp[],const int jn) { long nowday=now/VT_DAY; njournal=jn; //--- days with trades (trades are in close order) ndays=0; ArrayResize(days,0,256); for(int i=0; i0) days[x].w++; else if(tr[i].net<0) days[x].l++; days[x].c+=tr[i].net; days[x].pk=MathMax(days[x].pk,days[x].c); days[x].fb=MathMin(days[x].fb,days[x].c-days[x].pk); } s0=bal_now; if(ntr>0) { long mo=tr[0].open_time; for(int i=1; i0 ? 100.0*days[i].fb/days[i].bal : 0.0); cum+=days[i].net; days[i].cum=cum; } //--- period rows long a[VT_MAXROWS]; int kd[VT_MAXROWS]; nrows=0; a[nrows]=nowday; kd[nrows]=0; nrows++; for(int i=ndays-1; i>=0 && nrows<5; i--) if(days[i].day=from && days[i].day0 ? 1 : 0); rows[r].gain=(bal>0 ? 100.0*rows[r].net/bal : 0.0); bool ok=false,ba=false; double bm=0,bp=0; for(long dn=from; dn0) { rv[nr]=100.0*days[i].net/days[i].bal; rn[nr]=days[i].net; rd[nr]=days[i].day; nr++; } avg_day_ok=(nr>0 ? 1 : 0); double sum=0; for(int i=0; i0 ? sum/nr : 0.0); double msum=0; int mcount=0; long mkey=-1; double mnet=0; long mfirst=0; for(int i=0; i<=ndays; i++) { long key=-1; if(i=0) { double b=BalBefore(mfirst*VT_DAY,bal_now); if(b>0) { msum+=100.0*mnet/b; mcount++; } } if(i0 ? 1 : 0); avg_month=(mcount>0 ? msum/mcount : 0.0); //--- statistics st.n=ntr; st.w=0; st.l=0; st.gp=0; st.gl=0; st.comm=0; st.swap=0; st.fee=0; st.lots=0; for(int i=0; i0) { st.w++; st.gp+=tr[i].net; } else if(tr[i].net<0) { st.l++; st.gl+=-tr[i].net; } } st.winrate=(ntr>0 ? 100.0*st.w/ntr : 0.0); st.avgwin=(st.w>0 ? st.gp/st.w : 0.0); st.avgloss=(st.l>0 ? -st.gl/st.l : 0.0); st.rr=((st.w>0 && st.l>0 && st.gl>0) ? (st.gp/st.w)/(st.gl/st.l) : 0.0); st.expct=(ntr>0 ? (st.gp-st.gl)/ntr : 0.0); st.pf_ok=(st.gl>0 ? 1 : 0); st.pf=(st.gl>0 ? st.gp/st.gl : 0.0); double c=s0,peak=s0; st.maxdd_money=0; st.maxdd_pct=0; st.maxdd_time=0; st.rel_pct=0; st.rel_money=0; st.rel_time=0; for(int i=0; ist.maxdd_money) { st.maxdd_money=ddm; st.maxdd_pct=(peak>0 ? 100.0*ddm/peak : 0.0); st.maxdd_time=tr[i].close_time; } double rp=(peak>0 ? 100.0*ddm/peak : 0.0); if(rp>st.rel_pct) { st.rel_pct=rp; st.rel_money=ddm; st.rel_time=tr[i].close_time; } } st.sharpe_ok=0; st.sharpe=0; //--- Sharpe over the last 60 calendar days only (a ~2-month horizon; today and the 59 days before it) { double ss=0; int k=0; for(int i=0; i=nowday-59) { ss+=rv[i]; k++; } if(k>=2) { double mean=ss/k,var=0; for(int i=0; i=nowday-59) var+=(rv[i]-mean)*(rv[i]-mean); var/=(k-1); double sd=MathSqrt(var); st.sharpe_ok=(sd>1e-12 ? 1 : 0); st.sharpe=(sd>1e-12 ? ss/sd : 0.0); } } st.best_ok=(nr>0 ? 1 : 0); st.best_pct=0; st.best_net=0; st.best_day=0; st.worst_pct=0; st.worst_net=0; st.worst_day=0; if(nr>0) { int bi=0,wi=0; for(int i=0; irv[bi]) bi=i; if(rv[i]0 ? tr[0].close_time/VT_DAY : 0); } #endif //+------------------------------------------------------------------+