//+------------------------------------------------------------------+
//|                                             PlatinumCopier.mq5   |
//|  Copies the Elite Option Platinum signals to this MT5 account.   |
//|  The account owner runs it, sets the risk and can stop it at any  |
//|  time. Signals are read from the platform, levels come from the   |
//|  TradingView indicator alerts.                                    |
//+------------------------------------------------------------------+
#property copyright   "Elite Option Platinum"
#property link        "https://optionplatinum.com"
#property version     "1.11"
#property description "Elite Option Platinum signal copier for MT5."
#property description "1) Tools > Options > Expert Advisors > Allow WebRequest: add the site address."
#property description "2) Paste your copier key from the platform (Auto trading tab)."
#property description "3) Set your risk, then turn on Algo Trading."

#include <Trade\Trade.mqh>

//--- inputs
input string InpSite            = "https://optionplatinum.com"; // Platform address
input string InpKey             = "";       // Copier key (platform > Auto trading)
input double InpRiskPct         = 1.0;      // Risk per signal, % of balance (0 = fixed lot)
input double InpFixedLot        = 0.01;     // Total lot per signal when risk is 0
input int    InpParts           = 3;        // Positions per signal: 3 = TP1/TP2/TP3, 2 = TP1/TP2, 1 = TP2
input bool   InpMoveToBE        = true;     // Move stop to entry after TP1
input bool   InpSyncClose       = true;     // Close what is left when the signal closes on the platform
input bool   InpRelative        = true;     // Rebuild SL/TP from this broker's price (same distances)
input int    InpMaxAgeSec       = 300;      // Ignore signals older than this (seconds)
input double InpMaxDevR         = 0.35;     // Skip if price moved more than this x risk since the signal (0 = off)
input string InpSymbols         = "";       // Only these tickers, e.g. XAUUSD,EURUSD (empty = all)
input string InpTimeframes      = "";       // Only these timeframes, e.g. 15,60 (empty = all)
input string InpPrefix          = "";       // Broker symbol prefix
input string InpSuffix          = "";       // Broker symbol suffix, e.g. .m  or  pro
input string InpMap             = "";       // Custom names, e.g. XAUUSD=GOLD,NAS100USD=USTEC
input int    InpMaxOpen         = 6;        // Max signals open at the same time
input double InpMaxDailyLossPct = 5.0;      // No new trades after this daily loss % (0 = off)
input int    InpSignalTF        = 5;        // Smallest timeframe of the alerts, minutes (0 = 30-second alerts)
input long   InpMagic           = 7711777;  // Magic number
input int    InpSlippage        = 30;       // Max slippage, points
input double InpMaxLot          = 0.0;      // Max total lot per signal (0 = no limit)
input int    InpPanelSize       = 100;      // Panel size, % (100 = normal, 130 = bigger)
input int    InpPanelX          = 12;       // Panel position from the left, pixels
input int    InpPanelY          = 28;       // Panel position from the top, pixels

//--- one signal from the platform
struct KhSignal
  {
   string            sid;
   string            ticker;
   string            tf;
   bool              buy;
   double            entry;
   double            sl;
   double            tp1;
   double            tp2;
   double            tp3;
   long              recv;
   bool              open;
   string            status;
   int               hits;
  };

CTrade   gTrade;
string   gBase      = "";
string   gDayKey    = "";
long     gDayKeyAt  = 0;
string   gName      = "";
string   gUntil     = "";
string   gStatus    = "Starting...";
string   gLastSig   = "-";
string   gLastSkip  = "-";
int      gScale100  = 100;
string   PFX        = "EOP_";
bool     gTradingOn = true;
long     gLastPoll  = 0;
long     gBoundary  = -1;
int      gDoneMask  = 0;
long     gLastMgmt  = 0;
string   gMapFrom[];
string   gMapTo[];
int      gOffsets[5] = {4, 14, 40, 90, 200};
string   gVer       = "";
long     gLastFull  = 0;
int      gCopied    = 0;
int      gSkipped   = 0;
double   gInst      = 0.0;

//+------------------------------------------------------------------+
int OnInit()
  {
   gBase = InpSite;
   StringTrimLeft(gBase);
   StringTrimRight(gBase);
   while(StringLen(gBase) > 0 && StringSubstr(gBase, StringLen(gBase) - 1, 1) == "/")
      gBase = StringSubstr(gBase, 0, StringLen(gBase) - 1);
   gTrade.SetExpertMagicNumber((ulong)InpMagic);
   gTrade.SetDeviationInPoints((ulong)MathMax(0, InpSlippage));
   ArrayResize(gMapFrom, 0);
   ArrayResize(gMapTo, 0);
   if(StringLen(InpKey) < 10)
     {
      gStatus = "Paste your copier key from the platform (Auto trading tab).";
      Alert("Platinum Copier: paste your copier key in the inputs.");
     }
   double dpiScale = MathMax(1.0, (double)TerminalInfoInteger(TERMINAL_SCREEN_DPI) / 96.0);
   double userScale = MathMax(0.6, MathMin(2.5, (double)InpPanelSize / 100.0));
   gScale100 = (int)MathRound(dpiScale * userScale * 100.0);
   MathSrand((uint)GetTickCount());
   gInst = (double)(1 + MathRand()) * 100000.0 + (double)(GetTickCount() % 100000);
   EventSetTimer(1);
   DrawPanel();
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
   string lead = LeadName();
   if(GlobalVariableCheck(lead + ".c") && GlobalVariableGet(lead + ".c") == gInst)
      GlobalVariableDel(lead + ".c");
   ObjectsDeleteAll(0, PFX);
   Comment("");
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
void OnTick()
  {
  }

//+------------------------------------------------------------------+
void OnTimer()
  {
   long now = (long)TimeGMT();
   if(StringLen(InpKey) < 10)
     {
      DrawPanel();
      return;
     }
   //--- one robot per account: a second copy (another chart) stays idle
   if(!IsLeader())
     {
      gStatus = "Another Platinum robot runs on this account: this one waits.";
      DrawPanel();
      return;
     }
   if(now - gLastMgmt >= 2)
     {
      gLastMgmt = now;
      ManageBreakEven();
     }
   bool due = (gLastPoll == 0);
   long tfSec = TfSec();
   long phase = now % tfSec;
   long boundary = now - phase;
   if(boundary != gBoundary)
     {
      gBoundary = boundary;
      gDoneMask = 0;
     }
   for(int k = 4; k >= 0; k--)
     {
      if(phase >= gOffsets[k] && (gDoneMask & (1 << k)) == 0)
        {
         gDoneMask |= (1 << (k + 1)) - 1;
         due = true;
         break;
        }
     }
   if(due)
      Poll();
   DrawPanel();
  }

//--- seconds per candle of the smallest alert timeframe (0 = 30-second alerts)
long TfSec()
  {
   return (InpSignalTF <= 0) ? 30 : (long)InpSignalTF * 60;
  }

long MaxAgeSec()
  {
   return (long)InpMaxAgeSec + TfSec();
  }

string LeadName()
  {
   return "EOPlead" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN) % 1000000);
  }

bool IsLeader()
  {
   string base = LeadName();
   double now = (double)TimeGMT();
   double owner = GlobalVariableCheck(base + ".c") ? GlobalVariableGet(base + ".c") : 0.0;
   double beat = GlobalVariableCheck(base + ".t") ? GlobalVariableGet(base + ".t") : 0.0;
   if(owner != 0.0 && owner != gInst && now - beat < 20.0)
      return false;
   GlobalVariableSet(base + ".c", gInst);
   GlobalVariableSet(base + ".t", now);
   return true;
  }

//--- "5" -> "5m", "60" -> "1h", "1440" -> "1D", "30S" stays
string TfLabel(const string tf)
  {
   long n = StringToInteger(tf);
   if(n <= 0 || IntegerToString(n) != tf)
      return tf;
   if(n % 1440 == 0)
      return IntegerToString(n / 1440) + "D";
   if(n % 60 == 0)
      return IntegerToString(n / 60) + "h";
   return IntegerToString(n) + "m";
  }

//+------------------------------------------------------------------+
//| network                                                          |
//+------------------------------------------------------------------+
string UrlEncode(const string s)
  {
   uchar b[];
   int n = StringToCharArray(s, b, 0, WHOLE_ARRAY, CP_UTF8);
   string out = "";
   for(int i = 0; i < n; i++)
     {
      uchar c = b[i];
      if(c == 0)
         break;
      bool safe = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' || c == '%';
      if(safe)
         out += CharToString(c);
      else
         out += StringFormat("%%%02X", (int)c);
     }
   return out;
  }

bool HttpGet(const string url, string &body, int &code)
  {
   char post[];
   char res[];
   string resHeaders = "";
   ResetLastError();
   code = WebRequest("GET", url, "User-Agent: PlatinumCopier/1.1\r\n", 8000, post, res, resHeaders);
   if(code == -1)
     {
      int err = GetLastError();
      if(err == 4014)
         gStatus = "Allow WebRequest for " + gBase + " in Tools > Options > Expert Advisors.";
      else
         gStatus = "No connection (" + IntegerToString(err) + "). Retrying.";
      return false;
     }
   body = CharArrayToString(res, 0, WHOLE_ARRAY, CP_UTF8);
   return true;
  }

bool RefreshDayKey()
  {
   string url = gBase + "/.netlify/functions/copier?ck=" + UrlEncode(InpKey)
                + "&acct=" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))
                + "&broker=" + UrlEncode(AccountInfoString(ACCOUNT_COMPANY))
                + "&ver=1.11";
   string body = "";
   int code = 0;
   if(!HttpGet(url, body, code))
      return false;
   string f[];
   int n = StringSplit(body, '|', f);
   if(code != 200 || n < 3 || f[0] != "OK")
     {
      gDayKey = "";
      gStatus = "Key refused: check the key or the subscription date.";
      return false;
     }
   gDayKey = f[1];
   gUntil = f[2];
   gName = (n > 3 ? f[3] : "");
   StringReplace(gName, "\n", "");
   gDayKeyAt = (long)TimeGMT();
   return true;
  }

void Poll()
  {
   long now = (long)TimeGMT();
   gLastPoll = now;
   if(gDayKey == "" || now - gDayKeyAt > 3600)
      if(!RefreshDayKey())
         return;
   string body = "";
   int code = 0;
   //--- quick check first (a few bytes): the full list is downloaded only when something changed,
   //--- and at least every 5 minutes
   if(gVer != "" && now - gLastFull < 300)
     {
      if(!HttpGet(gBase + "/.netlify/functions/copier?v=1&d=" + UrlEncode(gDayKey), body, code))
         return;
      if(code == 403)
        {
         gDayKey = "";
         gStatus = "Access renewed, reconnecting...";
         return;
        }
      if(code == 200 && StringFind(body, "KHV|") == 0)
        {
         string pv[];
         int pn = StringSplit(body, '|', pv);
         if(pn > 2)
           {
            string v = pv[1];
            gTradingOn = (StringToInteger(pv[2]) == 1);
            gStatus = gTradingOn ? "Connected" : "Connected - new trades paused by the platform";
            if(v == gVer)
               return;
           }
        }
     }
   body = "";
   if(!HttpGet(gBase + "/.netlify/functions/copier?d=" + UrlEncode(gDayKey), body, code))
      return;
   if(code == 403)
     {
      gDayKey = "";
      gStatus = "Access renewed, reconnecting...";
      return;
     }
   if(code != 200)
     {
      gStatus = "Platform answered " + IntegerToString(code) + ". Retrying.";
      return;
     }
   string lines[];
   int n = StringSplit(body, '\n', lines);
   if(n < 1 || StringFind(lines[0], "KH1|") != 0)
     {
      gStatus = "Unexpected answer from the platform.";
      return;
     }
   string head[];
   StringReplace(lines[0], "\r", "");
   int hn = StringSplit(lines[0], '|', head);
   gTradingOn = (hn > 2 && StringToInteger(head[2]) == 1);
   gVer = (hn > 3) ? head[3] : "";
   gLastFull = now;
   KhSignal sigs[];
   int count = 0;
   ArrayResize(sigs, n);
   for(int i = 1; i < n; i++)
      if(ParseLine(lines[i], sigs[count]))
         count++;
   ArrayResize(sigs, count);
   gStatus = gTradingOn ? "Connected" : "Connected - new trades paused by the platform";
   //--- closes first (a flip closes the old signal and opens the new one)
   if(InpSyncClose)
      for(int i = 0; i < count; i++)
         if(!sigs[i].open)
            CloseSignal(sigs[i].sid);
   //--- a fresh signal that hit its target, stop or flip before this robot could read it: say so once
   for(int i = count - 1; i >= 0; i--)
      if(!sigs[i].open && now - sigs[i].recv <= MaxAgeSec() && !GlobalVariableCheck(DoneName(sigs[i].sid)))
         Skip(sigs[i], "closed on the platform (" + sigs[i].status + ") before the robot read it");
   //--- then entries, oldest first
   for(int i = count - 1; i >= 0; i--)
      if(sigs[i].open)
         TryEnter(sigs[i]);
  }

bool ParseLine(string line, KhSignal &s)
  {
   StringReplace(line, "\r", "");
   string f[];
   int n = StringSplit(line, '|', f);
   if(n < 14 || f[0] != "S")
      return false;
   s.sid    = StringSubstr(f[1], 0, 10);
   s.ticker = f[2];
   StringToUpper(s.ticker);
   s.tf     = f[3];
   s.buy    = (f[4] == "buy");
   s.entry  = StringToDouble(f[5]);
   s.sl     = StringToDouble(f[6]);
   s.tp1    = StringToDouble(f[7]);
   s.tp2    = StringToDouble(f[8]);
   s.tp3    = StringToDouble(f[9]);
   s.recv   = StringToInteger(f[10]);
   s.open   = (f[11] == "1");
   s.status = f[12];
   s.hits   = (int)StringToInteger(f[13]);
   return (StringLen(s.sid) >= 6 && s.entry > 0 && s.sl > 0);
  }

//+------------------------------------------------------------------+
//| entries                                                          |
//+------------------------------------------------------------------+
string DoneName(const string sid)
  {
   return "EOP" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN) % 1000000) + "." + sid;
  }

void MarkDone(const string sid, const string why)
  {
   GlobalVariableSet(DoneName(sid), (double)TimeGMT());
   if(why != "")
      Print("PLATINUM ", sid, ": ", why);
  }

bool InList(const string csv, const string item)
  {
   if(StringLen(csv) == 0)
      return true;
   string f[];
   int n = StringSplit(csv, ',', f);
   for(int i = 0; i < n; i++)
     {
      string x = f[i];
      StringTrimLeft(x);
      StringTrimRight(x);
      StringToUpper(x);
      string y = item;
      StringToUpper(y);
      if(x == y)
         return true;
     }
   return false;
  }

//--- a signal this robot will not take: remembered, logged and shown on the panel
void Skip(const KhSignal &s, const string why)
  {
   MarkDone(s.sid, s.ticker + " " + (s.buy ? "BUY" : "SELL") + " " + TfLabel(s.tf) + ": " + why);
   gLastSkip = s.ticker + " " + TfLabel(s.tf) + ": " + why;
   gSkipped++;
  }

//--- this broker's own price when the signal arrived (open of the M1 bar of that minute).
//--- Feeds differ between TradingView and the broker, so the drift is measured on the broker's chart.
double BrokerRef(const string sym, const long recvUtc, const double fallback)
  {
   long off = (long)MathRound((double)((long)TimeTradeServer() - (long)TimeGMT()) / 1800.0) * 1800;
   datetime at = (datetime)(recvUtc + off);
   int sh = iBarShift(sym, PERIOD_M1, at, false);
   if(sh < 0)
      return fallback;
   datetime bt = iTime(sym, PERIOD_M1, sh);
   if(bt <= 0 || MathAbs((double)((long)at - (long)bt)) > 180.0)
      return fallback;
   double o = iOpen(sym, PERIOD_M1, sh);
   return (o > 0.0 ? o : fallback);
  }

void TryEnter(KhSignal &s)
  {
   string done = DoneName(s.sid);
   if(GlobalVariableCheck(done))
      return;
   long now = (long)TimeGMT();
   //--- the robot reads the platform once per candle of the smallest alert timeframe,
   //--- so a fresh signal can be up to one candle old when it is first seen
   if(now - s.recv > MaxAgeSec())
     {
      Skip(s, "too old for entry");
      return;
     }
   if(!InList(InpSymbols, s.ticker) || !InList(InpTimeframes, s.tf))
     {
      Skip(s, "filtered by your settings");
      return;
     }
   if(!gTradingOn)
      return;
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || !MQLInfoInteger(MQL_TRADE_ALLOWED))
     {
      gStatus = "Turn on Algo Trading to copy the signals.";
      return;
     }
   if(DailyLossHit())
     {
      gStatus = "Daily loss limit reached: no new trades today.";
      return;
     }
   if(OpenSignals() >= InpMaxOpen)
     {
      Skip(s, "max open signals reached");
      return;
     }
   string sym = ResolveSymbol(s.ticker);
   if(sym == "")
     {
      Skip(s, "symbol not found, add it in Custom names");
      return;
     }
   double risk = MathAbs(s.entry - s.sl);
   if(risk <= 0.0)
     {
      Skip(s, "invalid stop");
      return;
     }
   double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
   double bid = SymbolInfoDouble(sym, SYMBOL_BID);
   if(ask <= 0.0 || bid <= 0.0)
      return;
   double price = s.buy ? ask : bid;
   double dir = s.buy ? 1.0 : -1.0;
   double ref = BrokerRef(sym, s.recv, s.entry);
   if(InpMaxDevR > 0.0 && MathAbs(price - ref) > InpMaxDevR * risk)
     {
      Skip(s, "price moved too far since the signal");
      return;
     }
   //--- levels
   double sl = 0.0;
   double tp[3];
   if(InpRelative)
     {
      sl = price - dir * risk;
      tp[0] = price + dir * MathAbs(s.tp1 - s.entry);
      tp[1] = price + dir * MathAbs(s.tp2 - s.entry);
      tp[2] = price + dir * MathAbs(s.tp3 - s.entry);
     }
   else
     {
      sl = s.sl;
      tp[0] = s.tp1;
      tp[1] = s.tp2;
      tp[2] = s.tp3;
      if((s.buy && (price <= sl || price >= tp[0])) || (!s.buy && (price >= sl || price <= tp[0])))
        {
         Skip(s, "price already beyond the stop or TP1");
         return;
        }
     }
   int digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
   double point = SymbolInfoDouble(sym, SYMBOL_POINT);
   double minDist = (double)SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL) * point;
   double spread = ask - bid;
   if(MathAbs(price - sl) <= MathMax(minDist, spread))
     {
      Skip(s, "stop closer than the broker allows");
      return;
     }
   sl = NormalizeDouble(sl, digits);
   for(int k = 0; k < 3; k++)
      tp[k] = NormalizeDouble(tp[k], digits);
   //--- size
   double lots = InpFixedLot;
   if(InpRiskPct > 0.0)
     {
      double pl = 0.0;
      ENUM_ORDER_TYPE ot = s.buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
      if(!OrderCalcProfit(ot, sym, 1.0, price, sl, pl) || pl >= 0.0)
        {
         Skip(s, "could not price the risk on " + sym);
         return;
        }
      double money = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPct / 100.0;
      lots = money / (-pl);
      double minLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
      if(lots < minLot)
        {
         if(minLot * (-pl) <= money * 1.5)
            lots = minLot;
         else
           {
            Skip(s, "balance too small for the minimum lot");
            return;
           }
        }
     }
   if(InpMaxLot > 0.0 && lots > InpMaxLot)
      lots = InpMaxLot;
   int parts = (int)MathMax(1, MathMin(3, InpParts));
   double per = NormalizeLot(sym, lots / parts);
   while(parts > 1 && per < SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN))
     {
      parts--;
      per = NormalizeLot(sym, lots / parts);
     }
   if(per < SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN))
      per = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   //--- send
   gTrade.SetTypeFillingBySymbol(sym);
   int opened = 0;
   uint lastRc = 0;
   for(int k = 1; k <= parts; k++)
     {
      double t = (parts == 1) ? tp[1] : tp[k - 1];
      string cm = "EP|" + s.sid + "|" + s.tf + "|" + IntegerToString(k) + "/" + IntegerToString(parts);
      bool ok = s.buy ? gTrade.Buy(per, sym, 0.0, sl, t, cm) : gTrade.Sell(per, sym, 0.0, sl, t, cm);
      uint rc = gTrade.ResultRetcode();
      if(ok && (rc == TRADE_RETCODE_DONE || rc == TRADE_RETCODE_PLACED || rc == TRADE_RETCODE_DONE_PARTIAL))
         opened++;
      else
        {
         lastRc = rc;
         Print("PLATINUM ", s.sid, ": order failed ", IntegerToString((int)rc), " ", gTrade.ResultRetcodeDescription());
        }
     }
   if(opened > 0)
     {
      MarkDone(s.sid, "opened " + IntegerToString(opened) + " position(s) on " + sym);
      gLastSig = s.ticker + " " + (s.buy ? "BUY" : "SELL") + " " + TfLabel(s.tf) + " -> " + sym + " " + DoubleToString(per * opened, 2) + " lot";
      gCopied++;
     }
   else
     {
      //--- a requote or a busy server: try again at the next check; anything else: give up on this signal
      bool retry = (lastRc == TRADE_RETCODE_REQUOTE || lastRc == TRADE_RETCODE_PRICE_CHANGED || lastRc == TRADE_RETCODE_PRICE_OFF
                    || lastRc == TRADE_RETCODE_TOO_MANY_REQUESTS || lastRc == TRADE_RETCODE_CONNECTION || lastRc == TRADE_RETCODE_TIMEOUT);
      if(retry)
         gLastSkip = s.ticker + " " + TfLabel(s.tf) + ": order refused (" + IntegerToString((int)lastRc) + "), retrying";
      else
         Skip(s, "broker refused the order (" + IntegerToString((int)lastRc) + ")");
     }
  }

double NormalizeLot(const string sym, double v)
  {
   double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
   double mx = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   if(step <= 0.0)
      step = 0.01;
   double r = MathFloor(v / step + 1e-9) * step;
   if(r > mx)
      r = mx;
   int d = (int)MathMax(0.0, MathRound(-MathLog10(step)));
   return NormalizeDouble(r, d);
  }

//+------------------------------------------------------------------+
//| positions of this robot                                          |
//+------------------------------------------------------------------+
bool ReadComment(const string cm, string &sid, int &part)
  {
   string f[];
   int n = StringSplit(cm, '|', f);
   if(n < 4 || f[0] != "EP")
      return false;
   sid = f[1];
   int slash = StringFind(f[3], "/");
   part = (int)StringToInteger(slash > 0 ? StringSubstr(f[3], 0, slash) : f[3]);
   return true;
  }

int OpenSignals()
  {
   string seen[];
   int count = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong tk = PositionGetTicket(i);
      if(tk == 0 || PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      string sid = "";
      int part = 0;
      if(!ReadComment(PositionGetString(POSITION_COMMENT), sid, part))
         continue;
      bool dup = false;
      for(int j = 0; j < count; j++)
         if(seen[j] == sid)
            dup = true;
      if(!dup)
        {
         ArrayResize(seen, count + 1);
         seen[count] = sid;
         count++;
        }
     }
   return count;
  }

void CloseSignal(const string sid)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong tk = PositionGetTicket(i);
      if(tk == 0 || PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      string psid = "";
      int part = 0;
      if(!ReadComment(PositionGetString(POSITION_COMMENT), psid, part) || psid != sid)
         continue;
      if(gTrade.PositionClose(tk))
         Print("PLATINUM ", sid, ": closed with the signal on the platform");
     }
  }

void ManageBreakEven()
  {
   if(!InpMoveToBE)
      return;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong tk = PositionGetTicket(i);
      if(tk == 0 || PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      string sid = "";
      int part = 0;
      if(!ReadComment(PositionGetString(POSITION_COMMENT), sid, part) || part <= 1)
         continue;
      if(PartOneOpen(sid))
         continue;
      if(!PositionSelectByTicket(tk))    // PartOneOpen walked the other positions
         continue;
      string sym = PositionGetString(POSITION_SYMBOL);
      double open = PositionGetDouble(POSITION_PRICE_OPEN);
      double sl = PositionGetDouble(POSITION_SL);
      double tp = PositionGetDouble(POSITION_TP);
      bool isBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
      double point = SymbolInfoDouble(sym, SYMBOL_POINT);
      if((isBuy && sl >= open - point) || (!isBuy && sl > 0.0 && sl <= open + point))
         continue;
      double minDist = (double)SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL) * point;
      double cur = isBuy ? SymbolInfoDouble(sym, SYMBOL_BID) : SymbolInfoDouble(sym, SYMBOL_ASK);
      if((isBuy && cur - open <= minDist) || (!isBuy && open - cur <= minDist))
         continue;
      if(gTrade.PositionModify(tk, NormalizeDouble(open, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS)), tp))
         Print("PLATINUM ", sid, ": TP1 done, stop moved to entry");
     }
  }

bool PartOneOpen(const string sid)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong tk = PositionGetTicket(i);
      if(tk == 0 || PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      string psid = "";
      int part = 0;
      if(ReadComment(PositionGetString(POSITION_COMMENT), psid, part) && psid == sid && part == 1)
         return true;
     }
   return false;
  }

//+------------------------------------------------------------------+
//| daily loss guard                                                 |
//+------------------------------------------------------------------+
bool DailyLossHit()
  {
   if(InpMaxDailyLossPct <= 0.0)
      return false;
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   double today = (double)(dt.year * 10000 + dt.mon * 100 + dt.day);
   string base = "EOPday" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN) % 1000000);
   if(!GlobalVariableCheck(base + ".d") || GlobalVariableGet(base + ".d") != today)
     {
      GlobalVariableSet(base + ".d", today);
      GlobalVariableSet(base + ".b", AccountInfoDouble(ACCOUNT_BALANCE));
     }
   double start = GlobalVariableGet(base + ".b");
   if(start <= 0.0)
      return false;
   double eq = AccountInfoDouble(ACCOUNT_EQUITY);
   return ((start - eq) / start * 100.0 >= InpMaxDailyLossPct);
  }

//+------------------------------------------------------------------+
//| broker symbol names                                              |
//+------------------------------------------------------------------+
void AddCand(string &list[], const string s)
  {
   if(s == "")
      return;
   int n = ArraySize(list);
   for(int i = 0; i < n; i++)
      if(list[i] == s)
         return;
   ArrayResize(list, n + 1);
   list[n] = s;
  }

//--- names the same market goes by on TradingView and at the brokers (both directions)
string gAlias[] =
  {
   "XAUUSD,GOLD,XAU,GOLDUSD,GC1!,GC,MGC1!",
   "XAGUSD,SILVER,XAG,SILVERUSD,SI1!,SI,SIL1!",
   "NAS100,USTEC,US100,NAS100USD,NDX,NDX100,USTECH,NQ1!,NQ,MNQ1!,NASDAQ",
   "US30,DJ30,DJI30,WS30,DOW30,US30USD,DJI,YM1!,YM,MYM1!",
   "US500,SPX500,SP500,SPX500USD,SPX,ES1!,ES,MES1!,US500USD",
   "GER40,DE40,DAX40,GER30,DE30EUR,DE40EUR,DAX,FDAX1!",
   "UK100,FTSE100,UK100GBP,FTSE",
   "JP225,JPN225,JP225USD,NI225,NIKKEI",
   "USOIL,WTI,XTIUSD,CL-OIL,WTICOUSD,USOUSD,CL1!,CRUDE",
   "UKOIL,BRENT,XBRUSD,BCOUSD,UKOUSD,BRN1!",
   "BTCUSD,BTCUSDT,XBTUSD,BTC",
   "ETHUSD,ETHUSDT,ETH",
   "XNGUSD,NATGAS,NGAS,NG1!",
   "COPPER,XCUUSD,HG1!"
  };

void Candidates(const string t, string &c[])
  {
   ArrayResize(c, 0);
   //--- the trader's own names first
   if(StringLen(InpMap) > 0)
     {
      string pairs[];
      int n = StringSplit(InpMap, ',', pairs);
      for(int i = 0; i < n; i++)
        {
         string kv[];
         if(StringSplit(pairs[i], '=', kv) == 2)
           {
            string k = kv[0];
            string v = kv[1];
            StringTrimLeft(k);
            StringTrimRight(k);
            StringTrimLeft(v);
            StringTrimRight(v);
            StringToUpper(k);
            if(k == t)
               AddCand(c, v);
           }
        }
     }
   AddCand(c, t);
   int groups = ArraySize(gAlias);
   for(int g = 0; g < groups; g++)
     {
      string names[];
      int n = StringSplit(gAlias[g], ',', names);
      bool member = false;
      for(int i = 0; i < n && !member; i++)
         if(names[i] == t)
            member = true;
      if(member)
         for(int i = 0; i < n; i++)
            AddCand(c, names[i]);
     }
   int L = StringLen(t);
   if(L > 4 && StringSubstr(t, L - 4) == "USDT")
      AddCand(c, StringSubstr(t, 0, L - 1));
  }

bool Usable(const string name)
  {
   if(!SymbolSelect(name, true))
      return false;
   long mode = SymbolInfoInteger(name, SYMBOL_TRADE_MODE);
   return (mode == SYMBOL_TRADE_MODE_FULL || mode == SYMBOL_TRADE_MODE_LONGONLY || mode == SYMBOL_TRADE_MODE_SHORTONLY);
  }

string ResolveSymbol(const string ticker)
  {
   int n = ArraySize(gMapFrom);
   for(int i = 0; i < n; i++)
      if(gMapFrom[i] == ticker)
         return gMapTo[i];
   string cand[];
   Candidates(ticker, cand);
   string found = "";
   int cn = ArraySize(cand);
   for(int i = 0; i < cn && found == ""; i++)
     {
      string a = InpPrefix + cand[i] + InpSuffix;
      if(Usable(a))
         found = a;
      else
         if(Usable(cand[i]))
            found = cand[i];
     }
   //--- same name in other letters (some servers list wti, xauusd, gbpjpy in small letters)
   if(found == "")
     {
      int total = SymbolsTotal(false);
      for(int i = 0; i < cn && found == ""; i++)
        {
         string w1 = InpPrefix + cand[i] + InpSuffix;
         string w2 = cand[i];
         StringToUpper(w1);
         StringToUpper(w2);
         for(int j = 0; j < total && found == ""; j++)
           {
            string nm = SymbolName(j, false);
            string up = nm;
            StringToUpper(up);
            if((up == w1 || up == w2) && Usable(nm))
               found = nm;
           }
        }
     }
   //--- last try: a broker name that starts with the ticker (XAUUSD.r, XAUUSDm ...)
   if(found == "")
     {
      int total = SymbolsTotal(false);
      for(int i = 0; i < cn && found == ""; i++)
         for(int j = 0; j < total && found == "" && StringLen(cand[i]) >= 4; j++)
           {
            string nm = SymbolName(j, false);
            string up = nm;
            StringToUpper(up);
            //--- ignore a leading # . or _ (stock CFDs like #AAPL)
            while(StringLen(up) > 0 && (StringGetCharacter(up, 0) == '#' || StringGetCharacter(up, 0) == '.' || StringGetCharacter(up, 0) == '_'))
               up = StringSubstr(up, 1);
            string want = InpPrefix + cand[i];
            StringToUpper(want);
            if(StringFind(up, want) == 0 && StringLen(up) <= StringLen(want) + 4 && Usable(nm))
               found = nm;
           }
     }
   if(found != "")
     {
      ArrayResize(gMapFrom, n + 1);
      ArrayResize(gMapTo, n + 1);
      gMapFrom[n] = ticker;
      gMapTo[n] = found;
     }
   return found;
  }

//+------------------------------------------------------------------+
//| panel: dark navy card with platinum frame, sized for the DPI     |
//+------------------------------------------------------------------+
#define KH_GOLD   C'206,214,226'
#define KH_BG     C'6,11,22'
#define KH_HEAD   C'10,24,48'
#define KH_LINE   C'28,46,78'
#define KH_MUTED  C'138,146,166'
#define KH_TEXT   C'232,234,240'
#define KH_GREEN  C'38,214,140'
#define KH_RED    C'255,92,110'
#define KH_AMBER  C'255,190,70'
#define KH_CYAN   C'92,200,255'
#define KH_BLUE   C'61,155,255'

int Px(const int v)
  {
   return (int)MathRound(v * gScale100 / 100.0);
  }

int Pt(const int v)
  {
   return (int)MathMax(7.0, MathRound(v * MathMax(0.6, MathMin(2.5, (double)InpPanelSize / 100.0))));
  }

void PRect(const string id, const int x, const int y, const int w, const int h, const color bg, const color border)
  {
   string n = PFX + id;
   if(ObjectFind(0, n) < 0)
     {
      ObjectCreate(0, n, OBJ_RECTANGLE_LABEL, 0, 0, 0);
      ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_UPPER);
      ObjectSetInteger(0, n, OBJPROP_BORDER_TYPE, BORDER_FLAT);
      ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, n, OBJPROP_BACK, false);
     }
   ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, n, OBJPROP_XSIZE, w);
   ObjectSetInteger(0, n, OBJPROP_YSIZE, h);
   ObjectSetInteger(0, n, OBJPROP_BGCOLOR, bg);
   ObjectSetInteger(0, n, OBJPROP_COLOR, border);
   ObjectSetInteger(0, n, OBJPROP_WIDTH, 1);
  }

void PText(const string id, const int x, const int y, string txt, const color clr, const int size, const string font)
  {
   string n = PFX + id;
   if(ObjectFind(0, n) < 0)
     {
      ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_UPPER);
      ObjectSetInteger(0, n, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
      ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, n, OBJPROP_BACK, false);
     }
   if(StringLen(txt) > 60)
      txt = StringSubstr(txt, 0, 59) + "~";
   if(txt == "")
      txt = " ";
   ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y);
   ObjectSetString(0, n, OBJPROP_TEXT, txt);
   ObjectSetString(0, n, OBJPROP_FONT, font);
   ObjectSetInteger(0, n, OBJPROP_FONTSIZE, size);
   ObjectSetInteger(0, n, OBJPROP_COLOR, clr);
  }

//--- one row: grey label on the left, coloured value on the right
void PRow(const int row, const int x0, const int y0, const string label, const string value, const color clr)
  {
   int y = y0 + Px(48) + row * Px(22);
   PText("l" + IntegerToString(row), x0 + Px(14), y, label, KH_MUTED, Pt(9), "Segoe UI");
   PText("v" + IntegerToString(row), x0 + Px(122), y, value, clr, Pt(10), "Segoe UI Semibold");
  }

double RobotPnL()
  {
   double sum = 0.0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong tk = PositionGetTicket(i);
      if(tk == 0 || PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      sum += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
     }
   return sum;
  }

//--- seconds until the next read of the platform
long NextCheckIn()
  {
   long now = (long)TimeGMT();
   long tfSec = TfSec();
   long phase = now % tfSec;
   for(int k = 0; k < 5; k++)
      if(gOffsets[k] < tfSec && phase < gOffsets[k])
         return gOffsets[k] - phase;
   return tfSec - phase + gOffsets[0];
  }

void DrawPanel()
  {
   int x0 = InpPanelX;
   int y0 = InpPanelY;
   int w = Px(380);
   int rows = 10;
   int h = Px(48) + rows * Px(22) + Px(34);
   bool ok = (StringFind(gStatus, "Connected") == 0);
   bool paused = (StringFind(gStatus, "paused") >= 0 || !gTradingOn);
   color stCol = ok ? (paused ? KH_AMBER : KH_GREEN) : KH_RED;

   PRect("bg", x0, y0, w, h, KH_BG, KH_GOLD);
   PRect("head", x0 + 1, y0 + 1, w - 2, Px(38), KH_HEAD, KH_HEAD);
   PRect("rule", x0 + 1, y0 + Px(39), w - 2, 1, KH_BLUE, KH_BLUE);
   PText("title", x0 + Px(14), y0 + Px(9), "ELITE OPTION PLATINUM", KH_GOLD, Pt(11), "Segoe UI Black");
   PText("ver", x0 + w - Px(98), y0 + Px(12), "v1.11  " + (ok ? "LIVE" : "OFF"), ok ? KH_GREEN : KH_RED, Pt(9), "Segoe UI Semibold");

   //--- status may be long: two lines when needed
   string st1 = gStatus;
   string st2 = "";
   if(StringLen(st1) > 34)
     {
      int cut = 34;
      for(int i = 34; i > 18; i--)
         if(StringSubstr(st1, i, 1) == " ")
           {
            cut = i;
            break;
           }
      st2 = StringSubstr(st1, cut + 1);
      st1 = StringSubstr(st1, 0, cut);
     }
   PRow(0, x0, y0, "Status", (ok ? ShortToString(0x25CF) + " " : "") + st1, stCol);
   PRow(1, x0, y0, "", st2, stCol);

   string member = (gName == "" ? "-" : gName) + (gUntil == "" ? "" : "  |  until " + gUntil);
   PRow(2, x0, y0, "Member", member, KH_TEXT);
   string broker = AccountInfoString(ACCOUNT_COMPANY);
   if(StringLen(broker) > 22)
      broker = StringSubstr(broker, 0, 22);
   PRow(3, x0, y0, "Account", IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + "  |  " + broker, KH_TEXT);
   string risk = (InpRiskPct > 0.0 ? DoubleToString(InpRiskPct, 2) + "% per signal" : DoubleToString(InpFixedLot, 2) + " lot per signal")
                 + (InpMaxLot > 0.0 ? "  |  max " + DoubleToString(InpMaxLot, 2) : "");
   PRow(4, x0, y0, "New trades", (gTradingOn ? "ON" : "PAUSED") + "  |  " + risk, gTradingOn ? KH_GREEN : KH_AMBER);
   double pnl = RobotPnL();
   PRow(5, x0, y0, "Open signals", IntegerToString(OpenSignals()) + " / " + IntegerToString(InpMaxOpen) + "  |  P/L " + (pnl >= 0.0 ? "+" : "") + DoubleToString(pnl, 2) + " " + AccountInfoString(ACCOUNT_CURRENCY),
        pnl > 0.0 ? KH_GREEN : (pnl < 0.0 ? KH_RED : KH_TEXT));
   string last = (gLastPoll > 0) ? TimeToString((datetime)gLastPoll, TIME_MINUTES | TIME_SECONDS) + " GMT" : "-";
   long nx = NextCheckIn();
   PRow(6, x0, y0, "Last check", last + "  |  next in " + IntegerToString(nx / 60) + "m " + IntegerToString(nx % 60) + "s", KH_TEXT);
   PRow(7, x0, y0, "Last copy", gLastSig, KH_CYAN);
   PRow(8, x0, y0, "Last skip", gLastSkip, gLastSkip == "-" ? KH_MUTED : KH_AMBER);
   string every = (InpSignalTF <= 0) ? "30s" : TfLabel(IntegerToString(InpSignalTF));
   PRow(9, x0, y0, "This session", "copied " + IntegerToString(gCopied) + "  |  skipped " + IntegerToString(gSkipped) + "  |  reads every " + every, KH_TEXT);

   PRect("rule2", x0 + 1, y0 + h - Px(28), w - 2, 1, KH_LINE, KH_LINE);
   PText("foot", x0 + Px(14), y0 + h - Px(22), "Details of every signal: Toolbox > Experts tab", KH_MUTED, Pt(8), "Segoe UI");
   ChartRedraw(0);
  }
//+------------------------------------------------------------------+
