//+------------------------------------------------------------------+
//| PortfolioRiskMonitorService.mq5                                  |
//| MetaTrader 5 SERVICE. Runs in the background with no chart of    |
//| its own, watches correlation-adjusted portfolio risk across      |
//| the account, draws a small plain-language panel on an open       |
//| chart, and sends a push notification to your phone when a        |
//| risk threshold is crossed. Same engine and same math as          |
//| PortfolioRiskCheck.mq5 (the article script).                     |
//| Reads open positions only. Never trades.                         |
//+------------------------------------------------------------------+
#property service
#property version   "2.00"
#property description "Background risk monitor: live panel on the chart plus push alerts."

#include "PortfolioRiskMonitor.mqh"

//--- engine settings (same defaults as the article script)
input ENUM_TIMEFRAMES CorrTimeframe      = PERIOD_H1; // timeframe for correlations
input int             CorrLookbackBars   = 200;       // history bars
input double          VaRConfidenceZ     = 1.645;     // ~95% one-tailed
//--- monitoring
input int             CheckSeconds       = 15;        // how often to check the account
//--- panel display
input bool            ShowPanel          = true;      // draw the panel on a chart
input string          PanelChartSymbol   = "";        // chart symbol to draw on ("" = first open chart)
input int             PanelX             = 20;        // panel x
input int             PanelY             = 30;        // panel y
input double          UIScale            = 1.0;       // interface scale
//--- alert thresholds
input double          AlertHiddenFactor  = 130;       // alert if real risk >= this % of naive
input double          AlertRiskPctEquity = 3.0;       // alert if daily risk >= this % of account
input double          AlertTopSharePct   = 50;        // alert if one position >= this % of total risk
//--- anti-spam
input int             CooldownMinutes    = 60;        // min minutes between repeat pushes
input bool            StartupPush        = true;      // send a test push on start

#define F_HIDDEN 1
#define F_RISK   2
#define F_CONC   4

//--- palette
#define CBG      C'18,24,21'
#define CBORD    C'34,48,41'
#define CHEAD    C'15,46,42'
#define CINK     C'232,240,234'
#define CMUT     C'140,166,154'
#define CKICK    C'111,131,120'
#define CTRACK   C'28,38,32'
#define CGREEN   C'52,201,138'
#define CAMBER   C'230,184,75'
#define CRED     C'229,86,86'
#define CFILLG   C'28,56,46'
#define CFILLA   C'58,50,26'
#define CFILLR   C'58,32,32'
#define PFX      "PRMS_"
#define UIF      "Segoe UI"
#define MONO     "Consolas"
//--- DPI scaling
double gScl=1.0, gFscl=1.0;
//+------------------------------------------------------------------+
//| Scale a distance by the DPI factor                               |
//+------------------------------------------------------------------+
int S(double v)  { return (int)MathRound(v*gScl); }
//+------------------------------------------------------------------+
//| Scale a font size by the UI scale factor                         |
//+------------------------------------------------------------------+
int FS(int p)    { int f=(int)MathRound(p*gFscl); return (f<6 ? 6 : f); }
//+------------------------------------------------------------------+
//| Account currency                                                 |
//+------------------------------------------------------------------+
string Cur() { return AccountInfoString(ACCOUNT_CURRENCY); }
//+------------------------------------------------------------------+
//| Format a value with thousand separators                          |
//+------------------------------------------------------------------+
string Grp(double v)
  {
   int neg=(v<0);
   v=MathAbs(v);
   long n=(long)MathRound(v);
   string s=IntegerToString(n), o="";
   int c=0;
   for(int i=StringLen(s)-1;i>=0;i--)
     {
      o=StringSubstr(s,i,1)+o;
      if(++c%3==0 && i>0)
         o=","+o;
     }
   return (neg ? "-" : "")+o;
  }
//+------------------------------------------------------------------+
//| Create a rectangle label object on the given chart               |
//+------------------------------------------------------------------+
void RC(long id,string n)
  {
   string nm=PFX+n;
   if(ObjectFind(id,nm)<0)
      ObjectCreate(id,nm,OBJ_RECTANGLE_LABEL,0,0,0);
   ObjectSetInteger(id,nm,OBJPROP_CORNER,CORNER_LEFT_UPPER);
   ObjectSetInteger(id,nm,OBJPROP_BORDER_TYPE,BORDER_FLAT);
   ObjectSetInteger(id,nm,OBJPROP_BACK,false);
   ObjectSetInteger(id,nm,OBJPROP_SELECTABLE,false);
   ObjectSetInteger(id,nm,OBJPROP_HIDDEN,true);
  }
//+------------------------------------------------------------------+
//| Position, size and color a rectangle label                       |
//+------------------------------------------------------------------+
void setRect(long id,string n,int x,int y,int w,int h,color bg,color bd=clrNONE)
  {
   RC(id,n);
   string nm=PFX+n;
   ObjectSetInteger(id,nm,OBJPROP_XDISTANCE,x);
   ObjectSetInteger(id,nm,OBJPROP_YDISTANCE,y);
   ObjectSetInteger(id,nm,OBJPROP_XSIZE,MathMax(1,w));
   ObjectSetInteger(id,nm,OBJPROP_YSIZE,MathMax(1,h));
   ObjectSetInteger(id,nm,OBJPROP_BGCOLOR,bg);
   ObjectSetInteger(id,nm,OBJPROP_COLOR,(bd==clrNONE ? bg : bd));
   ObjectSetInteger(id,nm,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
  }
//+------------------------------------------------------------------+
//| Hide a rectangle label                                           |
//+------------------------------------------------------------------+
void hideRect(long id,string n)
  {
   string nm=PFX+n;
   if(ObjectFind(id,nm)>=0)
      ObjectSetInteger(id,nm,OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
  }
//+------------------------------------------------------------------+
//| Create or update a text label on the given chart                 |
//+------------------------------------------------------------------+
void L(long id,string n,int x,int y,string text,color clr,int size,string font=UIF,ENUM_ANCHOR_POINT a=ANCHOR_LEFT_UPPER)
  {
   string nm=PFX+n;
   if(ObjectFind(id,nm)<0)
      ObjectCreate(id,nm,OBJ_LABEL,0,0,0);
   ObjectSetInteger(id,nm,OBJPROP_CORNER,CORNER_LEFT_UPPER);
   ObjectSetInteger(id,nm,OBJPROP_ANCHOR,a);
   ObjectSetInteger(id,nm,OBJPROP_XDISTANCE,x);
   ObjectSetInteger(id,nm,OBJPROP_YDISTANCE,y);
   ObjectSetString(id,nm,OBJPROP_TEXT,text);
   ObjectSetInteger(id,nm,OBJPROP_COLOR,clr);
   ObjectSetInteger(id,nm,OBJPROP_FONTSIZE,size);
   ObjectSetString(id,nm,OBJPROP_FONT,font);
   ObjectSetInteger(id,nm,OBJPROP_SELECTABLE,false);
   ObjectSetInteger(id,nm,OBJPROP_HIDDEN,true);
   ObjectSetInteger(id,nm,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
  }
//+------------------------------------------------------------------+
//| Hide a text label                                                |
//+------------------------------------------------------------------+
void hideL(long id,string n)
  {
   string nm=PFX+n;
   if(ObjectFind(id,nm)>=0)
      ObjectSetInteger(id,nm,OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
  }
//+------------------------------------------------------------------+
//| Color for the hidden factor state                                |
//+------------------------------------------------------------------+
color HiddenColor(double h)
  {
   if(h>=125) return CRED;
   if(h>=108) return CAMBER;
   if(h<=92)  return CGREEN;
   return CINK;
  }
//+------------------------------------------------------------------+
//| Plain-language headline for the hidden factor                    |
//+------------------------------------------------------------------+
string HiddenHeadline(double h)
  {
   int diff=(int)MathRound(MathAbs(h-100.0));
   if(h>=108) return StringFormat("Real risk is %d%% BIGGER than it looks",diff);
   if(h<=92)  return StringFormat("Hedges cut your risk by %d%%",diff);
   return "Your risk is about what it looks";
  }
//+------------------------------------------------------------------+
//| Secondary line under the headline                                |
//+------------------------------------------------------------------+
string HiddenExplain(double h)
  {
   if(h>=108) return "Your trades stack on the same bet";
   if(h<=92)  return "Some trades cancel each other out";
   return "Your trades are fairly independent";
  }

//+------------------------------------------------------------------+
//| Find the chart to draw on                                        |
//+------------------------------------------------------------------+
long FindChart()
  {
   long id=ChartFirst();
   long first=id;
   while(id>=0)
     {
      if(PanelChartSymbol=="" || ChartSymbol(id)==PanelChartSymbol)
         return id;
      id=ChartNext(id);
     }
   return first;
  }
//+------------------------------------------------------------------+
//| Draw the panel on the given chart                                |
//+------------------------------------------------------------------+
void DrawPanel(long id,CRiskEngine &eng,bool hasData)
  {
   int PX=PanelX, PY=PanelY;
   int pad=S(14), colW=S(280);
   int Wpx=pad+colW+pad;
   int hdH=S(30);

   setRect(id,"bg",PX,PY,Wpx,hdH+S(40),CBG,CBORD);
   setRect(id,"hd",PX,PY,Wpx,hdH,CHEAD);
   L(id,"title",PX+pad,PY+S(7),"PORTFOLIO RISK MONITOR",CINK,FS(11),UIF);

   if(!hasData)
     {
      hideRect(id,"hero_tk"); hideRect(id,"hero_fl"); hideRect(id,"d1"); hideRect(id,"d2");
      hideL(id,"head"); hideL(id,"headsub"); hideL(id,"hf_tech");
      hideL(id,"vr_k"); hideL(id,"vr_v"); hideL(id,"vr_sub");
      hideL(id,"tp_k"); hideL(id,"tp_v"); hideL(id,"tp_sub");
      L(id,"empty",PX+pad,PY+hdH+S(12),"Open at least 2 trades and this",CINK,FS(9),UIF);
      L(id,"empty2",PX+pad,PY+hdH+S(30),"panel shows your real risk.",CMUT,FS(9),UIF);
      setRect(id,"bg",PX,PY,Wpx,hdH+S(52),CBG,CBORD);
      ChartRedraw(id);
      return;
     }
   hideL(id,"empty");
   hideL(id,"empty2");

   double hidden=eng.HiddenFactor();
   double trueRisk=eng.TrueRisk();
   double equity=eng.Equity();
   string topSym=eng.TopSymbol();
   double topShare=eng.TopShare();

   int y=PY+hdH+S(14);
   int xL=PX+pad;
   color hc=HiddenColor(hidden);

//--- HERO: plain headline in a colored band
   int heroH=S(46);
   setRect(id,"hero_tk",xL,y,colW,heroH,CTRACK);
   double fillr=hidden/150.0;
   if(fillr<0.05) fillr=0.05;
   if(fillr>1)    fillr=1;
   color hf=(hidden>=125 ? CFILLR : (hidden>=108 ? CFILLA : (hidden<=92 ? CFILLG : CTRACK)));
   setRect(id,"hero_fl",xL,y,(int)MathMax(S(3),colW*fillr),heroH,hf);
   L(id,"head",xL+S(12),y+S(15),HiddenHeadline(hidden),CINK,FS(11),UIF,ANCHOR_LEFT);
   L(id,"headsub",xL+S(12),y+S(32),HiddenExplain(hidden),hc,FS(9),UIF,ANCHOR_LEFT);
   y+=heroH+S(4);
//--- tiny technical line for the quants
   L(id,"hf_tech",xL,y,StringFormat("hidden factor  %.0f%% of naive risk",hidden),CKICK,FS(7),MONO);
   y+=S(20);

   setRect(id,"d1",xL,y,colW,S(1),CBORD);
   y+=S(14);

//--- WORST DAY
   L(id,"vr_k",xL,y,"On a bad day you could lose",CMUT,FS(9),UIF);
   y+=S(18);
   L(id,"vr_v",xL,y,StringFormat("%.1f%% of the account",(equity>0 ? 100.0*trueRisk/equity : 0)),CINK,FS(13),UIF);
   L(id,"vr_sub",xL+colW,y+S(4),StringFormat("%s %s",Grp(trueRisk),Cur()),CMUT,FS(9),UIF,ANCHOR_RIGHT_UPPER);
   y+=S(28);

   setRect(id,"d2",xL,y,colW,S(1),CBORD);
   y+=S(14);

//--- CONCENTRATION
   L(id,"tp_k",xL,y,"Most of your risk is one trade",CMUT,FS(9),UIF);
   y+=S(18);
   int share=(int)MathRound(100.0*topShare);
   L(id,"tp_v",xL,y,StringFormat("%s  is  %d%% of your risk",topSym,share),CINK,FS(12),UIF);
   y+=S(20);
   string csub=(share>=50 ? "That is more than half in one position" :
               (share>=35 ? "That is a big chunk in one position" :
                            "Reasonably spread across trades"));
   L(id,"tp_sub",xL,y,csub,(share>=50 ? CAMBER : CMUT),FS(8),UIF);
   y+=S(24);

   int H=(y-PY);
   setRect(id,"bg",PX,PY,Wpx,H,CBG,CBORD);
   setRect(id,"hd",PX,PY,Wpx,hdH,CHEAD);
   ChartRedraw(id);
  }
//+------------------------------------------------------------------+
//| Compose the push message: reasons first, then the numbers        |
//+------------------------------------------------------------------+
string BuildAlert(const int state,CRiskEngine &eng)
  {
   string why="";
   if((state&F_HIDDEN)!=0)
      why+="stacked bets";
   if((state&F_RISK)!=0)
      why+=(why=="" ? "" : ", ")+"big day at risk";
   if((state&F_CONC)!=0)
      why+=(why=="" ? "" : ", ")+"one-trade concentration";
   return "RISK ALERT ("+why+"): "+eng.Summary();
  }
//+------------------------------------------------------------------+
//| Service entry point: infinite monitoring loop                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- DPI scaling for the panel
   double dpi=(double)TerminalInfoInteger(TERMINAL_SCREEN_DPI);
   if(dpi<=0) dpi=96;
   gScl=(dpi/96.0)*(UIScale>0 ? UIScale : 1.0);
   gFscl=(UIScale>0 ? UIScale : 1.0);

//--- make sure pushes can actually reach the phone
   if(!TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
      Print("Push notifications are DISABLED. Enable them in Tools -> Options -> ",
            "Notifications and enter your MetaQuotes ID, or no alert will arrive.");
   else if(StartupPush)
     {
      if(SendNotification("Portfolio risk monitor started. You will get an alert when risk thresholds are crossed."))
         Print("Startup push sent. Check your phone.");
      else
         Print("Startup push FAILED, error ",GetLastError(),
               ". Check your MetaQuotes ID in Tools -> Options -> Notifications.");
     }

   CRiskEngine engine;
   engine.Configure(CorrTimeframe,CorrLookbackBars,VaRConfidenceZ);

   int      lastState=0;   // which thresholds were breached on the previous pass
   datetime lastAlert=0;   // when the last push went out
   long     chart=-1;      // chart the panel lives on
   int      sleepSec=(CheckSeconds<5 ? 5 : CheckSeconds);

   Print("Portfolio risk monitor running. Checking every ",sleepSec," seconds.");

   while(!IsStopped())
     {
      bool ok=(TerminalInfoInteger(TERMINAL_CONNECTED) && engine.Compute());

      //--- keep the panel on a live chart (charts can be closed under us)
      if(ShowPanel)
        {
         if(chart<0 || ChartPeriod(chart)==0)
            chart=FindChart();
         if(chart>=0)
            DrawPanel(chart,engine,ok);
        }

      if(ok)
        {
         int state=0;
         if(engine.HiddenFactor()>=AlertHiddenFactor)
            state|=F_HIDDEN;
         if(engine.RiskPctOfEquity()>=AlertRiskPctEquity)
            state|=F_RISK;
         if(100.0*engine.TopShare()>=AlertTopSharePct)
            state|=F_CONC;

         bool changed=(state!=lastState);
         bool cooled =((long)TimeCurrent()-(long)lastAlert >= (long)CooldownMinutes*60);

         //--- push only on a NEW breach state, or after the cooldown
         //--- if the breach persists (respects notification frequency limits)
         if(state!=0 && (changed || cooled))
           {
            string msg=BuildAlert(state,engine);
            if(SendNotification(msg))
               lastAlert=TimeCurrent();
            else
               Print("Push failed, error ",GetLastError());
            Print(msg);
           }
         if(state==0 && lastState!=0)
            Print("Back under thresholds. ",engine.Summary());
         lastState=state;
        }
      else
         lastState=0; // fewer than 2 positions or no data: a new breach later must alert again

      Sleep(1000*sleepSec);
     }

//--- clean the panel off the chart when the service stops
   if(chart>=0 && ChartPeriod(chart)>0)
     {
      ObjectsDeleteAll(chart,PFX);
      ChartRedraw(chart);
     }
   Print("Portfolio risk monitor stopped.");
  }
//+------------------------------------------------------------------+
