//+------------------------------------------------------------------+ //| MTP-AutoSync.mq4 | //| My Trader Pro — broker Auto-Sync for MetaTrader 4 | //| | //| Pushes your CLOSED trades into your My Trader Pro journal. | //| It reads your own trade history. It never places, modifies | //| or closes an order, and no broker password is ever involved. | //| | //| SETUP (60 seconds): | //| 1. Portal → Signals & Sync → Generate my Auto-Sync key | //| 2. Drop this file in File → Open Data Folder → MQL4 → Experts | //| 3. Tools → Options → Expert Advisors → | //| tick "Allow WebRequest for listed URL" | //| and add https://mytraderpro.com | //| 4. Drag onto any chart, paste your key into ApiKey, press OK. | //+------------------------------------------------------------------+ #property copyright "My Trader Pro" #property link "https://mytraderpro.com/auto-sync-guide" #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; int g_sentIds[]; int g_sentCount = 0; /* Trades in the batch currently being sent. Promoted into g_sentIds only once the server actually accepts them — marking them earlier meant any failed send silently discarded those trades forever. */ int g_batchIds[]; int g_batchCount = 0; /* Real connection state, set only by an actual server response. The on-chart status must never claim "connected" just because there was nothing to send. */ bool g_connected = false; string g_status = "starting up"; datetime g_lastPing = 0; // forward declaration — Sweep/OnTimer call Ping before its definition bool Ping(); //+------------------------------------------------------------------+ bool AlreadySent(int id) { for(int i = 0; i < g_sentCount; i++) if(g_sentIds[i] == id) return true; return false; } void MarkSent(int 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(int 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; } //+------------------------------------------------------------------+ //| Build the JSON array of newly closed trades (MT4 order model) | //+------------------------------------------------------------------+ string CollectClosed(int &countOut) { countOut = 0; datetime from = (g_lastScan > 0) ? g_lastScan - 3600 : TimeCurrent() - LookbackDays * 86400; string acct = (AccountLabel != "") ? AccountLabel : (AccountCompany() + " " + IntegerToString(AccountNumber())); /* Order times are BROKER server time, not UTC and not the trader's local time. Send the offset-corrected epoch too so the portal can bucket each trade into the trader's own calendar day. */ long tzOff = (long)(TimeCurrent() - TimeGMT()); string body = ""; int total = OrdersHistoryTotal(); for(int i = 0; i < total; i++) { if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue; int otype = OrderType(); if(otype != OP_BUY && otype != OP_SELL) continue; // market trades only — no pendings, no balance ops if(OrderCloseTime() == 0) continue; // still open if(OrderCloseTime() < from) continue; // outside the window int ticket = OrderTicket(); if(AlreadySent(ticket)) continue; string sym = OrderSymbol(); int dg = (int)MarketInfo(sym, MODE_DIGITS); if(dg <= 0) dg = 5; double prof = OrderProfit() + OrderSwap() + OrderCommission(); if(body != "") body += ","; body += "{" + "\"ticket\":\"" + IntegerToString(ticket) + "\"," + "\"symbol\":\"" + JsonEscape(sym) + "\"," + "\"direction\":\"" + ((otype == OP_BUY) ? "buy" : "sell") + "\"," + "\"lots\":" + DoubleToString(OrderLots(), 2) + "," + "\"entry\":" + DoubleToString(OrderOpenPrice(), dg) + "," + "\"exit\":" + DoubleToString(OrderClosePrice(), dg) + "," + "\"sl\":" + DoubleToString(OrderStopLoss(), dg) + "," + "\"tp\":" + DoubleToString(OrderTakeProfit(), dg) + "," + "\"profit\":" + DoubleToString(prof, 2) + "," + "\"openTime\":\"" + TimeToString(OrderOpenTime(), TIME_DATE|TIME_SECONDS) + "\"," + "\"closeTime\":\"" + TimeToString(OrderCloseTime(), TIME_DATE|TIME_SECONDS) + "\"," + "\"openUtc\":" + IntegerToString((long)OrderOpenTime() - tzOff) + "," + "\"closeUtc\":" + IntegerToString((long)OrderCloseTime() - tzOff) + "," + "\"account\":\"" + JsonEscape(acct) + "\"," + "\"source\":\"MT4 Auto-Sync\"" + "}"; BatchAdd(ticket); // 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 == 4060) { 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 = "cannot reach server (error " + IntegerToString(err) + ")"; Print("MTP Auto-Sync: connection failed, error ", err, ". Will retry."); } g_connected = false; return false; } if(code != 200) { g_status = (code == 401) ? "API KEY REJECTED - copy the CURRENT key from the portal" : "server returned HTTP " + IntegerToString(code); g_connected = false; Print("MTP Auto-Sync: server answered HTTP ", code, (code == 401 ? " — API key rejected. Re-copy it from Portal → Signals & Sync." : ". Will retry.")); return false; } g_connected = true; g_lastPing = TimeCurrent(); Print("MTP Auto-Sync: sent ", count, " closed trade", (count > 1 ? "s" : ""), " to your journal."); return true; } //+------------------------------------------------------------------+ void ShowStatus() { if(g_connected) Comment("MTP Auto-Sync — connected · watching ", IntegerToString(OrdersTotal()), " open order(s) · closed trades sync automatically · ", g_status, " · checked ", TimeToString(TimeCurrent(), TIME_MINUTES)); else Comment("MTP Auto-Sync — NOT CONNECTED · ", g_status, " · see the Experts tab · checked ", TimeToString(TimeCurrent(), TIME_MINUTES)); } void Sweep() { if(ApiKey == "") return; int n = 0; string body = CollectClosed(n); if(n > 0 && body != "") { if(Push(body, n)) { BatchCommit(); // safe to stop resending these g_lastScan = TimeCurrent(); // only advance the window on success g_status = IntegerToString(n) + " trade(s) sent"; } else { /* Keep the window and forget the batch so these trades retry next tick rather than being silently dropped. */ BatchDiscard(); } } else { if(TimeCurrent() - g_lastPing >= 600) Ping(); if(g_connected) g_status = "up to date"; } ShowStatus(); } //+------------------------------------------------------------------+ // 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(AccountNumber()); if(AccountLabel != "") acct = AccountLabel; string payload = "{\"key\":\"" + JsonEscape(ApiKey) + "\",\"ping\":true," + "\"platform\":\"MT4\",\"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 == 4060 || 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 = "cannot reach server (error " + IntegerToString(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 = "server returned HTTP " + IntegerToString(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"; /* 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. */ string resp = CharArrayToString(result, 0, ArraySize(result), CP_UTF8); 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() { if(ApiKey == "") { Comment("MTP Auto-Sync — NO API KEY SET"); Print("MTP Auto-Sync: paste your key from Portal → Signals & Sync into the ApiKey input."); return(INIT_SUCCEEDED); } 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 trades every ", CheckSeconds, "s."); EventSetTimer(MathMax(15, CheckSeconds)); Sweep(); // first pass right away return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { EventKillTimer(); Comment(""); } void OnTimer() { Sweep(); } // MT4 has no OnTradeTransaction — OnTrade fires on account trade events, // so a close is picked up within a second instead of waiting for the timer. void OnTrade() { Sweep(); } //+------------------------------------------------------------------+