//+------------------------------------------------------------------+ //| MTP-AutoSync.mq5 | //| My Trader Pro — automatic trade journal sync | //| | //| Drop this on ANY chart in MetaTrader 5. Every position you close | //| is pushed straight into your My Trader Pro journal — broker or | //| prop firm, it does not care. | //| | //| SETUP (60 seconds) | //| 1. Portal → Auto-Sync → Generate key. Copy the key + URL. | //| 2. MT5 → Tools → Options → Expert Advisors → | //| tick "Allow WebRequest for listed URL" | //| add: https://mytraderpro.com | //| 3. Drag this EA onto any chart, paste your key, press OK. | //| 4. Done. Closed trades appear in your journal within a minute. | //+------------------------------------------------------------------+ #property copyright "My Trader Pro" #property link "https://mytraderpro.com" #property version "1.00" #property strict input string ApiKey = ""; // Your MTP API key input string Endpoint = "https://mytraderpro.com/wp-json/mtp/v1/ingest"; // Ingest URL input string AccountLabel = ""; // Account name in MTP (blank = auto) input int LookbackDays = 90; // Days of history to sync on first run input int CheckSeconds = 60; // How often to check for new closes datetime g_lastScan = 0; ulong g_sentIds[]; int g_sentCount = 0; /* Trades in the batch currently being sent. They are only promoted into g_sentIds once the server has actually accepted them — marking them earlier meant any failed send silently discarded those trades forever. */ ulong g_batchIds[]; int g_batchCount = 0; /* Real connection state, set only by an actual server response. The on-chart status must never claim "connected" on the strength of having found nothing to send — that is how a dead key looked healthy. */ bool g_connected = false; string g_status = "starting up"; datetime g_lastPing = 0; /* Guardrails the trader armed in the portal. The terminal shows them so the trader can see, on the chart, exactly what they committed to — the settings live server-side so they are the same on every device. */ bool g_armed = false; string g_guards = ""; // forward declaration — Sweep/OnTimer call Ping before its definition bool Ping(); //+------------------------------------------------------------------+ bool AlreadySent(ulong id) { for(int i = 0; i < g_sentCount; i++) if(g_sentIds[i] == id) return true; return false; } void MarkSent(ulong id) { if(g_sentCount >= ArraySize(g_sentIds)) ArrayResize(g_sentIds, g_sentCount + 256); g_sentIds[g_sentCount++] = id; } // Queue an id as part of the batch being built (NOT yet confirmed sent). void BatchAdd(ulong id) { if(g_batchCount >= ArraySize(g_batchIds)) ArrayResize(g_batchIds, g_batchCount + 256); g_batchIds[g_batchCount++] = id; } // The server accepted the batch — only now is it safe to stop resending. void BatchCommit() { for(int i = 0; i < g_batchCount; i++) MarkSent(g_batchIds[i]); g_batchCount = 0; } // Send failed. Forget the batch so every trade in it is retried next cycle. void BatchDiscard() { g_batchCount = 0; } string JsonEscape(string s) { string out = ""; for(int i = 0; i < StringLen(s); i++) { ushort c = StringGetCharacter(s, i); if(c == '"' || c == '\\') { out += "\\"; out += ShortToString(c); } else if(c == '\n' || c == '\r' || c == '\t') out += " "; else out += ShortToString(c); } return out; } string Num(double v, int digits) { return DoubleToString(v, digits); } //+------------------------------------------------------------------+ //| Build the JSON array of newly closed positions | //+------------------------------------------------------------------+ string CollectClosed(int &countOut) { countOut = 0; datetime from = (g_lastScan > 0) ? g_lastScan - 3600 : TimeCurrent() - (datetime)LookbackDays * 86400; if(!HistorySelect(from, TimeCurrent() + 3600)) return ""; string acct = (AccountLabel != "") ? AccountLabel : (AccountInfoString(ACCOUNT_COMPANY) + " " + (string)AccountInfoInteger(ACCOUNT_LOGIN)); /* Deal times are in BROKER server time, which is not UTC and not the trader's local time. Sending only a wall-clock string forced the portal to guess, which put trades in the wrong calendar day and left "daily P&L" empty while net P&L was right. Send the offset-corrected epoch too — that is unambiguous and the portal can bucket it in the trader's own day. */ long tzOff = (long)(TimeCurrent() - TimeGMT()); string body = ""; int deals = HistoryDealsTotal(); for(int i = 0; i < deals; i++) { ulong ticket = HistoryDealGetTicket(i); if(ticket == 0) continue; // only count deals that CLOSED a position — that's a finished trade if(HistoryDealGetInteger(ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; long dtype = HistoryDealGetInteger(ticket, DEAL_TYPE); if(dtype != DEAL_TYPE_BUY && dtype != DEAL_TYPE_SELL) continue; ulong posId = (ulong)HistoryDealGetInteger(ticket, DEAL_POSITION_ID); if(AlreadySent(posId)) continue; string sym = HistoryDealGetString(ticket, DEAL_SYMBOL); double vol = HistoryDealGetDouble(ticket, DEAL_VOLUME); double closeP = HistoryDealGetDouble(ticket, DEAL_PRICE); double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) + HistoryDealGetDouble(ticket, DEAL_SWAP) + HistoryDealGetDouble(ticket, DEAL_COMMISSION); datetime closeT = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME); // the closing deal is a SELL when the position was a BUY, so invert string dir = (dtype == DEAL_TYPE_SELL) ? "buy" : "sell"; // find the opening deal of the same position for entry price / time / SL / TP double openP = 0; datetime openT = 0; double sl = 0, tp = 0; for(int j = 0; j < deals; j++) { ulong t2 = HistoryDealGetTicket(j); if(t2 == 0) continue; if((ulong)HistoryDealGetInteger(t2, DEAL_POSITION_ID) != posId) continue; if(HistoryDealGetInteger(t2, DEAL_ENTRY) != DEAL_ENTRY_IN) continue; openP = HistoryDealGetDouble(t2, DEAL_PRICE); openT = (datetime)HistoryDealGetInteger(t2, DEAL_TIME); break; } if(openP == 0) continue; // SL/TP live on the order that opened the position if(HistoryOrderSelect(posId)) { sl = HistoryOrderGetDouble(posId, ORDER_SL); tp = HistoryOrderGetDouble(posId, ORDER_TP); } int dg = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); if(dg <= 0) dg = 5; if(body != "") body += ","; body += "{" + "\"ticket\":\"" + (string)posId + "\"," + "\"symbol\":\"" + JsonEscape(sym) + "\"," + "\"direction\":\"" + dir + "\"," + "\"lots\":" + Num(vol, 2) + "," + "\"entry\":" + Num(openP, dg) + "," + "\"exit\":" + Num(closeP, dg) + "," + "\"sl\":" + Num(sl, dg) + "," + "\"tp\":" + Num(tp, dg) + "," + "\"profit\":" + Num(profit, 2) + "," + "\"openTime\":\"" + TimeToString(openT, TIME_DATE|TIME_SECONDS) + "\"," + "\"closeTime\":\"" + TimeToString(closeT, TIME_DATE|TIME_SECONDS) + "\"," + "\"openUtc\":" + IntegerToString((long)openT - tzOff) + "," + "\"closeUtc\":" + IntegerToString((long)closeT - tzOff) + "," + "\"account\":\"" + JsonEscape(acct) + "\"," + "\"source\":\"MT5 Auto-Sync\"" + "}"; BatchAdd(posId); // confirmed only after the server accepts countOut++; if(countOut >= 100) break; // stay well inside the server's per-call cap } return body; } //+------------------------------------------------------------------+ bool Push(string tradesJson, int count) { string payload = "{\"key\":\"" + JsonEscape(ApiKey) + "\",\"trades\":[" + tradesJson + "]}"; char post[], result[]; string headers = "Content-Type: application/json\r\n"; string resultHeaders; // StringToCharArray with an explicit count copies NO terminating null, // so the old ArrayResize(-1) was chopping a real byte off the JSON and the // server received a malformed body. Copy WHOLE_ARRAY (which does include the // null) and drop exactly that one null. Byte-correct for UTF-8 too. ArrayResize(post, StringToCharArray(payload, post, 0, WHOLE_ARRAY, CP_UTF8) - 1); ResetLastError(); int code = WebRequest("POST", Endpoint, headers, 15000, post, result, resultHeaders); if(code == -1) { int err = GetLastError(); if(err == 4014) { g_status = "WebRequest blocked - add https://mytraderpro.com in Tools > Options > Expert Advisors"; Print("MTP Auto-Sync: WebRequest is not allowed. Tools → Options → Expert Advisors → ", "tick 'Allow WebRequest for listed URL' and add https://mytraderpro.com"); } else { g_status = StringFormat("cannot reach server (error %d)", err); Print("MTP Auto-Sync: connection failed, error ", err); } g_connected = false; return false; } string body = CharArrayToString(result, 0, ArraySize(result), CP_UTF8); if(code == 401) { g_status = "API KEY REJECTED - copy the CURRENT key from the portal"; g_connected = false; Print("MTP Auto-Sync: API key rejected. Generating a new key invalidates the old one — ", "re-copy the current key from Portal → Signals & Sync."); return false; } if(code < 200 || code >= 300) { g_status = StringFormat("server returned HTTP %d", code); g_connected = false; Print("MTP Auto-Sync: server returned HTTP ", code, " — ", body); return false; } g_connected = true; g_lastPing = TimeCurrent(); Print("MTP Auto-Sync: sent ", count, " closed trade(s) to My Trader Pro."); return true; } //+------------------------------------------------------------------+ // Announce this terminal to My Trader Pro. Sends no trades — it exists purely // so a correctly-installed EA is visibly distinguishable from one that never // reached the server, which is otherwise impossible to tell apart. bool Ping() { string acct = IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); if(AccountLabel != "") acct = AccountLabel; string payload = "{\"key\":\"" + JsonEscape(ApiKey) + "\",\"ping\":true," + "\"platform\":\"MT5\",\"account\":\"" + JsonEscape(acct) + "\"}"; char post[], result[]; string headers = "Content-Type: application/json\r\n"; string resultHeaders; // StringToCharArray with an explicit count copies NO terminating null, // so the old ArrayResize(-1) was chopping a real byte off the JSON and the // server received a malformed body. Copy WHOLE_ARRAY (which does include the // null) and drop exactly that one null. Byte-correct for UTF-8 too. ArrayResize(post, StringToCharArray(payload, post, 0, WHOLE_ARRAY, CP_UTF8) - 1); ResetLastError(); int code = WebRequest("POST", Endpoint, headers, 15000, post, result, resultHeaders); if(code == -1) { int err = GetLastError(); if(err == 4014) { g_status = "WebRequest blocked - add https://mytraderpro.com in Tools > Options > Expert Advisors"; Print("MTP Auto-Sync: WebRequest is BLOCKED. Tools → Options → Expert Advisors → ", "tick 'Allow WebRequest for listed URL' and add https://mytraderpro.com"); } else { g_status = StringFormat("cannot reach server (error %d)", err); Print("MTP Auto-Sync: could not reach the server, error ", err); } g_connected = false; return false; } if(code == 401) { g_status = "API KEY REJECTED - copy the CURRENT key from the portal"; g_connected = false; Print("MTP Auto-Sync: API KEY REJECTED. Generating a new key in the portal ", "invalidates the previous one — copy the current key from ", "Portal → Signals & Sync and paste it in again."); return false; } if(code < 200 || code >= 300) { g_status = StringFormat("server returned HTTP %d", code); g_connected = false; Print("MTP Auto-Sync: server returned HTTP ", code, " on connect."); return false; } g_connected = true; g_lastPing = TimeCurrent(); g_status = "handshake ok"; // read back which guardrails the trader has armed string resp = CharArrayToString(result, 0, ArraySize(result), CP_UTF8); g_armed = (StringFind(resp, "\"armed\":true") >= 0); g_guards = ""; string names[7] = {"autoSize","dailyLossLock","tiltLock","planRequired","newsStandDown","propFirmCap",""}; string labels[7] = {"auto-size","daily-loss lock","tilt lock","plan required","news stand-down","prop cap",""}; for(int i = 0; i < 6; i++) if(StringFind(resp, "\"" + names[i] + "\":true") >= 0) g_guards += (g_guards == "" ? "" : ", ") + labels[i]; /* The portal can ask for a full replay — used when trades were lost on the way into the journal. Forget everything we believe we have already sent and reset the history window, so the next sweep resends the whole lookback period from scratch. */ if(StringFind(resp, "\"resync\":true") >= 0) { g_sentCount = 0; ArrayResize(g_sentIds, 256); g_lastScan = 0; g_status = "full resync requested - resending history"; Print("MTP Auto-Sync: full resync requested by the portal. Resending the last ", LookbackDays, " days of closed trades."); } return true; } //+------------------------------------------------------------------+ int OnInit() { ArrayResize(g_sentIds, 256); if(StringLen(ApiKey) < 8) { Print("MTP Auto-Sync: paste your API key in the EA settings (Portal → Auto-Sync → Generate key)."); Comment("MTP Auto-Sync — NO API KEY SET"); return INIT_SUCCEEDED; } // Handshake first: proves the key and the WebRequest permission are good // BEFORE any trade has ever closed, so the portal can say "terminal // connected" instead of leaving the member staring at zeros. if(Ping()) { Comment("MTP Auto-Sync — connected. Closed trades sync automatically."); Print("MTP Auto-Sync: connected to My Trader Pro. Key accepted."); } else { Comment("MTP Auto-Sync — NOT CONNECTED. See the Experts tab for the reason."); } Print("MTP Auto-Sync: armed. Watching for closed positions every ", CheckSeconds, "s."); EventSetTimer(MathMax(15, CheckSeconds)); OnTimer(); // catch up on history immediately return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); Comment(""); } void ShowStatus() { if(g_connected) Comment("MTP Auto-Sync — connected · watching ", PositionsTotal(), " open position(s) · closed trades sync automatically · ", g_status, (g_armed ? "\nGUARDRAILS ARMED: " + (g_guards == "" ? "none selected" : g_guards) : ""), " · checked ", TimeToString(TimeCurrent(), TIME_MINUTES)); else Comment("MTP Auto-Sync — NOT CONNECTED · ", g_status, " · see the Experts tab · checked ", TimeToString(TimeCurrent(), TIME_MINUTES)); } void OnTimer() { if(StringLen(ApiKey) < 8) return; int n = 0; string trades = CollectClosed(n); if(n > 0 && trades != "") { if(Push(trades, n)) { BatchCommit(); // safe to stop resending these g_lastScan = TimeCurrent(); // only advance the window on success g_status = StringFormat("%d trade(s) sent", n); } else { /* Keep the window where it is and forget the batch, so these trades are retried on the next tick instead of being silently dropped. */ BatchDiscard(); } } else { /* Nothing to send. Re-handshake occasionally so the status reflects a connection that is actually alive right now, and so the portal's TERMINAL indicator stays current rather than going stale. */ if(TimeCurrent() - g_lastPing >= 600) Ping(); if(g_connected) g_status = "up to date"; } ShowStatus(); } // closing a position triggers an immediate push rather than waiting for the timer void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { if(trans.type == TRADE_TRANSACTION_DEAL_ADD) OnTimer(); } //+------------------------------------------------------------------+