2026-10-21 19:15:46 +08:00 | | | //+------------------------------------------------------------------+
|
| | | //| HealthLive.mqh |
|
| | | //| Copyright 2026, Allan Niñal |
|
| | | //| https://www.allanninal.dev/mql5 |
|
| | | //+------------------------------------------------------------------+
|
| | | //| The live side of EA Health Monitor: reads only the deals added |
|
| | | //| since the last refresh (a new day or a shorter history starts |
|
| | | //| again), the open positions per magic, and the names: the user's |
|
| | | //| list "magic=name,...", else the comment of the EA's first |
|
| | | //| opening deal, "Manual" for magic 0. |
|
| | | //+------------------------------------------------------------------+
|
| | | // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| | | #ifndef HEALTHLIVE_MQH
|
| | | #define HEALTHLIVE_MQH
|
| | |
|
| | | #include "HealthCore.mqh"
|
| | |
|
2026-10-31 08:12:10 +08:00 | | |
|
2026-10-21 19:15:46 +08:00 | | | struct EHLive
|
| | | {
|
| | | EHBook b;
|
| | | int done; // history deals processed
|
| | | ulong last_ticket; // ticket of deal done-1 (older deals synced in later move it: start again)
|
2026-10-31 08:12:10 +08:00 | | | long since,day;
|
| | | int pos_n; // positions and balance at the last history read: unchanged = no new deal to read
|
| | | double bal;
|
| | | ulong full_ms;
|
2026-10-21 19:15:46 +08:00 | | | long map_magic[];
|
| | | string map_name[];
|
| | | };
|
| | |
|
| | | void EHLiveInit(EHLive &L,const string names)
|
| | | {
|
| | | EHInit(L.b);
|
2026-10-31 08:12:10 +08:00 | | | L.done=0; L.since=-1; L.day=-1; L.last_ticket=0;
|
| | | L.pos_n=-1; L.bal=0; L.full_ms=0;
|
2026-10-21 19:15:46 +08:00 | | | ArrayResize(L.map_magic,0);
|
| | | ArrayResize(L.map_name,0);
|
| | | string items[];
|
| | | int n=StringSplit(names,',',items);
|
| | | for(int i=0; i<n; i++)
|
| | | {
|
| | | string kv[];
|
| | | if(StringSplit(items[i],'=',kv)!=2)
|
| | | continue;
|
| | | StringTrimLeft(kv[0]); StringTrimRight(kv[0]); StringTrimLeft(kv[1]); StringTrimRight(kv[1]);
|
| | | int k=ArraySize(L.map_magic);
|
| | | ArrayResize(L.map_magic,k+1);
|
| | | ArrayResize(L.map_name,k+1);
|
| | | L.map_magic[k]=StringToInteger(kv[0]);
|
| | | L.map_name[k]=kv[1];
|
| | | }
|
| | | }
|
| | |
|
| | | string EHMapped(const EHLive &L,const long magic)
|
| | | {
|
| | | for(int i=0; i<ArraySize(L.map_magic); i++)
|
| | | if(L.map_magic[i]==magic)
|
| | | return(L.map_name[i]);
|
| | | return("");
|
| | | }
|
| | |
|
2026-10-31 08:12:10 +08:00 | | |
|
2026-10-21 19:15:46 +08:00 | | | //--- server midnight of today
|
| | | long EHDayStart()
|
| | | {
|
| | | MqlDateTime t;
|
| | | TimeToStruct(TimeTradeServer(),t);
|
| | | t.hour=0; t.min=0; t.sec=0;
|
| | | return((long)StructToTime(t));
|
| | | }
|
| | |
|
2026-10-31 08:12:10 +08:00 | | | //--- read the deals added since the last read; `ok` false when the history is not available now
|
| | | bool EHReadHistory(EHLive &L,const long since,const long day,bool &ok)
|
2026-10-21 19:15:46 +08:00 | | | {
|
2026-10-31 08:12:10 +08:00 | | | ok=HistorySelect(0,TimeCurrent()+86400);
|
| | | if(!ok)
|
2026-10-21 19:15:46 +08:00 | | | return(false);
|
| | | int n=HistoryDealsTotal();
|
2026-10-31 08:12:10 +08:00 | | | bool restart=(n<L.done || day!=L.day || since!=L.since ||
|
| | | (L.done>0 && HistoryDealGetTicket(L.done-1)!=L.last_ticket));
|
2026-10-21 19:15:46 +08:00 | | | if(restart)
|
| | | {
|
| | | EHInit(L.b);
|
2026-10-31 08:12:10 +08:00 | | | L.done=0; L.day=day; L.since=since;
|
2026-10-21 19:15:46 +08:00 | | | }
|
| | | bool chg=restart;
|
| | | for(int i=L.done; i<n; i++)
|
| | | {
|
| | | ulong t=HistoryDealGetTicket(i);
|
| | | if(t==0)
|
| | | continue;
|
| | | long type=HistoryDealGetInteger(t,DEAL_TYPE);
|
| | | if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
|
| | | continue;
|
| | | long en=HistoryDealGetInteger(t,DEAL_ENTRY);
|
| | | EHDeal d;
|
| | | d.time=HistoryDealGetInteger(t,DEAL_TIME);
|
| | | d.pos=HistoryDealGetInteger(t,DEAL_POSITION_ID);
|
| | | d.magic=HistoryDealGetInteger(t,DEAL_MAGIC);
|
| | | d.entry=(en==DEAL_ENTRY_IN ? EH_IN : (en==DEAL_ENTRY_OUT ? EH_OUT : (en==DEAL_ENTRY_INOUT ? EH_INOUT : EH_OUTBY)));
|
| | | d.volume=HistoryDealGetDouble(t,DEAL_VOLUME);
|
| | | d.profit=HistoryDealGetDouble(t,DEAL_PROFIT);
|
| | | d.swap=HistoryDealGetDouble(t,DEAL_SWAP);
|
| | | d.commission=HistoryDealGetDouble(t,DEAL_COMMISSION);
|
| | | d.fee=HistoryDealGetDouble(t,DEAL_FEE);
|
2026-10-31 08:12:10 +08:00 | | | bool closed;
|
| | | double tnet;
|
| | | EHAdd(L.b,d,since,day,closed,tnet);
|
2026-10-21 19:15:46 +08:00 | | | if(d.entry==EH_IN && d.magic!=0)
|
| | | {
|
| | | string cm=HistoryDealGetString(t,DEAL_COMMENT);
|
| | | int k=EHStatIdx(L.b,d.magic);
|
| | | if(cm!="" && L.b.st[k].name=="") // the first comment names it (comments that change per trade do not)
|
| | | L.b.st[k].name=cm;
|
| | | }
|
| | | chg=true;
|
| | | }
|
| | | L.done=n;
|
| | | L.last_ticket=(n>0 ? HistoryDealGetTicket(n-1) : 0);
|
2026-10-31 08:12:10 +08:00 | | | return(chg);
|
| | | }
|
| | |
|
| | | //--- bring the book up to date; true when anything changed
|
| | | //--- the history (the costly part) is read again only when the positions or the balance changed, or every 30 s
|
| | | bool EHRefresh(EHLive &L,const long since)
|
| | | {
|
| | | long day=EHDayStart();
|
| | | int pn=PositionsTotal();
|
| | | double bal=AccountInfoDouble(ACCOUNT_BALANCE);
|
| | | ulong ms=GetTickCount64();
|
| | | bool chg=false;
|
| | | if(!(L.done>0 && day==L.day && since==L.since && pn==L.pos_n && bal==L.bal && ms-L.full_ms<30000))
|
| | | {
|
| | | bool ok;
|
| | | chg=EHReadHistory(L,since,day,ok);
|
| | | if(!ok)
|
| | | return(false);
|
| | | L.pos_n=pn;
|
| | | L.bal=bal;
|
| | | L.full_ms=ms;
|
| | | }
|
2026-10-21 19:15:46 +08:00 | | | //--- open positions
|
| | | for(int i=0; i<L.b.nst; i++)
|
| | | { L.b.st[i].open=0; L.b.st[i].floating=0; }
|
| | | for(int i=PositionsTotal()-1; i>=0; i--)
|
| | | {
|
| | | ulong t=PositionGetTicket(i);
|
| | | if(t==0)
|
| | | continue;
|
| | | int k=EHStatIdx(L.b,PositionGetInteger(POSITION_MAGIC));
|
| | | L.b.st[k].open++;
|
| | | L.b.st[k].floating+=PositionGetDouble(POSITION_PROFIT)+PositionGetDouble(POSITION_SWAP);
|
| | | }
|
| | | return(chg);
|
| | | }
|
| | |
|
| | | string EHName(const EHLive &L,const EHStat &s)
|
| | | {
|
| | | string m=EHMapped(L,s.magic);
|
| | | if(m!="")
|
| | | return(m);
|
| | | if(s.magic==0)
|
| | | return("Manual");
|
| | | string c=s.name;
|
| | | if(StringLen(c)>14)
|
| | | c=StringSubstr(c,0,14);
|
| | | return(c=="" ? "#"+IntegerToString(s.magic) : c+" #"+IntegerToString(s.magic));
|
| | | }
|
| | |
|
| | | //--- seconds since a deal as "45m", "3h", "2d" (short, so the panel stays narrow)
|
| | | string EHAgo(const long t)
|
| | | {
|
| | | if(t<=0)
|
| | | return("-");
|
| | | long s=MathMax(0,(long)TimeTradeServer()-t);
|
| | | if(s<3600)
|
| | | return(IntegerToString(s/60)+"m");
|
| | | if(s<86400*2)
|
| | | return(IntegerToString(s/3600)+"h");
|
| | | return(IntegerToString(s/86400)+"d");
|
| | | }
|
| | |
|
| | | //--- 1234.5 -> "1,234.50", with a sign when asked
|
| | | string EHMoney(const double v,const bool sign=false)
|
| | | {
|
| | | string s=DoubleToString(MathAbs(v),2);
|
| | | int dot=StringFind(s,".");
|
| | | string ip=StringSubstr(s,0,dot),out="";
|
| | | for(int i=StringLen(ip)-1,k=0; i>=0; i--,k++)
|
| | | out=(k>0 && k%3==0 ? StringSubstr(ip,i,1)+"," : StringSubstr(ip,i,1))+out;
|
| | | string r=out+StringSubstr(s,dot);
|
| | | if(v<0 && MathAbs(v)>=0.005)
|
| | | return("-"+r);
|
| | | return((sign && v>=0.005 ? "+" : "")+r);
|
| | | }
|
| | |
|
| | | #endif
|
| | | //+------------------------------------------------------------------+
|