//+------------------------------------------------------------------+ //| 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" #include "VtLegacy.mqh" #define VT_VERSION "1.1" // keep equal to #property version of Vitals #define VT_NAME "Vitals" //--- 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) //--- Display input ENUM_VT_THEME InpTheme = VT_THEME_AUTO; // Theme input double InpScale = 1.0; // UI scale (multiplies the screen DPI scale) 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; i0 ? " "+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 ---------------------------------------------------------------------------------------------------------- //--- journal: this account's file (the Vitals 1.0 journal is imported the first time, see VtLegacy.mqh) void VtJournalOpen(const long login) { VtJournalOpenAt(g_jr,login); } //--- one refresh of everything the card shows ----------------------------------------------------------------------- void VtRefresh(const bool first) { VtReadAccount(g_acct); g_server_now=(long)VtServerNow(); 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()); VtJournalOpen(g_acct.login); g_jr_login=g_acct.login; g_hist_dirty=true; } bool rebuilt=false; ulong t0=VtMicros(); 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=VtMicros()-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(after4 && 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; i4 && 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()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+=VtMicros()-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)VtServerNow(); 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 && arg4 || 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+" MT4",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)IsTesting(); for(int i=0; i<8; i++) g_h_tab[i]=0; VtApplyFilter(); VtReadAccount(g_acct); g_server_now=(long)VtServerNow(); VtJournalOpen(g_acct.login); 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(); } } //--- scripted card checks (test builds only; InpSelfTest=1): the chart's own custom-event queue carries the same mouse events // the handler gets from CHARTEVENT_MOUSE_MOVE; a step sends events on one timer tick and checks the result on the next. 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 //+------------------------------------------------------------------+