vitals-account-monitor/mql4/Include/Vitals/VtCore.mqh
573 lines
18 KiB
MQL5
//+------------------------------------------------------------------+
//| 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<nop && op[hint].pos==pos)
return(hint);
for(int i=nop-1; i>=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<nop)
op[pi]=op[nop];
hint=0;
}
return(ok);
}
double CVtEngine::BalBefore(const long t,const double bal_now) const
{
int lo=0,hi=ncc; // first index with ct >= t
while(lo<hi)
{
int mid=(lo+hi)/2;
if(ct[mid]<t)
lo=mid+1;
else
hi=mid;
}
double before=(lo>0 ? 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<hi)
{
int mid=(lo+hi)/2;
if(days[mid].day<day)
lo=mid+1;
else
hi=mid;
}
return(lo<ndays && days[lo].day==day ? lo : -1);
}
//--- the floating drawdown of one calendar day: the journal's, else the closed-trade fallback (approx)
bool CVtEngine::DdOf(const long dn,const long &jd[],const double &jm[],const double &jp[],const int jn,
double &money,double &pct,bool &approx) const
{
int lo=0,hi=jn;
while(lo<hi)
{
int mid=(lo+hi)/2;
if(jd[mid]<dn)
lo=mid+1;
else
hi=mid;
}
if(lo<jn && jd[lo]==dn)
{
money=MathMin(jm[lo],0.0); pct=MathMin(jp[lo],0.0); approx=false;
return(true);
}
int i=DayIdx(dn);
if(i>=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; i<ntr; i++)
{
long dn=tr[i].close_time/VT_DAY;
if(ndays==0 || days[ndays-1].day!=dn)
{
ArrayResize(days,ndays+1,256);
int k=ndays++;
days[k].day=dn; days[k].n=0; days[k].w=0; days[k].l=0; days[k].net=0; days[k].lots=0; days[k].cum=0;
days[k].fb=0; days[k].fbp=0; days[k].c=0; days[k].pk=0; days[k].bal=0;
}
int x=ndays-1;
days[x].n++;
days[x].net+=tr[i].net;
days[x].lots+=tr[i].volume;
if(tr[i].net>0)
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; i<ntr; i++)
mo=MathMin(mo,tr[i].open_time);
s0=BalBefore(mo,bal_now);
}
double cum=s0;
for(int i=0; i<ndays; i++)
{
days[i].bal=BalBefore(days[i].day*VT_DAY,bal_now);
days[i].fbp=(days[i].bal>0 ? 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<nowday)
{ a[nrows]=days[i].day; kd[nrows]=0; nrows++; }
int y,m,dd;
VtCivil(nowday,y,m,dd);
a[nrows]=nowday-VtWeekday(nowday); kd[nrows]=1; nrows++;
a[nrows]=VtDaysFromCivil(y,m,1); kd[nrows]=2; nrows++;
a[nrows]=VtDaysFromCivil(y,1,1); kd[nrows]=3; nrows++;
for(int r=0; r<nrows; r++)
{
long from=a[r],to=(kd[r]==0 ? a[r]+1 : nowday+1);
rows[r].kind=kd[r]; rows[r].day=from;
rows[r].n=0; rows[r].w=0; rows[r].l=0; rows[r].net=0; rows[r].lots=0;
for(int i=0; i<ndays; i++)
if(days[i].day>=from && days[i].day<to)
{
rows[r].n+=days[i].n; rows[r].w+=days[i].w; rows[r].l+=days[i].l;
rows[r].net+=days[i].net; rows[r].lots+=days[i].lots;
}
double bal=BalBefore(from*VT_DAY,bal_now);
rows[r].gain_ok=(bal>0 ? 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; dn<to; dn++)
{
double mny,pc;
bool ap;
if(DdOf(dn,jd,jm,jp,jn,mny,pc,ap) && (!ok || pc<bp))
{ ok=true; bm=mny; bp=pc; ba=ap; }
}
rows[r].dd_ok=(ok ? 1 : 0); rows[r].dd_money=bm; rows[r].dd_pct=bp; rows[r].dd_approx=(ba ? 1 : 0);
}
//--- daily returns, averages
double rv[],rn[];
long rd[];
int nr=0;
ArrayResize(rv,ndays); ArrayResize(rn,ndays); ArrayResize(rd,ndays);
for(int i=0; i<ndays; i++)
if(days[i].bal>0)
{ 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; i<nr; i++)
sum+=rv[i];
avg_day=(nr>0 ? 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<ndays)
{
int yy,mm,d2;
VtCivil(days[i].day,yy,mm,d2);
key=(long)yy*12+mm;
}
if(i==ndays || key!=mkey)
{
if(mkey>=0)
{
double b=BalBefore(mfirst*VT_DAY,bal_now);
if(b>0)
{ msum+=100.0*mnet/b; mcount++; }
}
if(i<ndays)
{
int yy,mm,d2;
VtCivil(days[i].day,yy,mm,d2);
mkey=key; mnet=0; mfirst=VtDaysFromCivil(yy,mm,1);
}
}
if(i<ndays)
mnet+=days[i].net;
}
avg_month_ok=(mcount>0 ? 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; i<ntr; i++)
{
st.lots+=tr[i].volume; st.comm+=tr[i].comm; st.swap+=tr[i].swap; st.fee+=tr[i].fee;
if(tr[i].net>0)
{ 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; i<ntr; i++)
{
c+=tr[i].net;
peak=MathMax(peak,c);
double ddm=peak-c;
if(ddm>st.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<nr; i++)
if(rd[i]>=nowday-59)
{ ss+=rv[i]; k++; }
if(k>=2)
{
double mean=ss/k,var=0;
for(int i=0; i<nr; i++)
if(rd[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; i<nr; i++)
{
if(rv[i]>rv[bi]) bi=i;
if(rv[i]<rv[wi]) wi=i;
}
st.best_pct=rv[bi]; st.best_net=rn[bi]; st.best_day=rd[bi];
st.worst_pct=rv[wi]; st.worst_net=rn[wi]; st.worst_day=rd[wi];
}
st.first_day=(ntr>0 ? tr[0].close_time/VT_DAY : 0);
}
#endif
//+------------------------------------------------------------------+