vitals-account-monitor/mql4/Include/Vitals/VtViews.mqh
Allan Niñal Allan Niñal d54b15105b Alerts on by default; regenerated from the private core
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:54:51 +08:00
400 lines
16 KiB
MQL5
//+------------------------------------------------------------------+
//| VtViews.mqh |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| The model and the three tabs: Overview, History and Stats. Each |
//| Draw* function paints into the card from the body top and |
//| returns the y (logical units) where it ended. |
//+------------------------------------------------------------------+
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
#ifndef VTVIEWS_MQH
#define VTVIEWS_MQH
#include "VtCore.mqh"
#include "VtExposure.mqh"
#include "VtJournal.mqh"
#include "VtLive.mqh"
#include "VtCard.mqh"
#define VT_BODY_X 12
#define VT_BODY_R 448
#define VT_BODY_TOP 64
//--- the model the views read
CVtEngine g_eng;
CVtJournal g_jr;
CVtCard g_card;
VtAcct g_acct;
VtPos g_pos[];
int g_npos=0;
long g_server_now=0;
long g_today_low_time=0; // when today's floating low was set (memory only)
string g_filter_text="";
bool g_demo=false; // synthetic data (tests and screenshots): account text is made up
int g_cal_y=0,g_cal_m=0; // calendar month
string g_note=""; // one-line message (export result ...)
//--- formatting -----------------------------------------------------------------------------------------------------
string VtMinus() { return(ShortToString(0x2212)); }
string VtGroup(const string s) // 1234567.89 -> 1,234,567.89
{
int dot=StringFind(s,".");
string ip=(dot>=0 ? StringSubstr(s,0,dot) : s),fp=(dot>=0 ? StringSubstr(s,dot) : "");
string o="";
int n=StringLen(ip);
for(int i=0; i<n; i++)
{
o+=StringSubstr(ip,i,1);
int left=n-1-i;
if(left>0 && left%3==0)
o+=",";
}
return(o+fp);
}
string VtNum(const double v,const int d=2)
{
string s=VtGroup(DoubleToString(MathAbs(v),d));
return(v<0 && StringToDouble(DoubleToString(MathAbs(v),d))>0 ? VtMinus()+s : s);
}
string VtSgn(const double v,const int d=2)
{
double r=StringToDouble(DoubleToString(MathAbs(v),d));
if(r==0)
return(VtGroup(DoubleToString(0,d)));
return((v<0 ? VtMinus() : "+")+VtGroup(DoubleToString(MathAbs(v),d)));
}
color VtSignClr(const double v,const int d=2)
{
double r=StringToDouble(DoubleToString(MathAbs(v),d));
return(r==0 ? g_card.pal.dim : (v>0 ? g_card.pal.pos : g_card.pal.neg));
}
string VtWd3(const long day)
{
string n[7]={"Mon","Tue","Wed","Thu","Fri","Sat","Sun"};
return(n[VtWeekday(day)]);
}
string VtMon3(const int m)
{
string n[12]={"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
return(n[(m-1+12)%12]);
}
string VtMonFull(const int m)
{
string n[12]={"January","February","March","April","May","June","July","August","September","October","November","December"};
return(n[(m-1+12)%12]);
}
string VtDM(const long day) // "12 Oct"
{
int y,m,d;
VtCivil(day,y,m,d);
return(IntegerToString(d)+" "+VtMon3(m));
}
string VtDMY(const long day) // "5 Jan 2026"
{
int y,m,d;
VtCivil(day,y,m,d);
return(IntegerToString(d)+" "+VtMon3(m)+" "+IntegerToString(y));
}
string VtHM(const long t)
{
return(StringFormat("%02d:%02d",(int)((t%86400)/3600),(int)((t%3600)/60)));
}
string VtMaskLogin(const long login)
{
string s=IntegerToString(login);
int n=StringLen(s);
if(n<7)
return(s);
return(StringSubstr(s,0,4)+ShortToString(0x2022)+ShortToString(0x2022)+ShortToString(0x2022)+StringSubstr(s,n-2));
}
//--- building blocks ------------------------------------------------------------------------------------------------
double VtSec(CVtCard &c,const double y,const string left,const string right="",const color rclr=clrNONE)
{
string u=left;
StringToUpper(u);
c.Txt(VT_BODY_X,y,u,c.pal.label,VTK_SEC);
if(right!="")
c.Txt(VT_BODY_R,y,right,rclr==clrNONE ? c.pal.dim : rclr,VTK_SMALL,1);
return(y+16);
}
void VtKV(CVtCard &c,const double x1,const double x2,const double y,const string k,const string v,const color vc)
{
c.Txt(x1,y,k,c.pal.label,VTK_SANS);
c.Txt(x2,y,v,vc,VTK_MONO,1);
}
//--- OVERVIEW ---------------------------------------------------------------------------------------------------------
double VtDrawOverview(CVtCard &c)
{
double y=VT_BODY_TOP;
VtAcct a=g_acct;
double floating=a.equity-a.balance-a.credit;
//--- hero
c.Txt(VT_BODY_X,y,"EQUITY",c.pal.label,VTK_SEC);
c.Txt(190,y,"BALANCE",c.pal.label,VTK_SEC);
c.Txt(320,y,"FLOATING",c.pal.label,VTK_SEC);
c.Txt(VT_BODY_X,y+12,VtNum(a.equity),c.pal.ink,VTK_BIG);
c.Txt(VT_BODY_X,y+40,a.balance>0 ? DoubleToString(100.0*a.equity/a.balance,2)+"% of balance" : "-",c.pal.label,VTK_MONOS);
c.Txt(190,y+16,VtNum(a.balance),c.pal.ink,VTK_MONO);
c.Txt(320,y+16,VtSgn(floating),VtSignClr(floating),VTK_MONO);
c.Txt(320,y+34,a.balance>0 ? VtSgn(100.0*floating/a.balance)+"%" : "-",VtSignClr(floating),VTK_MONOS);
y+=60;
//--- floating drawdown
string since="";
if(g_jr.n>0)
since="journal since "+VtDM(g_jr.d[0]);
y=VtSec(c,y,"Floating drawdown",since);
double now_pct=(a.balance>0 && floating<0 ? 100.0*floating/a.balance : 0.0);
long today=g_server_now/86400;
double tw_pct=0,tw_money=0;
bool tw_ok=false;
if(g_jr.n>0 && g_jr.d[g_jr.n-1]==today)
{ tw_pct=g_jr.p[g_jr.n-1]; tw_money=g_jr.m[g_jr.n-1]; tw_ok=true; }
double ww_pct=0;
long ww_day=0;
bool ww_ok=false;
for(int i=0; i<g_jr.n; i++)
if(!ww_ok || g_jr.p[i]<ww_pct)
{ ww_pct=g_jr.p[i]; ww_day=g_jr.d[i]; ww_ok=true; }
double mx=MathMax(MathMax(MathAbs(ww_pct),MathAbs(tw_pct)),MathMax(MathAbs(now_pct),0.0001));
double gx1=VT_BODY_X,gx2=VT_BODY_R,gy=y+2,gh=16;
c.Fill(gx1,gy,gx2,gy+gh,c.pal.track);
double wfull=gx2-gx1;
if(tw_ok && tw_pct<0)
c.Fill(gx2-wfull*MathAbs(tw_pct)/mx,gy,gx2,gy+gh,c.pal.neg,90);
if(now_pct<0)
c.Fill(gx2-wfull*MathAbs(now_pct)/mx,gy,gx2,gy+gh,c.pal.neg);
if(tw_ok && tw_pct<0)
c.Fill(gx2-wfull*MathAbs(tw_pct)/mx-1,gy-3,gx2-wfull*MathAbs(tw_pct)/mx+1,gy+gh+3,c.pal.warn);
y=gy+gh+7;
c.Txt(VT_BODY_X,y,"Now",c.pal.label,VTK_SMALL);
c.Txt(VT_BODY_X+30,y,now_pct<0 ? VtSgn(now_pct)+"%" : "0.00%",now_pct<0 ? c.pal.neg : c.pal.dim,VTK_MONOS);
string twt=(tw_ok && tw_pct<0 ? VtSgn(tw_pct)+"%" : "0.00%");
c.Txt(150,y,"Today's worst",c.pal.label,VTK_SMALL);
c.Txt(150+78,y,twt,tw_ok && tw_pct<0 ? c.pal.warn : c.pal.dim,VTK_MONOS);
if(tw_ok && tw_pct<0 && g_today_low_time>0 && g_today_low_time/86400==today)
c.Txt(150+78+c.TextW(twt,VTK_MONOS)+6,y,VtHM(g_today_low_time),c.pal.dim,VTK_MONOS);
string wwt=(ww_ok && ww_pct<0 ? VtSgn(ww_pct)+"%" : "-");
c.Txt(VT_BODY_R,y,(ww_ok && ww_pct<0 ? VtDM(ww_day) : ""),c.pal.dim,VTK_MONOS,1);
int dw=(ww_ok && ww_pct<0 ? c.TextW(VtDM(ww_day),VTK_MONOS)+6 : 0);
c.Txt(VT_BODY_R-dw,y,wwt,ww_ok && ww_pct<0 ? c.pal.neg : c.pal.dim,VTK_MONOS,1);
c.Txt(VT_BODY_R-dw-c.TextW(wwt,VTK_MONOS)-6,y,"Worst day",c.pal.label,VTK_SMALL,1);
y+=20;
//--- margin
y=VtSec(c,y,"Margin");
string lvl=(a.margin>0 ? VtNum(a.level,0)+"%" : "-");
string so=(a.so_mode==VT_SO_PERCENT ? DoubleToString(a.so,0)+"%" : VtNum(a.so));
VtKV(c,VT_BODY_X,206,y,"Used",VtNum(a.margin),c.pal.ink);
VtKV(c,242,VT_BODY_R,y,"Free",VtNum(a.free),c.pal.ink);
y+=17;
VtKV(c,VT_BODY_X,206,y,"Level",lvl,c.pal.ink);
VtKV(c,242,VT_BODY_R,y,"Stop out",so,c.pal.dim);
y+=22;
//--- open positions
string rk="";
color rkc=c.pal.dim;
if(g_eng.o_nostop<g_eng.o_n[2] || g_eng.o_risk!=0)
{
rk="risk at stops "+VtSgn(g_eng.o_risk)+" ("+DoubleToString(MathAbs(g_eng.o_risk_pct),2)+"%)";
rkc=(g_eng.o_risk<0 ? c.pal.neg : c.pal.pos);
}
y=VtSec(c,y,"Open positions",rk,rkc);
double cx[5]={VT_BODY_X,236,284,364,VT_BODY_R};
c.Txt(cx[1],y,"COUNT",c.pal.label,VTK_SEC,1);
c.Txt(cx[2],y,"LOTS",c.pal.label,VTK_SEC,1);
c.Txt(cx[3],y,"P/L",c.pal.label,VTK_SEC,1);
c.Txt(cx[4],y,"PIPS",c.pal.label,VTK_SEC,1);
y+=15;
c.HLine(VT_BODY_X,VT_BODY_R,y,c.pal.rule);
y+=3;
string nm[3]={"Long","Short","Total"};
for(int k=0; k<3; k++)
{
if(k==2)
{ c.HLine(VT_BODY_X,VT_BODY_R,y,c.pal.edge); y+=2; }
c.Txt(cx[0],y,nm[k],k==2 ? c.pal.ink : c.pal.label,VTK_SANS);
c.Txt(cx[1],y,IntegerToString(g_eng.o_n[k]),c.pal.ink,VTK_MONO,1);
c.Txt(cx[2],y,DoubleToString(g_eng.o_lots[k],2),c.pal.ink,VTK_MONO,1);
c.Txt(cx[3],y,VtSgn(g_eng.o_pl[k]),VtSignClr(g_eng.o_pl[k]),VTK_MONO,1);
c.Txt(cx[4],y,VtSgn(g_eng.o_pips[k],1),VtSignClr(g_eng.o_pips[k],1),VTK_MONO,1);
y+=17;
}
if(g_eng.o_nostop>0)
{
y+=3;
string t=(g_eng.o_nostop==1 ? "1 position without a stop" : IntegerToString(g_eng.o_nostop)+" positions without a stop")+
" "+ShortToString(0x00B7)+" "+g_eng.o_nostop_first+(g_eng.o_nostop>1 ? " +"+IntegerToString(g_eng.o_nostop-1) : "");
int tw=c.TextW(t,VTK_SMALL);
c.Box(VT_BODY_X,y,VT_BODY_X+tw+12,y+17,c.pal.warn);
c.Txt(VT_BODY_X+6,y+2,t,c.pal.warn,VTK_SMALL);
y+=22;
}
else
y+=4;
//--- exposure
y=VtSec(c,y,"Exposure by currency","net, "+g_acct.ccy+" thousands");
int rows=MathMin(g_eng.o_nex,6);
if(rows==0)
{
c.Txt(VT_BODY_X,y,"No open positions",c.pal.dim,VTK_SMALL);
y+=18;
}
double big=0;
for(int i=0; i<rows; i++)
big=MathMax(big,MathAbs(g_eng.ex_net[i]));
for(int i=0; i<rows; i++)
{
double v=g_eng.ex_net[i];
c.Txt(VT_BODY_X,y,g_eng.ex_name[i],c.pal.label,VTK_MONO);
double mid=VT_BODY_X+42+(VT_BODY_R-70-VT_BODY_X-42)/2.0,half=(VT_BODY_R-70-VT_BODY_X-42)/2.0;
c.VLine(mid,y,y+15,c.pal.edge);
double bw=(big>0 ? MathAbs(v)/big*half : 0);
if(v<0)
c.Fill(mid-bw,y+3,mid,y+12,c.pal.neg,190);
else
c.Fill(mid,y+3,mid+bw,y+12,c.pal.pos,190);
c.Txt(VT_BODY_R,y,VtSgn(v/1000.0,1)+"k",VtSignClr(v/1000.0,1),VTK_MONO,1);
y+=17;
}
return(y);
}
//--- HISTORY ----------------------------------------------------------------------------------------------------------
string VtRowLabel(const VtRow &r,const long today)
{
int y,m,d;
if(r.kind==0)
{
VtCivil(r.day,y,m,d);
if(r.day==today)
return("Today "+ShortToString(0x00B7)+" "+VtWd3(r.day)+" "+IntegerToString(d));
if(r.day==today-1)
return("Yesterday "+ShortToString(0x00B7)+" "+VtWd3(r.day)+" "+IntegerToString(d));
return(VtWd3(r.day)+" "+IntegerToString(d));
}
VtCivil(today,y,m,d);
if(r.kind==1)
return("Week");
if(r.kind==2)
return(VtMonFull(m));
return(IntegerToString(y));
}
void VtHistRow(CVtCard &c,const double y,const VtRow &r,const long today)
{
c.Txt(VT_BODY_X,y,VtRowLabel(r,today),c.pal.label,VTK_SANS);
string wl=IntegerToString(r.w)+"/"+IntegerToString(r.l);
c.Txt(194,y,r.n>0 ? wl : "",c.pal.dim,VTK_MONOS,1);
int ww=(r.n>0 ? c.TextW(wl,VTK_MONOS)+5 : 0);
c.Txt(194-ww,y,IntegerToString(r.n),c.pal.ink,VTK_MONO,1);
c.Txt(262,y,VtSgn(r.net),VtSignClr(r.net),VTK_MONO,1);
c.Txt(326,y,r.gain_ok ? VtSgn(r.gain)+"%" : "-",r.gain_ok ? VtSignClr(r.gain) : c.pal.dim,VTK_MONO,1);
if(r.dd_ok)
{
bool zero=(StringToDouble(DoubleToString(MathAbs(r.dd_pct),2))==0);
string t=(r.dd_approx ? "~" : "")+(zero ? "0.00%" : VtSgn(r.dd_pct)+"%");
color col=(r.dd_approx || zero ? c.pal.dim : c.pal.neg);
c.Txt(390,y,t,col,VTK_MONO,1);
}
else
c.Txt(390,y,"-",c.pal.dim,VTK_MONO,1);
c.Txt(VT_BODY_R,y,DoubleToString(r.lots,2),c.pal.ink,VTK_MONO,1);
}
double VtDrawHistory(CVtCard &c)
{
double y=VT_BODY_TOP;
long today=g_server_now/86400;
string h[6]={"PERIOD","TRADES W/L","NET","GAIN","WORST DD","LOTS"};
c.Txt(VT_BODY_X,y,h[0],c.pal.label,VTK_SEC);
double hx[6]={0,194,262,326,390,VT_BODY_R};
for(int i=1; i<6; i++)
c.Txt(hx[i],y,h[i],c.pal.label,VTK_SEC,1);
y+=15;
c.HLine(VT_BODY_X,VT_BODY_R,y,c.pal.rule);
y+=4;
int nday=0;
for(int i=0; i<g_eng.nrows; i++)
if(g_eng.rows[i].kind==0)
nday++;
for(int i=0; i<g_eng.nrows; i++)
{
if(g_eng.rows[i].kind!=0 && g_eng.rows[i].kind==1)
{ y+=2; c.HLine(VT_BODY_X,VT_BODY_R,y,c.pal.edge); y+=4; }
VtHistRow(c,y,g_eng.rows[i],today);
y+=18;
}
y+=4;
y=VtSec(c,y,"Averages");
VtKV(c,VT_BODY_X,206,y,"Per trading day",g_eng.avg_day_ok ? VtSgn(g_eng.avg_day)+"%" : "-",g_eng.avg_day_ok ? VtSignClr(g_eng.avg_day) : c.pal.dim);
VtKV(c,242,VT_BODY_R,y,"Per month",g_eng.avg_month_ok ? VtSgn(g_eng.avg_month)+"%" : "-",g_eng.avg_month_ok ? VtSignClr(g_eng.avg_month) : c.pal.dim);
y+=22;
c.Txt(VT_BODY_X,y,"Net includes commission, swap and fees. Worst DD is the day's deepest",c.pal.dim,VTK_SMALL);
c.Txt(VT_BODY_X,y+13,"floating drawdown from the journal; ~ marks days before the journal,",c.pal.dim,VTK_SMALL);
c.Txt(VT_BODY_X,y+26,"taken from closed trades. Week, month and year show their worst day.",c.pal.dim,VTK_SMALL);
return(y+42);
}
//--- STATS ------------------------------------------------------------------------------------------------------------
void VtStatCell(CVtCard &c,const double x,const double y,const string k,const string v,const string sub,const color vc)
{
string u=k;
StringToUpper(u);
c.Txt(x,y,u,c.pal.label,VTK_SEC);
c.Txt(x,y+13,v,vc,VTK_MONO);
c.Txt(x,y+30,sub,c.pal.dim,VTK_MONOS);
}
double VtDrawStats(CVtCard &c)
{
double y=VT_BODY_TOP;
VtStats s=g_eng.st;
string right="";
if(s.n>0)
right="since "+VtDMY(s.first_day)+" "+ShortToString(0x00B7)+" "+DoubleToString(s.lots,2)+" lots";
y=VtSec(c,y,"Account statistics",right);
double col[3]={VT_BODY_X,166,320};
int i=0;
string m=VtMinus();
bool any=(s.n>0);
//--- row 1
VtStatCell(c,col[0],y,"Win rate",any ? DoubleToString(s.winrate,1)+"%" : "-",any ? IntegerToString(s.w)+" of "+IntegerToString(s.n) : "",c.pal.ink);
VtStatCell(c,col[1],y,"Profit factor",s.pf_ok ? DoubleToString(s.pf,2) : (any && s.gp>0 ? "no losses" : "-"),"",c.pal.ink);
VtStatCell(c,col[2],y,"Expectancy",any ? VtSgn(s.expct) : "-",any ? "per trade" : "",any ? VtSignClr(s.expct) : c.pal.dim);
y+=48;
VtStatCell(c,col[0],y,"Avg win",s.w>0 ? VtSgn(s.avgwin) : "-","",s.w>0 ? c.pal.pos : c.pal.dim);
VtStatCell(c,col[1],y,"Avg loss",s.l>0 ? VtSgn(s.avgloss) : "-","",s.l>0 ? c.pal.neg : c.pal.dim);
VtStatCell(c,col[2],y,"Reward/risk",(s.w>0 && s.l>0) ? DoubleToString(s.rr,2) : "-",(s.w>0 && s.l>0) ? "avg win / avg loss" : "",c.pal.ink);
y+=48;
VtStatCell(c,col[0],y,"Max drawdown",s.maxdd_money>0 ? m+DoubleToString(s.maxdd_pct,2)+"%" : (any ? "0.00%" : "-"),
s.maxdd_money>0 ? m+VtNum(s.maxdd_money)+" "+ShortToString(0x00B7)+" "+VtDM(s.maxdd_time/86400) : "",s.maxdd_money>0 ? c.pal.neg : c.pal.dim);
VtStatCell(c,col[1],y,"Relative DD",s.rel_pct>0 ? m+DoubleToString(s.rel_pct,2)+"%" : (any ? "0.00%" : "-"),
s.rel_pct>0 ? m+VtNum(s.rel_money)+" "+ShortToString(0x00B7)+" "+VtDM(s.rel_time/86400) : "",s.rel_pct>0 ? c.pal.neg : c.pal.dim);
VtStatCell(c,col[2],y,"Sharpe "+ShortToString(0x00B7)+" 60 days",s.sharpe_ok ? DoubleToString(s.sharpe,2) : "-",s.sharpe_ok ? "daily returns" : "needs 2+ trading days",c.pal.ink);
y+=48;
VtStatCell(c,col[0],y,"Best day",s.best_ok ? VtSgn(s.best_pct)+"%" : "-",s.best_ok ? VtDM(s.best_day) : "",s.best_ok ? VtSignClr(s.best_pct) : c.pal.dim);
VtStatCell(c,col[1],y,"Worst day",s.best_ok ? VtSgn(s.worst_pct)+"%" : "-",s.best_ok ? VtDM(s.worst_day) : "",s.best_ok ? VtSignClr(s.worst_pct) : c.pal.dim);
double costs=s.comm+s.swap+s.fee;
VtStatCell(c,col[2],y,"Costs",any ? VtSgn(costs) : "-","",any ? VtSignClr(costs) : c.pal.dim);
y+=50;
if(any)
{
c.Txt(VT_BODY_X,y,"commission "+VtSgn(s.comm)+" "+ShortToString(0x00B7)+" swap "+VtSgn(s.swap)+" "+ShortToString(0x00B7)+" fees "+VtSgn(s.fee),
c.pal.dim,VTK_MONOS);
y+=16;
}
else
{
c.Txt(VT_BODY_X,y,"No closed trades in the history yet.",c.pal.dim,VTK_SMALL);
y+=16;
}
c.Txt(VT_BODY_X,y,"Max and relative drawdown: closed-trade balance curve.",c.pal.dim,VTK_SMALL);
c.Txt(VT_BODY_X,y+13,"Sharpe: cumulative return / stdev of daily returns, last 60 days.",c.pal.dim,VTK_SMALL);
return(y+30);
}
#endif
//+------------------------------------------------------------------+