vitals-account-monitor/mql5/Include/Vitals/VtApp.mqh
463 lines
15 KiB
MQL5
//+------------------------------------------------------------------+
//| VtApp.mqh |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| The program: inputs, events, the 1 s timer, the card chrome. |
//| Read-only: no order is ever placed, changed or closed. |
//+------------------------------------------------------------------+
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
#ifndef VTAPP_MQH
#define VTAPP_MQH
#include "../ChartKit/CkDebug.mqh"
#include "VtViews.mqh"
#define VT_VERSION "1.1" // keep equal to #property version of Vitals
#define VT_NAME "Vitals"
input group "Filters (trades, positions, risk and exposure; account figures are never filtered)"
input bool InpThisSymbol = false; // Only the chart's symbol
input long InpMagic = -1; // Only this magic number (-1 = all)
input string InpComment = ""; // Only comments containing
input string InpStartDate = ""; // Ignore trades closed before (YYYY.MM.DD)
input group "Display"
input ENUM_VT_THEME InpTheme = VT_THEME_AUTO; // Theme
input double InpScale = 1.0; // UI scale (multiplies the screen DPI scale)
input group "Support"
input bool InpDebug = false; // Debug log for bug reports (Experts tab)
const string VT_PREFIX="VITALS_CARD_";
const string VT_TAB_NAMES[3]={"Overview","History","Stats"};
CVtHistory g_hist;
bool g_tester=false;
long g_filter_start=0;
bool g_hist_dirty=true;
int g_report_ver=0;
string g_last_sig="";
ulong g_last_theme_ms=0;
ulong g_note_until=0;
double g_h_tab[8];
bool g_scroll_orig=true,g_mouse_orig=false;
long g_alert_dd_day=0,g_alert_ml_day=0;
ulong g_perf_rebuild_us=0,g_perf_timer_us=0;
int g_perf_timer_n=0;
bool g_shot_done=false;
long g_tester_tick=0;
long g_jr_login=0; // the account the journal belongs to
ulong g_last_tick_ms=0;
int VtTabCount() { return(3); }
//--- filter text for the footer
string VtFilterText()
{
string p[];
int n=0;
ArrayResize(p,4);
p[n++]=(InpThisSymbol ? _Symbol : "All symbols");
p[n++]=(InpMagic>=0 ? "magic "+IntegerToString(InpMagic) : "all magics");
if(InpComment!="")
p[n++]="comment \""+InpComment+"\"";
p[n++]=(g_filter_start>0 ? "from "+InpStartDate : "all history");
string o="";
for(int i=0; i<n; i++)
o+=(i>0 ? " "+ShortToString(0x00B7)+" " : "")+p[i];
return(o);
}
void VtApplyFilter()
{
g_filter_start=0;
if(InpStartDate!="")
{
datetime t=StringToTime(InpStartDate);
if(t>0)
g_filter_start=(long)t;
}
g_eng.Filter(InpThisSymbol ? _Symbol : "",InpMagic,InpComment,g_filter_start);
g_filter_text=VtFilterText();
}
//--- demo data ----------------------------------------------------------------------------------------------------------
//--- one refresh of everything the card shows -----------------------------------------------------------------------
void VtRefresh(const bool first)
{
VtReadAccount(g_acct);
g_server_now=(long)TimeTradeServer();
if(g_acct.login!=g_jr_login && g_jr_login!=0) // another account logged in: its own journal and caches
{
CkDbg("another account logged in: journal and caches start again");
g_jr.Flush(true,GetTickCount());
g_jr.Init("Vitals\\FDD_"+IntegerToString(g_acct.login)+".csv",GetTickCount());
g_jr_login=g_acct.login;
g_hist_dirty=true;
}
bool rebuilt=false;
ulong t0=GetMicrosecondCount();
if(g_hist.Refresh(g_eng,rebuilt))
g_hist_dirty=true;
if(rebuilt)
CkDbg(StringFormat("history rebuilt: %d deals, %d trades",g_hist.ndeals,g_eng.ntr));
if(rebuilt)
g_perf_rebuild_us=GetMicrosecondCount()-t0;
g_npos=VtReadPositions(g_pos,g_acct.ccy);
}
void VtRecompute()
{
VtSummarize(g_eng,g_pos,g_npos,g_acct.balance);
g_eng.Report(g_server_now,g_acct.balance,g_jr.d,g_jr.m,g_jr.p,g_jr.n);
g_report_ver++;
g_hist_dirty=false;
}
//--- journal sample with the time of today's new low
void VtSample()
{
long today=g_server_now/86400;
double before=0;
if(g_jr.n>0 && g_jr.d[g_jr.n-1]==today)
before=g_jr.m[g_jr.n-1];
int n0=g_jr.n;
g_jr.Sample(g_server_now,g_acct.equity-g_acct.credit,g_acct.balance,GetTickCount());
double after=0;
if(g_jr.n>0 && g_jr.d[g_jr.n-1]==today)
after=g_jr.m[g_jr.n-1];
if(after<before || g_jr.n!=n0)
{
if(after<before)
g_today_low_time=g_server_now;
g_hist_dirty=true;
}
}
//--- drawing --------------------------------------------------------------------------------------------------------------
string VtAcctText()
{
string s=VtMaskLogin(g_acct.login)+" "+ShortToString(0x00B7)+" "+g_acct.server+" "+ShortToString(0x00B7)+" "+g_acct.ccy+
" "+ShortToString(0x00B7)+" 1:"+IntegerToString(g_acct.leverage)+" "+ShortToString(0x00B7)+" "+g_acct.mode;
return(s);
}
void VtChromeTop(CVtCard &c)
{
//--- header: brand, account, fold button
c.Txt(10,7,"VITALS",c.pal.ink,VTK_BRAND);
int bw=c.TextW("VITALS",VTK_BRAND);
double ax=10+bw+8;
string t=VtAcctText();
double room=VT_CARD_W-34-ax;
while(StringLen(t)>4 && c.TextW(t,VTK_SMALL)>room)
t=StringSubstr(t,0,StringLen(t)-2);
if(t!=VtAcctText())
t+=ShortToString(0x2026);
c.Txt(ax,8,t,c.pal.label,VTK_SMALL);
c.Zone(0,0,VT_CARD_W,28,VTZ_HEAD);
c.Box(VT_CARD_W-28,5,VT_CARD_W-8,23,c.pal.edge);
c.Txt(VT_CARD_W-18,6,c.folded ? "+" : ShortToString(0x2013),c.pal.label,VTK_MONO,2);
c.Zone(VT_CARD_W-30,3,VT_CARD_W-6,25,VTZ_FOLD);
}
void VtChromeTabs(CVtCard &c)
{
double x=0;
int n=VtTabCount();
for(int i=0; i<n; i++)
{
double w=c.TextW(VT_TAB_NAMES[i],VTK_SANS)+18;
bool on=(i==c.tab);
c.Txt(x+w/2.0,36,VT_TAB_NAMES[i],on ? c.pal.ink : c.pal.label,VTK_SANS,2);
if(on)
c.Fill(x,52,x+w,54,c.pal.acc);
c.Zone(x,28,x+w,55,VTZ_TAB,i);
x+=w;
}
c.HLine(0,VT_CARD_W,54,c.pal.edge);
// drawn after the active underline so the rule stays under it
if(c.tab<n)
{
double xs=0;
for(int i=0; i<c.tab; i++)
xs+=c.TextW(VT_TAB_NAMES[i],VTK_SANS)+18;
c.Fill(xs,52,xs+c.TextW(VT_TAB_NAMES[c.tab],VTK_SANS)+18,54,c.pal.acc);
}
}
double VtChromeFoot(CVtCard &c,const double y)
{
c.HLine(10,VT_CARD_W-10,y,c.pal.rule);
string left=g_filter_text;
color lc=c.pal.dim;
if(g_note!="" && GetTickCount()<g_note_until)
{ left=g_note; lc=c.pal.acc; }
while(StringLen(left)>4 && c.TextW(left,VTK_SMALL)>VT_CARD_W-24-100)
left=StringSubstr(left,0,StringLen(left)-2);
c.Txt(12,y+5,left,lc,VTK_SMALL);
c.Txt(VT_CARD_W-12,y+5,"Allan Ni"+ShortToString(0x00F1)+"al",c.pal.dim,VTK_SMALL,1);
double end=y+24;
return(end);
}
double VtDrawBody(CVtCard &c)
{
switch(c.tab)
{
case 0: return(VtDrawOverview(c));
case 1: return(VtDrawHistory(c));
case 2: return(VtDrawStats(c));
}
return(VT_BODY_TOP+10);
}
void VtPaint(const double total)
{
g_card.ZoneReset();
g_card.Frame(g_card.folded ? 29 : total);
VtChromeTop(g_card);
}
void VtRedraw()
{
if(!g_card.ok)
return;
if(g_card.folded)
{
g_card.Resize(29);
VtPaint(29);
g_card.Update();
return;
}
double guess=(g_h_tab[g_card.tab]>0 ? g_h_tab[g_card.tab] : 360);
for(int pass=0; pass<2; pass++)
{
g_card.Resize(guess);
VtPaint(guess);
VtChromeTabs(g_card);
double y=VtDrawBody(g_card);
double total=VtChromeFoot(g_card,y+4);
total+=4;
if(MathAbs(total-guess)<0.5)
{ g_h_tab[g_card.tab]=total; break; }
guess=total;
g_h_tab[g_card.tab]=total;
if(pass==1)
{ g_card.Resize(guess); VtPaint(guess); VtChromeTabs(g_card); y=VtDrawBody(g_card); VtChromeFoot(g_card,y+4); }
}
g_card.Clamp();
g_card.Update();
}
string VtSignature()
{
VtAcct a=g_acct;
return(StringFormat("%.2f|%.2f|%.2f|%.2f|%.2f|%d|%.2f|%d|%d|%d|%d|%I64d|%d|%d",a.equity,a.balance,a.margin,a.free,a.level,g_npos,
g_eng.o_pl[2],g_card.tab,g_card.folded,g_report_ver,g_card.light,g_server_now/86400,g_cal_m*10000+g_cal_y,
(g_note!="" && GetTickCount()<g_note_until) ? 1 : 0));
}
void VtTick(const bool force)
{
ulong t0=GetMicrosecondCount();
g_last_tick_ms=GetTickCount();
VtRefresh(false);
VtSample();
if(g_hist_dirty)
VtRecompute();
else
VtSummarize(g_eng,g_pos,g_npos,g_acct.balance);
if(g_card.ok)
{
if(GetTickCount()-g_last_theme_ms>3000)
{
g_last_theme_ms=GetTickCount();
bool was=g_card.light;
g_card.SetTheme(InpTheme);
if(was!=g_card.light)
g_last_sig="";
}
string sig=VtSignature();
if(force || sig!=g_last_sig)
{
g_last_sig=sig;
VtRedraw();
}
}
g_perf_timer_us+=GetMicrosecondCount()-t0;
g_perf_timer_n++;
if(g_tester && (g_perf_timer_n==1 || g_perf_timer_n%1500==0)) // proof of life in the tester log
PrintFormat("Vitals: tester tick %d at %s: balance %.2f equity %.2f, %d deals read, %d trades, %d journal days, report %d",
g_perf_timer_n,TimeToString((datetime)g_server_now),g_acct.balance,g_acct.equity,g_hist.ndeals,g_eng.ntr,g_jr.n,g_report_ver);
}
//--- OnCalculate: keep the journal fed on every tick
void VtCalc()
{
VtReadAccount(g_acct);
g_server_now=(long)TimeTradeServer();
VtSample();
if(g_tester && g_server_now-g_tester_tick>=60) // the tester does not deliver OnTimer to an indicator
{
g_tester_tick=g_server_now;
VtTick(false);
}
if(!g_tester && g_card.ok && GetTickCount()-g_last_tick_ms>=2500) // the timer normally keeps this fresh
VtTick(false);
}
void VtClick(const int id,const int arg)
{
switch(id)
{
case VTZ_FOLD:
g_card.folded=!g_card.folded;
g_card.Save();
break;
case VTZ_TAB:
if(arg>=0 && arg<VtTabCount())
{ g_card.tab=arg; g_card.Save(); }
break;
}
g_last_sig="";
VtRedraw();
}
void VtMouse(const int mx,const int my,const int flags)
{
if(!g_card.ok)
return;
bool down=((flags&1)!=0);
if(down && !g_card.m_down) // press
{
g_card.m_down=true;
g_card.m_press_x=mx; g_card.m_press_y=my;
g_card.m_hit_card=g_card.Inside(mx,my);
g_card.m_drag=false;
if(g_card.m_hit_card)
{
g_card.m_scroll_saved=true;
ChartSetInteger(g_card.chart,CHART_MOUSE_SCROLL,false);
int id,arg;
if(g_card.ZoneAt(mx-g_card.px,my-g_card.py,id,arg) && id==VTZ_HEAD)
{ g_card.m_drag=true; g_card.m_off_x=mx-g_card.px; g_card.m_off_y=my-g_card.py; }
}
}
else
if(down && g_card.m_down) // move with the button down
{
if(g_card.m_drag)
{
g_card.MoveTo(mx-g_card.m_off_x,my-g_card.m_off_y);
g_card.Clamp();
ChartRedraw(g_card.chart);
}
}
else
if(!down && g_card.m_down) // release
{
g_card.m_down=false;
if(g_card.m_scroll_saved)
{
ChartSetInteger(g_card.chart,CHART_MOUSE_SCROLL,g_scroll_orig);
g_card.m_scroll_saved=false;
}
bool moved=(MathAbs(mx-g_card.m_press_x)>4 || MathAbs(my-g_card.m_press_y)>4);
if(g_card.m_drag && moved)
g_card.Save();
else
if(g_card.m_hit_card && !moved)
{
int id,arg;
if(g_card.ZoneAt(mx-g_card.px,my-g_card.py,id,arg) && id!=VTZ_HEAD)
VtClick(id,arg);
}
g_card.m_drag=false;
}
}
//--- start-up and shut-down -----------------------------------------------------------------------------------------------
int VtInit()
{
bool bad=(InpScale<=0);
if(bad)
{
Print(VT_NAME,": the UI scale must be above 0 and the alert levels 0 or more");
return(INIT_PARAMETERS_INCORRECT);
}
CkDebugStart(VT_NAME+"",VT_VERSION,InpDebug,StringFormat("scale %.2f, theme %d, this symbol %d, magic %I64d, comment filter %d, start date %s",
InpScale,(int)InpTheme,InpThisSymbol,InpMagic,StringLen(InpComment)>0,InpStartDate));
g_tester=(bool)MQLInfoInteger(MQL_TESTER);
for(int i=0; i<8; i++)
g_h_tab[i]=0;
VtApplyFilter();
VtReadAccount(g_acct);
g_server_now=(long)TimeTradeServer();
g_jr.Init("Vitals\\FDD_"+IntegerToString(g_acct.login)+".csv",GetTickCount());
g_jr_login=g_acct.login;
if(!g_tester)
{
g_scroll_orig=(bool)ChartGetInteger(0,CHART_MOUSE_SCROLL);
g_mouse_orig=(bool)ChartGetInteger(0,CHART_EVENT_MOUSE_MOVE);
ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
g_card.scale=CVtCard::ScaleFor(InpScale);
if(g_card.Create(VT_PREFIX+VT_NAME+"_"+IntegerToString(ChartID()),VT_CARD_W,360))
{
g_card.Load(12,22);
g_card.SetTheme(InpTheme);
if(g_card.tab<0 || g_card.tab>=VtTabCount())
g_card.tab=0;
g_card.MoveTo(g_card.px,g_card.py);
}
else
CkDbg("card could not be created, error "+IntegerToString(GetLastError()));
}
VtRefresh(true);
VtSample();
VtRecompute();
bool tm=EventSetTimer(1);
if(!tm)
CkDbg("timer could not be set, error "+IntegerToString(GetLastError()));
VtTick(true);
return(INIT_SUCCEEDED);
}
void VtDeinit(const int reason)
{
EventKillTimer();
g_jr.Flush(true,GetTickCount());
if(!g_tester)
{
ChartSetInteger(0,CHART_MOUSE_SCROLL,g_scroll_orig);
ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,g_mouse_orig);
}
if(g_perf_timer_n>0)
PrintFormat("Vitals: tab %d, history rebuild %.1f ms; timer cost %.0f us average over %d ticks",g_card.tab,g_perf_rebuild_us/1000.0,
(double)g_perf_timer_us/g_perf_timer_n,g_perf_timer_n);
if(g_card.ok)
{
g_card.Save();
g_card.Destroy();
}
}
void VtTimer()
{
VtTick(false);
}
void VtEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
if(id==CHARTEVENT_MOUSE_MOVE)
VtMouse((int)lparam,(int)dparam,(int)StringToInteger(sparam));
else
if(id==CHARTEVENT_CHART_CHANGE && g_card.ok)
{
g_card.Clamp();
g_last_theme_ms=0;
}
}
#endif
//+------------------------------------------------------------------+