ea-health-monitor/mql5/Indicators/EAHealth/EAHealthMonitor.mq5
Allan Niñal Allan Niñal 286e60f707 Free source regenerated: Pro-only code removed from the engine, debug log added
Generated by the publish tool from the private source; all publish gates pass, compile 0 errors 0 warnings on MT5 and MT4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:12:10 +08:00
187 lines
7.6 KiB
MQL5
//+------------------------------------------------------------------+
//| EAHealthMonitor.mq5 |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| Free indicator: one table row per Expert Advisor (magic number) |
//| on the account, plus manual trades and a total: closed trades, |
//| net result, win rate, profit factor, max drawdown of its realized|
//| result, today's result, open positions and their floating P/L. |
//| A manual close of an EA's trade stays with that EA. Reads the |
//| account history only; it never trades. |
//| Core: Include/EAHealth/HealthCore.mqh |
//+------------------------------------------------------------------+
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
#property copyright "Copyright 2026, Allan Niñal <https://www.allanninal.dev/mql5>"
#property link "https://www.allanninal.dev/mql5"
#property version "1.1"
#property description "Per-EA results on one panel: trades, net, win rate, profit factor, max drawdown, today, open trades, floating P/L."
#property description "Grouped by magic number; manual trades on their own row. Reads the history only, never trades."
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
#include <EAHealth/HealthLive.mqh>
#include <ChartKit/CkDebug.mqh>
#include <EAHealth/EAClock.mqh>
#include <EAHealth/Panel.mqh>
input group "Expert Advisors"
input string InpNames = ""; // Names: magic=name, comma separated (e.g. 771500=Trend EA,0=Manual)
input bool InpManual = true; // Show manual trades (magic 0)
input bool InpHideIdle = false; // Hide EAs with no trade in the period and nothing open
input group "Display"
input ENUM_SD_CORNER InpCorner = SD_TOP_LEFT; // Panel corner
input int InpX = 10; // Distance from the side, pixels
input int InpY = 20; // Distance from the top or bottom, pixels
input int InpFontSize = 9; // Font size
input color InpColPanel = C'22,27,36'; // Panel colour
input color InpColCell = C'30,36,48'; // Cell colour
input color InpColText = C'232,237,243'; // Text colour
input color InpColMuted = C'120,130,145'; // Header colour
input color InpColPos = C'38,166,98'; // Positive colour
input color InpColNeg = C'226,74,70'; // Negative colour
input group "Support"
input bool InpDebug = false; // Debug log for bug reports (Experts tab)
const string PRODUCT="EA Health Monitor";
const string PREFIX ="EHM_";
EHLive g_L;
CSDPanel g_panel;
long g_rows[]; // magic of each panel row (the last row is the total)
string g_sig="";
ulong g_last_ms=0;
bool g_ready=false;
color Sign(const double v) { return(v>0.005 ? InpColPos : (v<-0.005 ? InpColNeg : InpColText)); }
void Show()
{
//--- rows: EAs in the order they first traded, manual, then the total
long rows[];
ArrayResize(rows,0);
for(int i=0; i<g_L.b.nst; i++)
{
EHStat s=g_L.b.st[i];
if(s.magic==0 && !InpManual)
continue;
if(InpHideIdle && s.n==0 && s.open==0 && s.deals==0)
continue;
int k=ArraySize(rows);
ArrayResize(rows,k+1);
rows[k]=s.magic;
}
string sig="";
for(int i=0; i<ArraySize(rows); i++)
sig+=IntegerToString(rows[i])+"|"+EHName(g_L,g_L.b.st[EHStatIdx(g_L.b,rows[i])])+";";
if(sig!=g_sig)
{
string heads[9]={"Trades","Net","Win %","PF","Max DD","Today","Open","Floating","Last deal"},labels[];
ArrayResize(labels,ArraySize(rows)+1);
for(int i=0; i<ArraySize(rows); i++)
labels[i]=EHName(g_L,g_L.b.st[EHStatIdx(g_L.b,rows[i])]);
labels[ArraySize(rows)]="All";
CkDbg(StringFormat("rows changed: %d EA rows",ArraySize(rows)));
g_panel.Layout(heads,labels);
g_sig=sig;
ArrayCopy(g_rows,rows);
ArrayResize(g_rows,ArraySize(rows));
}
EHStat tot;
ZeroMemory(tot);
int r=0;
for(; r<ArraySize(g_rows); r++)
{
EHStat s=g_L.b.st[EHStatIdx(g_L.b,g_rows[r])];
string tip=StringFormat("%s (magic %I64d): %d trades, %d won, %d lost; gross profit %s, gross loss %s; expectancy %s per trade",
EHName(g_L,s),s.magic,s.n,s.w,s.l,EHMoney(s.gp),EHMoney(s.gl),EHMoney(EHExpectancy(s)))+
". Last deal "+(s.last>0 ? TimeToString((datetime)s.last,TIME_DATE|TIME_MINUTES) : "-");
Row(r,s,tip);
tot.n+=s.n; tot.w+=s.w; tot.l+=s.l; tot.gp+=s.gp; tot.gl+=s.gl; tot.net+=s.net; tot.today+=s.today;
tot.open+=s.open; tot.floating+=s.floating; tot.last=MathMax(tot.last,s.last);
tot.maxdd=MathMax(tot.maxdd,s.maxdd);
}
Row(r,tot,"All rows together. Max DD here is the largest of the rows, not the drawdown of the account.");
g_panel.SetTitle(StringFormat("%s (%s)",PRODUCT,AccountInfoString(ACCOUNT_CURRENCY)));
ChartRedraw();
}
void Row(const int r,const EHStat &s,const string tip)
{
double pf=EHPF(s);
g_panel.SetCell(r,0,IntegerToString(s.n),InpColText,InpColCell,tip);
g_panel.SetCell(r,1,EHMoney(s.net,true),Sign(s.net),InpColCell,tip);
g_panel.SetCell(r,2,s.n>0 ? DoubleToString(EHWinRate(s),0)+"%" : "-",InpColText,InpColCell,tip);
g_panel.SetCell(r,3,pf==EMPTY_VALUE ? "no loss" : (s.n>0 ? DoubleToString(pf,2) : "-"),InpColText,InpColCell,tip);
g_panel.SetCell(r,4,s.maxdd>0.005 ? "-"+EHMoney(s.maxdd) : "0.00",s.maxdd>0.005 ? InpColNeg : InpColText,InpColCell,
tip+". Max DD: the largest fall of the realized result from its high.");
g_panel.SetCell(r,5,EHMoney(s.today,true),Sign(s.today),InpColCell,tip);
g_panel.SetCell(r,6,IntegerToString(s.open),InpColText,InpColCell,tip);
g_panel.SetCell(r,7,s.open>0 ? EHMoney(s.floating,true) : "-",Sign(s.floating),InpColCell,tip);
g_panel.SetCell(r,8,EHAgo(s.last),InpColMuted,InpColCell,tip);
}
void Tick()
{
ulong now=SDNowMs();
if(g_last_ms!=0 && now-g_last_ms<2000)
return;
g_last_ms=now;
EHRefresh(g_L,0);
//--- a non-visual tester run shows nothing: skip the panel there
if(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
Show();
}
int OnInit()
{
if(InpFontSize<7 || InpFontSize>14)
{
Print(PRODUCT,": font size 7-14");
return(INIT_PARAMETERS_INCORRECT);
}
IndicatorSetString(INDICATOR_SHORTNAME,PRODUCT);
CkDebugStart("EA Health Monitor","1.1",InpDebug,StringFormat("font %d, manual %d, hide idle %d, names %d",InpFontSize,InpManual,InpHideIdle,StringLen(InpNames)>0));
EHLiveInit(g_L,InpNames);
g_panel.Setup(PREFIX+"t_",PRODUCT,InpCorner,InpX,InpY,InpFontSize,InpColPanel,InpColText,InpColMuted);
g_panel.SetRowHead("EA");
g_sig="-";
g_ready=true;
EventSetTimer(2);
Tick();
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
EventKillTimer();
g_panel.Destroy();
ObjectsDeleteAll(0,PREFIX);
}
void OnTimer()
{
if(g_ready)
Tick();
}
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
if(id==CHARTEVENT_CHART_CHANGE)
g_panel.ChartResized();
else
if(id==CHARTEVENT_OBJECT_CLICK)
{
int row,col;
g_panel.Click(sparam,row,col); // the collapse button
}
}
int OnCalculate(const int rates_total,const int prev_calculated,const int begin,const double &price[])
{
if(g_ready)
Tick();
return(rates_total);
}
//+------------------------------------------------------------------+