//+------------------------------------------------------------------+
//|                                  AlgoVerdictPublisher_v1.0.mq5   |
//|                                                                  |
//|  AlgoVerdict Publisher v1.0                                      |
//|  built by Pitt Petruschke (PiP) — AlgoVerdict.com                |
//|                                                                  |
//|  Publishes read-only account statistics (balance, equity, open   |
//|  positions, closed-deal history) from YOUR OWN terminal to your  |
//|  AlgoVerdict dashboard. No passwords involved — not even the     |
//|  investor password. The EA never trades, never modifies orders,  |
//|  and only sends data OUT.                                        |
//|                                                                  |
//|  SETUP (3 steps):                                                |
//|   1. Compile this file in MetaEditor (F7) and attach it to ANY   |
//|      chart (the symbol does not matter).                         |
//|   2. In MT5: Tools -> Options -> Expert Advisors -> enable       |
//|      "Allow WebRequest for listed URL" and add:                  |
//|          https://algoverdict.com                                 |
//|   3. Paste your per-account token (from algoverdict.com ->       |
//|      Accounts -> EA setup) into the InpToken input. Done.        |
//|                                                                  |
//|  License: free to use for publishing to AlgoVerdict.com          |
//|  (MIT-style — no warranty; use at your own risk).                |
//+------------------------------------------------------------------+
#property copyright "Pitt Petruschke (PiP) — AlgoVerdict.com"
#property link      "https://algoverdict.com"
#property version   "1.0"
#property description "AlgoVerdict Publisher v1.0 — pushes read-only account stats to your AlgoVerdict dashboard. No passwords, no trading."

//--- inputs -----------------------------------------------------------------
input string InpToken           = "";                                      // AlgoVerdict account token
input int    InpPushIntervalMin = 15;                                      // Push interval (minutes)
input string InpEndpoint        = "https://algoverdict.com/api/ea/push";   // Endpoint (leave default)

//--- constants --------------------------------------------------------------
// Max closed deals per push. The server caps at the same number; if the
// account has more history, the OLDEST deals are dropped and the growth curve
// simply starts at the oldest retained deal.
#define AVPUB_MAX_DEALS     5000
// Max open positions per push (server cap).
#define AVPUB_MAX_POSITIONS 200
// Persisted last-successful-push timestamp (survives EA re-attach / restart).
#define AVPUB_GV_PREFIX     "AVPUB_lastPush_"

//--- state ------------------------------------------------------------------
datetime g_lastPush = 0;   // last SUCCESSFUL push (server accepted)
datetime g_lastTry  = 0;   // last attempt (success or not) — retry pacing

//+------------------------------------------------------------------+
//| Initialization                                                    |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(StringLen(InpToken) < 16)
     {
      Print("AlgoVerdict Publisher: ERROR — no token set. ",
            "Paste your account token from algoverdict.com -> Accounts -> EA setup ",
            "into the 'InpToken' input.");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(InpPushIntervalMin < 1)
     {
      Print("AlgoVerdict Publisher: ERROR — push interval must be >= 1 minute.");
      return(INIT_PARAMETERS_INCORRECT);
     }

   // Restore the last successful push time (per account login).
   string gv = AVPUB_GV_PREFIX + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN));
   if(GlobalVariableCheck(gv))
      g_lastPush = (datetime)(long)GlobalVariableGet(gv);

   EventSetTimer(60); // check once per minute; push when the interval elapsed

   Print("AlgoVerdict Publisher v1.0 ready — account ",
         IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)),
         ", pushing every ", IntegerToString(InpPushIntervalMin),
         " min to ", InpEndpoint);
   Print("AlgoVerdict Publisher — built by Pitt Petruschke (PiP), algoverdict.com");
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Deinitialization                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }

//+------------------------------------------------------------------+
//| Timer: push when the configured interval has elapsed              |
//+------------------------------------------------------------------+
void OnTimer()
  {
   datetime now = TimeCurrent();
   // Pace retries: never attempt more than once per 60 s (server rate limit).
   if(g_lastTry != 0 && (long)(now - g_lastTry) < 60)
      return;
   if(g_lastPush != 0 && (long)(now - g_lastPush) < (long)InpPushIntervalMin * 60)
      return;
   g_lastTry = now;
   CollectAndPush();
  }

//+------------------------------------------------------------------+
//| Escape a string for safe embedding in JSON                        |
//+------------------------------------------------------------------+
string JsonEscape(string s)
  {
   StringReplace(s, "\\", "\\\\");
   StringReplace(s, "\"", "\\\"");
   StringReplace(s, "\r", "\\r");
   StringReplace(s, "\n", "\\n");
   StringReplace(s, "\t", "\\t");
   return(s);
  }

//+------------------------------------------------------------------+
//| Format a datetime as ISO-8601 ("YYYY-MM-DDTHH:MM:SSZ").           |
//| Note: broker-server time, labelled Z for stable lexicographic     |
//| ordering — AlgoVerdict uses these for ordering/day-bucketing only.|
//+------------------------------------------------------------------+
string IsoTime(datetime t)
  {
   string s = TimeToString(t, TIME_DATE | TIME_SECONDS); // "2026.06.12 10:30:00"
   StringReplace(s, ".", "-");
   StringReplace(s, " ", "T");
   return(s + "Z");
  }

//+------------------------------------------------------------------+
//| Is this history deal a CLOSING trade deal (realized P&L)?         |
//+------------------------------------------------------------------+
bool IsClosingTradeDeal(const ulong ticket)
  {
   long dtype = HistoryDealGetInteger(ticket, DEAL_TYPE);
   if(dtype != DEAL_TYPE_BUY && dtype != DEAL_TYPE_SELL)
      return(false);
   long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
   return(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_INOUT || entry == DEAL_ENTRY_OUT_BY);
  }

//+------------------------------------------------------------------+
//| Is this history deal a balance operation (deposit/withdrawal)?    |
//+------------------------------------------------------------------+
bool IsBalanceDeal(const ulong ticket)
  {
   long dtype = HistoryDealGetInteger(ticket, DEAL_TYPE);
   return(dtype == DEAL_TYPE_BALANCE || dtype == DEAL_TYPE_CREDIT);
  }

//+------------------------------------------------------------------+
//| Build the JSON object for ONE closed/balance deal.                |
//| Per the AlgoVerdict push contract, `profit` already includes      |
//| swap + commission (DEAL_PROFIT + DEAL_SWAP + DEAL_COMMISSION).    |
//+------------------------------------------------------------------+
string DealJson(const ulong ticket)
  {
   long   dtype  = HistoryDealGetInteger(ticket, DEAL_TYPE);
   string dealType;
   if(dtype == DEAL_TYPE_BUY)          dealType = "buy";
   else if(dtype == DEAL_TYPE_SELL)    dealType = "sell";
   else if(dtype == DEAL_TYPE_BALANCE) dealType = "balance";
   else                                dealType = "credit";

   double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
                 + HistoryDealGetDouble(ticket, DEAL_SWAP)
                 + HistoryDealGetDouble(ticket, DEAL_COMMISSION);

   string json = "{";
   json += "\"id\":\""     + (string)ticket + "\",";
   json += "\"time\":\""   + IsoTime((datetime)HistoryDealGetInteger(ticket, DEAL_TIME)) + "\",";
   json += "\"type\":\""   + dealType + "\",";
   json += "\"profit\":"   + DoubleToString(profit, 2);

   string symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
   if(StringLen(symbol) > 0)
      json += ",\"symbol\":\"" + JsonEscape(symbol) + "\"";

   double volume = HistoryDealGetDouble(ticket, DEAL_VOLUME);
   if(volume > 0)
      json += ",\"volume\":" + DoubleToString(volume, 3);

   long magic = HistoryDealGetInteger(ticket, DEAL_MAGIC);
   if(magic != 0)
      json += ",\"magic\":" + IntegerToString(magic);

   string comment = HistoryDealGetString(ticket, DEAL_COMMENT);
   if(StringLen(comment) > 0)
      json += ",\"comment\":\"" + JsonEscape(StringSubstr(comment, 0, 80)) + "\"";

   json += "}";
   return(json);
  }

//+------------------------------------------------------------------+
//| Build the JSON array of ALL relevant closed deals (full history,  |
//| capped to the most recent AVPUB_MAX_DEALS — full-snapshot         |
//| semantics: the server rebuilds everything from each push).        |
//+------------------------------------------------------------------+
string BuildDealsJson()
  {
   if(!HistorySelect(0, TimeCurrent() + 60))
     {
      Print("AlgoVerdict Publisher: HistorySelect failed — sending without deals.");
      return("[]");
     }
   int total = HistoryDealsTotal();

   // Pass 1: count qualifying deals (closing trades + balance ops).
   int qualifying = 0;
   for(int i = 0; i < total; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;
      if(IsClosingTradeDeal(ticket) || IsBalanceDeal(ticket))
         qualifying++;
     }
   int skip = (qualifying > AVPUB_MAX_DEALS) ? (qualifying - AVPUB_MAX_DEALS) : 0;
   if(skip > 0)
      Print("AlgoVerdict Publisher: history has ", IntegerToString(qualifying),
            " deals — sending the most recent ", IntegerToString(AVPUB_MAX_DEALS),
            " (growth curve starts at the oldest retained deal).");

   // Pass 2: emit oldest -> newest, skipping the oldest beyond the cap.
   string json = "[";
   int    seen = 0;
   int    written = 0;
   for(int i = 0; i < total; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;
      if(!IsClosingTradeDeal(ticket) && !IsBalanceDeal(ticket))
         continue;
      seen++;
      if(seen <= skip)
         continue;
      if(written > 0)
         json += ",";
      json += DealJson(ticket);
      written++;
     }
   json += "]";
   return(json);
  }

//+------------------------------------------------------------------+
//| Build the JSON array of currently open positions.                 |
//+------------------------------------------------------------------+
string BuildPositionsJson()
  {
   string json = "[";
   int written = 0;
   int total = PositionsTotal();
   for(int i = 0; i < total && written < AVPUB_MAX_POSITIONS; i++)
     {
      string symbol = PositionGetSymbol(i); // selects the position
      if(StringLen(symbol) == 0)
         continue;

      long ptype = PositionGetInteger(POSITION_TYPE);
      string dir = (ptype == POSITION_TYPE_BUY) ? "buy" : "sell";
      // Floating P&L incl. accrued swap.
      double profit = PositionGetDouble(POSITION_PROFIT)
                    + PositionGetDouble(POSITION_SWAP);

      if(written > 0)
         json += ",";
      json += "{";
      json += "\"id\":\""           + (string)PositionGetInteger(POSITION_TICKET) + "\",";
      json += "\"symbol\":\""       + JsonEscape(symbol) + "\",";
      json += "\"type\":\""         + dir + "\",";
      json += "\"volume\":"         + DoubleToString(PositionGetDouble(POSITION_VOLUME), 3) + ",";
      json += "\"openPrice\":"      + DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), 5) + ",";
      json += "\"currentPrice\":"   + DoubleToString(PositionGetDouble(POSITION_PRICE_CURRENT), 5) + ",";
      json += "\"profit\":"         + DoubleToString(profit, 2) + ",";
      json += "\"openTime\":\""     + IsoTime((datetime)PositionGetInteger(POSITION_TIME)) + "\"";
      long magic = PositionGetInteger(POSITION_MAGIC);
      if(magic != 0)
         json += ",\"magic\":" + IntegerToString(magic);
      json += "}";
      written++;
     }
   json += "]";
   return(json);
  }

//+------------------------------------------------------------------+
//| Collect everything and POST it to AlgoVerdict                     |
//+------------------------------------------------------------------+
void CollectAndPush()
  {
   // ---- account block -------------------------------------------------------
   string json = "{";
   json += "\"token\":\"" + JsonEscape(InpToken) + "\",";
   json += "\"account\":{";
   json += "\"broker\":\""   + JsonEscape(AccountInfoString(ACCOUNT_COMPANY)) + "\",";
   json += "\"currency\":\"" + JsonEscape(AccountInfoString(ACCOUNT_CURRENCY)) + "\",";
   json += "\"balance\":"    + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2) + ",";
   json += "\"equity\":"     + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2) + ",";
   json += "\"leverage\":"   + IntegerToString(AccountInfoInteger(ACCOUNT_LEVERAGE)) + ",";
   json += "\"login\":\""    + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + "\",";
   json += "\"server\":\""   + JsonEscape(AccountInfoString(ACCOUNT_SERVER)) + "\"";
   json += "},";

   // ---- open positions + full (capped) deal history -------------------------
   json += "\"openPositions\":" + BuildPositionsJson() + ",";
   json += "\"deals\":"         + BuildDealsJson();
   json += "}";

   // ---- POST -----------------------------------------------------------------
   char data[];
   char result[];
   string resultHeaders;
   int n = StringToCharArray(json, data, 0, WHOLE_ARRAY, CP_UTF8);
   if(n > 0)
      ArrayResize(data, n - 1); // drop the terminating '\0'

   ResetLastError();
   int status = WebRequest("POST", InpEndpoint,
                           "Content-Type: application/json\r\n",
                           15000, data, result, resultHeaders);

   if(status == -1)
     {
      int err = GetLastError();
      Print("AlgoVerdict Publisher: WebRequest failed (error ", IntegerToString(err), ").");
      Print(">>> Fix: MT5 -> Tools -> Options -> Expert Advisors -> ",
            "'Allow WebRequest for listed URL' -> add: https://algoverdict.com ",
            "— then remove and re-attach this EA.");
      return;
     }

   string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8);

   if(status == 200 && StringFind(response, "\"ok\":true") >= 0)
     {
      g_lastPush = TimeCurrent();
      string gv = AVPUB_GV_PREFIX + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN));
      GlobalVariableSet(gv, (double)(long)g_lastPush);
      Print("AlgoVerdict Publisher: push OK (", TimeToString(g_lastPush, TIME_DATE | TIME_SECONDS), ").");
      return;
     }

   if(status == 401)
     {
      Print("AlgoVerdict Publisher: REJECTED (401) — token unknown. ",
            "Re-copy the token from algoverdict.com -> Accounts -> EA setup ",
            "into the 'InpToken' input.");
      return;
     }
   if(status == 429)
     {
      // Server-side 60 s rate limit — back off quietly; the next timer tick retries.
      Print("AlgoVerdict Publisher: rate-limited (429) — retrying later.");
      return;
     }

   Print("AlgoVerdict Publisher: unexpected response (HTTP ", IntegerToString(status),
         "): ", StringSubstr(response, 0, 200));
  }
//+------------------------------------------------------------------+
