//+------------------------------------------------------------------+ //| ChartX_Bridge.mq5 | //| Copyright 2026, ChartX AI SaaS | //| https://chartx.com | //+------------------------------------------------------------------+ #property copyright "ChartX AI" #property link "https://chartx.com" #property version "1.10" #property strict #include CTrade trade; //--- Inputs input string InpChartX_URL = "https://api.chartxpro.com"; // ChartX API Server URL input string InpAPI_Key = "YOUR_CHARTX_API_KEY"; // Your ChartX API Key input int InpExecutionMode = 0; // Execution Mode: 0=Dynamic Offset (Recommended), 1=Raw Absolute input double InpMaxDriftPercent = 1.5; // Max Price Slippage/Drift % (0 = Disabled) input double InpRiskPercent = 1.0; // Risk Percentage per Trade (%) input double InpDefaultLot = 0.01; // Default Lot Size (if 0, auto-calc) input int InpPollSeconds = 2; // Signal Poll Interval (seconds) input bool InpAutoBreakEven = false; // Auto Move SL to Break-Even at 50% TP input double InpBETriggerRatio = 0.85; // Trigger Ratio (0.50 = 50% progress to TP) ulong g_TrackedTickets[]; //+------------------------------------------------------------------+ //| Get clean base URL (strips trailing slashes and /api paths) | //+------------------------------------------------------------------+ string GetBaseUrl() { string url = InpChartX_URL; // Remove trailing slashes while(StringLen(url) > 0 && StringSubstr(url, StringLen(url)-1, 1) == "/") { url = StringSubstr(url, 0, StringLen(url)-1); } int len = StringLen(url); if(len > 7 && StringSubstr(url, len - 7) == "/api/ea") { url = StringSubstr(url, 0, len - 7); } else if(len > 4 && StringSubstr(url, len - 4) == "/api") { url = StringSubstr(url, 0, len - 4); } return url; } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { Print("🚀 ChartX Universal EA Bridge v1.35 initialized for MT5"); Print("🔗 Connecting to ChartX Backend: ", GetBaseUrl()); Print("🛡️ Auto Break-Even Protection: ", (InpAutoBreakEven ? "ENABLED (at 50% TP)" : "DISABLED")); EventSetTimer(InpPollSeconds); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); Print("🛑 ChartX Universal EA Bridge stopped"); } //+------------------------------------------------------------------+ //| Timer event handler | //+------------------------------------------------------------------+ void OnTimer() { PollChartXSignals(); AuditOpenPositionsProtection(); CheckClosedPositions(); } //+------------------------------------------------------------------+ //| Continuous SL/TP audit & protection loop | //+------------------------------------------------------------------+ void AuditOpenPositionsProtection() { int totalPositions = PositionsTotal(); for(int p = 0; p < totalPositions; p++) { ulong pTicket = PositionGetTicket(p); if(pTicket > 0) { long magic = PositionGetInteger(POSITION_MAGIC); string comment = PositionGetString(POSITION_COMMENT); if(magic == 998877 || StringFind(comment, "ChartX") >= 0) { double posSL = PositionGetDouble(POSITION_SL); double posTP = PositionGetDouble(POSITION_TP); string posSymbol = PositionGetString(POSITION_SYMBOL); long posType = PositionGetInteger(POSITION_TYPE); double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); int digits = (int)SymbolInfoInteger(posSymbol, SYMBOL_DIGITS); double point = SymbolInfoDouble(posSymbol, SYMBOL_POINT); long stopLevel = SymbolInfoInteger(posSymbol, SYMBOL_TRADE_STOPS_LEVEL); double curBid = SymbolInfoDouble(posSymbol, SYMBOL_BID); double curAsk = SymbolInfoDouble(posSymbol, SYMBOL_ASK); if(curBid <= 0 || curAsk <= 0 || point <= 0) continue; double spread = curAsk - curBid; double minStopDist = MathMax((stopLevel > 0 ? stopLevel : 10), MathMax((long)(spread / point) + 5, 10)) * point; if(posSL == 0.0) // Missing Stop Loss! { // Smart volatility safety buffer: min 1.5% of current price or 150 points (never a 50-cent dummy buffer!) double safetyBuffer = MathMax(minStopDist * 2, MathMax(curBid * 0.015, 150 * point)); double emergencySL = 0.0; double emergencyTP = (posTP > 0) ? posTP : 0.0; if(posType == POSITION_TYPE_BUY) { emergencySL = NormalizeDouble(curBid - safetyBuffer, digits); if(emergencyTP > 0.0 && emergencyTP <= (curBid + minStopDist)) emergencyTP = NormalizeDouble(curBid + safetyBuffer * 2, digits); } else if(posType == POSITION_TYPE_SELL) { emergencySL = NormalizeDouble(curAsk + safetyBuffer, digits); if(emergencyTP > 0.0 && emergencyTP >= (curAsk - minStopDist)) emergencyTP = NormalizeDouble(curAsk - safetyBuffer * 2, digits); } if(emergencySL > 0) { if(trade.PositionModify(pTicket, emergencySL, emergencyTP)) { Print("🛡️ Auto-Protection: Attached guaranteed Stop Loss to Position #", pTicket, " on ", posSymbol, " -> SL: ", emergencySL, " | TP: ", emergencyTP); } } } // Auto Break-Even Protection: when trade hits 50% TP progress, move SL to open price + spread else if(InpAutoBreakEven && posSL > 0.0 && posTP > 0.0) { if(posType == POSITION_TYPE_BUY && posTP > openPrice) { double totalTarget = posTP - openPrice; double currentGain = curBid - openPrice; if(currentGain >= totalTarget * InpBETriggerRatio && posSL < openPrice) { double beSL = NormalizeDouble(openPrice + spread * 1.5, digits); if(beSL < (curBid - minStopDist)) { if(trade.PositionModify(pTicket, beSL, posTP)) { Print("🎯 Auto Break-Even: Position #", pTicket, " on ", posSymbol, " locked to Break-Even at ", beSL, " (Gain: +", DoubleToString(currentGain, digits), ")"); } } } } else if(posType == POSITION_TYPE_SELL && posTP < openPrice) { double totalTarget = openPrice - posTP; double currentGain = openPrice - curAsk; if(currentGain >= totalTarget * InpBETriggerRatio && posSL > openPrice) { double beSL = NormalizeDouble(openPrice - spread * 1.5, digits); if(beSL > (curAsk + minStopDist)) { if(trade.PositionModify(pTicket, beSL, posTP)) { Print("🎯 Auto Break-Even: Position #", pTicket, " on ", posSymbol, " locked to Break-Even at ", beSL, " (Gain: +", DoubleToString(currentGain, digits), ")"); } } } } } } } } } //+------------------------------------------------------------------+ //| Poll ChartX backend for trade signals | //+------------------------------------------------------------------+ void PollChartXSignals() { string url = GetBaseUrl() + "/api/ea/poll"; string headers = "Content-Type: application/json\r\n"; string payload = StringFormat( "{\"api_key\":\"%s\",\"broker_name\":\"%s\",\"account_number\":%I64d,\"terminal_type\":\"MT5\",\"balance\":%.2f,\"equity\":%.2f,\"leverage\":%d}", InpAPI_Key, AccountInfoString(ACCOUNT_COMPANY), AccountInfoInteger(ACCOUNT_LOGIN), AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), (int)AccountInfoInteger(ACCOUNT_LEVERAGE) ); char post[], result[]; string result_headers; StringToCharArray(payload, post, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(post, ArraySize(post) - 1); int res = WebRequest("POST", url, headers, 3000, post, result, result_headers); if(res == 200) { string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); // 1. Check for incoming signals if(StringFind(response, "\"has_signal\":true") >= 0) { Print("🎯 Received ChartX Signal: ", response); ExecuteSignal(response); } // 2. Auto-Reconcile open DB tickets with MT5 terminal int otStart = StringFind(response, "\"open_tickets\":["); if(otStart >= 0) { otStart += 16; int otEnd = StringFind(response, "]", otStart); if(otEnd > otStart) { string ticketListStr = StringSubstr(response, otStart, otEnd - otStart); string ticketTokens[]; int numTickets = StringSplit(ticketListStr, ',', ticketTokens); for(int k = 0; k < numTickets; k++) { string tStr = ticketTokens[k]; StringTrimLeft(tStr); StringTrimRight(tStr); ulong dbTicket = (ulong)StringToInteger(tStr); if(dbTicket > 0) { // Check if position is still open in MT5 bool isStillOpen = false; int totalPos = PositionsTotal(); for(int p = 0; p < totalPos; p++) { ulong pTicket = PositionGetTicket(p); if(pTicket > 0) { ulong pId = (ulong)PositionGetInteger(POSITION_IDENTIFIER); if(pTicket == dbTicket || pId == dbTicket) { isStillOpen = true; break; } } } if(!isStillOpen) { // Closed position! Query History Deals HistorySelect(TimeCurrent() - 86400 * 14, TimeCurrent() + 60); int totalDeals = HistoryDealsTotal(); double totalProfit = 0.0; double closePrice = 0.0; bool foundExitDeal = false; for(int d = totalDeals - 1; d >= 0; d--) { ulong dealTicket = HistoryDealGetTicket(d); if(dealTicket > 0) { ulong posId = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); if(posId == dbTicket) { long entryType = HistoryDealGetInteger(dealTicket, DEAL_ENTRY); if(entryType == DEAL_ENTRY_OUT || entryType == DEAL_ENTRY_OUT_BY || entryType == DEAL_ENTRY_INOUT) { totalProfit += HistoryDealGetDouble(dealTicket, DEAL_PROFIT); totalProfit += HistoryDealGetDouble(dealTicket, DEAL_SWAP); totalProfit += HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); closePrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE); foundExitDeal = true; break; } } } } if(foundExitDeal) { string outcome = (totalProfit >= 0.0) ? "win" : "loss"; Print("🔄 Auto-Reconciled Closed Position #", dbTicket, " -> Close: ", closePrice, " | PnL: ", totalProfit, " (", outcome, ")"); SendCloseCallback(dbTicket, closePrice, totalProfit, outcome); } } } } } } } } bool MatchKeywordInList(string target, string csv) { string items[]; int count = StringSplit(csv, ',', items); for(int i = 0; i < count; i++) { string item = items[i]; StringTrimLeft(item); StringTrimRight(item); if(item != "" && StringFind(target, item) >= 0) return true; } return false; } //+------------------------------------------------------------------+ //| Universal Intelligent Symbol Resolver | //| Automatically matches broker instruments across all naming styles| //+------------------------------------------------------------------+ string ResolveSymbol(string sym) { if(sym == "" || sym == "UNKNOWN") return ""; string clean = sym; StringToUpper(clean); StringReplace(clean, "/", ""); StringReplace(clean, "-", ""); StringReplace(clean, "_", ""); StringReplace(clean, " ", ""); // 1. Direct match test if(SymbolSelect(clean, true)) return clean; // 2. Comprehensive Suffixes (!, .m, .pro, m, _i, .a, .c, .s, .raw, .ecn, .std, .stp, .cash, etc.) string suffixes[] = {"!", ".m", "m", ".pro", "_i", ".a", ".c", ".s", ".raw", ".ecn", "pro", "_raw", "i", ".std", ".stp", ".cash", "#", "+"}; for(int i = 0; i < ArraySize(suffixes); i++) { string testSym = clean + suffixes[i]; if(SymbolSelect(testSym, true)) return testSym; } // 3. Determine semantic keyword group for fuzzy broker catalog scan string targetKeywords = ""; if(MatchKeywordInList(clean, "NAS100,US100,USTEC,USTECH,NASDAQ,NDX,TECH100,NQ")) targetKeywords = "NAS100,USTECH,USTEC,US100,NASDAQ,NDX,TECH100,NQ100,TECH"; else if(MatchKeywordInList(clean, "US30,DJI,DOW,WALLSTREET,WS30,DJ30,USA30,YM")) targetKeywords = "US30,DJI,DOW,WALLSTREET,WS30,DJ30,USA30,WALLST"; else if(MatchKeywordInList(clean, "SPX500,US500,SP500,SPX,USA500,ES")) targetKeywords = "US500,SPX500,SP500,SPX,USA500"; else if(MatchKeywordInList(clean, "GER40,DE40,DAX40,GER30,DE30,DAX")) targetKeywords = "GER40,DE40,DAX40,GER30,DE30,DAX"; else if(MatchKeywordInList(clean, "UK100,FTSE100,FTSE")) targetKeywords = "UK100,FTSE100,FTSE"; else if(MatchKeywordInList(clean, "JP225,JPN225,NIKKEI,N225")) targetKeywords = "JP225,JPN225,NIKKEI,N225"; else if(MatchKeywordInList(clean, "XAUUSD,GOLD")) targetKeywords = "XAUUSD,GOLD"; else if(MatchKeywordInList(clean, "XAGUSD,SILVER")) targetKeywords = "XAGUSD,SILVER"; else if(MatchKeywordInList(clean, "USOIL,WTI,CRUDE,CL,BRENT,UKOIL,XTIUSD,XBRUSD")) targetKeywords = "USOIL,WTI,CRUDE,BRENT,UKOIL,XTIUSD,XBRUSD,CL"; else { // Crypto bases string cryptoBases[] = {"BTC", "ETH", "SOL", "DOGE", "XRP", "BNB", "ADA", "AVAX", "DOT", "LINK", "MATIC", "LTC", "SHIB", "NEAR", "TRX", "BCH"}; for(int c = 0; c < ArraySize(cryptoBases); c++) { if(StringFind(clean, cryptoBases[c]) >= 0) { targetKeywords = cryptoBases[c] + "USD," + cryptoBases[c] + "USDT," + cryptoBases[c]; break; } } } // 4. Exhaustive Broker Symbol Catalog Search (scans all broker instruments) int totalBrokerSymbols = SymbolsTotal(false); for(int i = 0; i < totalBrokerSymbols; i++) { string bSym = SymbolName(i, false); string bClean = bSym; StringToUpper(bClean); StringReplace(bClean, "/", ""); StringReplace(bClean, "-", ""); StringReplace(bClean, "_", ""); StringReplace(bClean, ".", ""); // Exact clean match or direct inclusion if(bClean == clean || StringFind(bClean, clean) >= 0 || StringFind(clean, bClean) >= 0) { if(SymbolSelect(bSym, true)) return bSym; } // Semantic keyword group match if(targetKeywords != "" && MatchKeywordInList(bClean, targetKeywords)) { if(SymbolSelect(bSym, true)) return bSym; } } return ""; } //+------------------------------------------------------------------+ //| Execute received signal | //+------------------------------------------------------------------+ void ExecuteSignal(string json) { string rawSymbol = ExtractJsonString(json, "symbol"); string action = ExtractJsonString(json, "action"); string signalId = ExtractJsonString(json, "signal_id"); double entryPrice = ExtractJsonDouble(json, "entry_price"); double sl = ExtractJsonDouble(json, "sl"); double tp = ExtractJsonDouble(json, "tp"); double slDist = ExtractJsonDouble(json, "sl_distance"); double tpDist = ExtractJsonDouble(json, "tp_distance"); double slPct = ExtractJsonDouble(json, "sl_pct"); double tpPct = ExtractJsonDouble(json, "tp_pct"); double lot = InpDefaultLot; double customVol = ExtractJsonDouble(json, "volume"); if(customVol > 0.0) lot = customVol; if(action == "") return; string symbol = ResolveSymbol(rawSymbol); if(symbol == "") { Print("❌ Symbol '", rawSymbol, "' is NOT offered by your broker (", AccountInfoString(ACCOUNT_COMPANY), "). Trade skipped to prevent wrong-asset execution."); return; } SymbolSelect(symbol, true); // Configure broker filling mode uint filling = (uint)SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE); if((filling & SYMBOL_FILLING_FOK) != 0) trade.SetTypeFilling(ORDER_FILLING_FOK); else if((filling & SYMBOL_FILLING_IOC) != 0) trade.SetTypeFilling(ORDER_FILLING_IOC); else trade.SetTypeFilling(ORDER_FILLING_RETURN); trade.SetExpertMagicNumber(998877); trade.SetDeviationInPoints(50); int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); if(digits <= 0) digits = 2; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); if(point <= 0.0) point = MathPow(10, -digits); // Fetch live quotes for any broker symbol via SymbolInfoTick with synchronized retry loop MqlTick last_tick; double ask = 0.0; double bid = 0.0; for(int attempt = 0; attempt < 25; attempt++) { if(SymbolInfoTick(symbol, last_tick)) { ask = last_tick.ask; bid = last_tick.bid; if(ask > 0.0 && bid > 0.0) break; if(ask <= 0.0 && last_tick.last > 0.0) { ask = last_tick.last; bid = last_tick.last; break; } } if(ask <= 0.0) ask = SymbolInfoDouble(symbol, SYMBOL_ASK); if(bid <= 0.0) bid = SymbolInfoDouble(symbol, SYMBOL_BID); if(ask > 0.0 && bid > 0.0) break; Sleep(100); } if(ask <= 0.0 || bid <= 0.0) { Print("❌ Market prices unavailable for ", symbol, ". Skipping execution."); SendExecutionFailure(signalId, symbol, action, 10018, "Market prices unavailable for " + symbol); return; } double spread = ask - bid; double currentPrice = (action == "BUY") ? ask : bid; // 1. SETUP INVALIDATION CHECK (Protect capital against already-expired/broken setups) if(action == "BUY") { if(sl > 0.0 && bid <= sl) { Print("🛑 Setup Invalidated: Market Bid (", bid, ") has already broken below Stop Loss (", sl, "). Trade cancelled to prevent immediate loss."); return; } if(tp > 0.0 && ask >= tp) { Print("🎯 Setup Finished: Market Ask (", ask, ") has already reached Target TP (", tp, "). Trade cancelled to prevent buying the top."); return; } } else if(action == "SELL") { if(sl > 0.0 && ask >= sl) { Print("🛑 Setup Invalidated: Market Ask (", ask, ") has already exceeded Stop Loss (", sl, "). Trade cancelled to prevent immediate loss."); return; } if(tp > 0.0 && bid <= tp) { Print("🎯 Setup Finished: Market Bid (", bid, ") has already reached Target TP (", tp, "). Trade cancelled to prevent selling the bottom."); return; } } // 2. SLIPPAGE DRIFT CHECK if(InpMaxDriftPercent > 0.0 && entryPrice > 0.0) { double driftPct = (MathAbs(currentPrice - entryPrice) / entryPrice) * 100.0; if(driftPct > InpMaxDriftPercent) { Print("⚠️ Price Drift Exceeded: Current broker price (", currentPrice, ") drifted ", DoubleToString(driftPct, 2), "% from setup entry (", entryPrice, "). Exceeds max allowed drift (", InpMaxDriftPercent, "%). Trade cancelled."); return; } } // 3. BROKER MIN STOP DISTANCE (prevent 10016 invalid stops) long stopLevel = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL); double minStopDist = MathMax((stopLevel > 0 ? stopLevel : 10), MathMax((long)(spread / point) + 5, 10)) * point; // 4. INTELLIGENT STOP LOSS & TAKE PROFIT CALCULATION double targetSL = 0.0; double targetTP = 0.0; if(InpExecutionMode == 0) // DYNAMIC RELATIVE OFFSET (Anchored to live fill price) { bool similarScale = (entryPrice > 0.0 && (currentPrice / entryPrice >= 0.5 && currentPrice / entryPrice <= 2.0)); if(action == "BUY") { if(slDist > 0.0 && similarScale) targetSL = currentPrice - MathMax(slDist, minStopDist); else if(slPct > 0.0) targetSL = currentPrice * (1.0 - slPct); else if(sl > 0.0) targetSL = sl; if(tpDist > 0.0 && similarScale) targetTP = currentPrice + MathMax(tpDist, minStopDist); else if(tpPct > 0.0) targetTP = currentPrice * (1.0 + tpPct); else if(tp > 0.0) targetTP = tp; // Clamp to ensure valid stop levels if(targetSL > 0.0 && targetSL >= (bid - minStopDist)) targetSL = bid - minStopDist; if(targetTP > 0.0 && targetTP <= (bid + minStopDist)) targetTP = bid + minStopDist * 2; } else if(action == "SELL") { if(slDist > 0.0 && similarScale) targetSL = currentPrice + MathMax(slDist, minStopDist); else if(slPct > 0.0) targetSL = currentPrice * (1.0 + slPct); else if(sl > 0.0) targetSL = sl; if(tpDist > 0.0 && similarScale) targetTP = currentPrice - MathMax(tpDist, minStopDist); else if(tpPct > 0.0) targetTP = currentPrice * (1.0 - tpPct); else if(tp > 0.0) targetTP = tp; // Clamp to ensure valid stop levels if(targetSL > 0.0 && targetSL <= (ask + minStopDist)) targetSL = ask + minStopDist; if(targetTP > 0.0 && targetTP >= (ask - minStopDist)) targetTP = ask - minStopDist * 2; } } else // RAW ABSOLUTE PRICES { targetSL = (sl > 0.0) ? sl : 0.0; targetTP = (tp > 0.0) ? tp : 0.0; } if(targetSL > 0.0) targetSL = NormalizeDouble(targetSL, digits); if(targetTP > 0.0) targetTP = NormalizeDouble(targetTP, digits); // Ensure valid lot size double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); double stepLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); if(minLot > 0 && lot < minLot) lot = minLot; if(maxLot > 0 && lot > maxLot) lot = maxLot; if(stepLot > 0) lot = MathFloor(lot / stepLot) * stepLot; // 5. EXECUTE ORDER bool success = false; if(action == "BUY") { double price = (ask > 0) ? NormalizeDouble(ask, digits) : 0; success = trade.PositionOpen(symbol, ORDER_TYPE_BUY, lot, price, targetSL, targetTP, "ChartX AI Long"); if(!success) success = trade.Buy(lot, symbol, 0, 0, 0, "ChartX AI Long"); } else if(action == "SELL") { double price = (bid > 0) ? NormalizeDouble(bid, digits) : 0; success = trade.PositionOpen(symbol, ORDER_TYPE_SELL, lot, price, targetSL, targetTP, "ChartX AI Short"); if(!success) success = trade.Sell(lot, symbol, 0, 0, 0, "ChartX AI Short"); } if(success) { // 6. RELIABLE POSITION IDENTIFICATION ulong ticket = 0; double openPrice = 0.0; int totalPositions = PositionsTotal(); for(int p = totalPositions - 1; p >= 0; p--) { ulong pTicket = PositionGetTicket(p); if(pTicket > 0 && PositionGetString(POSITION_SYMBOL) == symbol) { long magic = PositionGetInteger(POSITION_MAGIC); if(magic == 998877) { ticket = pTicket; openPrice = PositionGetDouble(POSITION_PRICE_OPEN); break; } } } if(ticket == 0) { ulong deal = trade.ResultDeal(); if(deal > 0 && HistoryDealSelect(deal)) ticket = (ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID); if(ticket == 0) ticket = trade.ResultOrder(); } if(openPrice <= 0.0) openPrice = trade.ResultPrice(); if(openPrice <= 0.0) openPrice = currentPrice; Print("✅ ChartX Trade Executed! Position Ticket #", ticket, " on ", symbol, " at ", openPrice); // 7. GUARANTEE STOP LOSS & TAKE PROFIT ON THE OPEN POSITION if(ticket > 0 && PositionSelectByTicket(ticket)) { double currentSL = PositionGetDouble(POSITION_SL); double currentTP = PositionGetDouble(POSITION_TP); if(currentSL <= 0.0 && targetSL > 0.0) { if(trade.PositionModify(ticket, targetSL, targetTP)) { Print("🛡️ SL/TP Attached to Position #", ticket, " -> SL: ", targetSL, " | TP: ", targetTP); } else { Print("⚠️ PositionModify retrying SL-only on #", ticket, ": ", trade.ResultRetcodeDescription()); trade.PositionModify(ticket, targetSL, 0); } } } // Add to active tracked tickets for close reporting bool alreadyTracked = false; int size = ArraySize(g_TrackedTickets); for(int k = 0; k < size; k++) { if(g_TrackedTickets[k] == ticket) { alreadyTracked = true; break; } } if(!alreadyTracked && ticket > 0) { ArrayResize(g_TrackedTickets, size + 1); g_TrackedTickets[size] = ticket; } SendExecutionCallback(signalId, (int)ticket, symbol, action, lot, openPrice, targetSL, targetTP); } else { uint errCode = trade.ResultRetcode(); string errDesc = trade.ResultRetcodeDescription(); if(errCode == 0) { errCode = GetLastError(); errDesc = "Order rejected by broker"; } Print("❌ OrderSend failed on ", symbol, ": ", errDesc, " (Code: ", errCode, ")"); SendExecutionFailure(signalId, symbol, action, (int)errCode, errDesc); } } //+------------------------------------------------------------------+ //| Check and report closed positions to ChartX | //+------------------------------------------------------------------+ void CheckClosedPositions() { int size = ArraySize(g_TrackedTickets); if(size == 0) return; for(int i = size - 1; i >= 0; i--) { ulong ticket = g_TrackedTickets[i]; if(ticket == 0) continue; // 1. Check if the position is STILL actively open in MT5 bool isPositionOpen = false; int totalPositions = PositionsTotal(); for(int p = 0; p < totalPositions; p++) { ulong pTicket = PositionGetTicket(p); if(pTicket > 0) { ulong pId = (ulong)PositionGetInteger(POSITION_IDENTIFIER); if(pTicket == ticket || pId == ticket) { isPositionOpen = true; break; } } } // If position is still open in terminal, keep tracking! if(isPositionOpen) continue; // 2. Position has closed - fetch outcome and PnL from History HistorySelect(TimeCurrent() - 86400 * 7, TimeCurrent() + 60); int totalDeals = HistoryDealsTotal(); double totalProfit = 0.0; double closePrice = 0.0; bool foundExitDeal = false; for(int d = totalDeals - 1; d >= 0; d--) { ulong dealTicket = HistoryDealGetTicket(d); if(dealTicket > 0) { ulong posId = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); if(posId == ticket) { long entryType = HistoryDealGetInteger(dealTicket, DEAL_ENTRY); if(entryType == DEAL_ENTRY_OUT || entryType == DEAL_ENTRY_OUT_BY || entryType == DEAL_ENTRY_INOUT) { totalProfit += HistoryDealGetDouble(dealTicket, DEAL_PROFIT); totalProfit += HistoryDealGetDouble(dealTicket, DEAL_SWAP); totalProfit += HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); closePrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE); foundExitDeal = true; break; } } } } // Only report close if exit deal is confirmed in history if(foundExitDeal) { string outcome = (totalProfit >= 0.0) ? "win" : "loss"; SendCloseCallback(ticket, closePrice, totalProfit, outcome); // Remove ticket from tracking array for(int j = i; j < size - 1; j++) { g_TrackedTickets[j] = g_TrackedTickets[j + 1]; } ArrayResize(g_TrackedTickets, size - 1); size--; } } } //+------------------------------------------------------------------+ //| Send execution failure back to ChartX | //+------------------------------------------------------------------+ void SendExecutionFailure(string sigId, string symbol, string action, int code, string desc) { string url = GetBaseUrl() + "/api/ea/execution-failed"; string headers = "Content-Type: application/json\r\n"; string payload = StringFormat( "{\"api_key\":\"%s\",\"signal_id\":\"%s\",\"symbol\":\"%s\",\"action\":\"%s\",\"error_code\":%d,\"error_message\":\"%s\"}", InpAPI_Key, sigId, symbol, action, code, desc ); char post[], result[]; string result_headers; StringToCharArray(payload, post, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(post, ArraySize(post)-1); WebRequest("POST", url, headers, 3000, post, result, result_headers); } //+------------------------------------------------------------------+ //| Send execution confirmation back to ChartX | //+------------------------------------------------------------------+ void SendExecutionCallback(string sigId, int ticket, string symbol, string action, double volume, double price, double sl, double tp) { string url = GetBaseUrl() + "/api/ea/execute-callback"; string headers = "Content-Type: application/json\r\n"; string payload = StringFormat( "{\"api_key\":\"%s\",\"signal_id\":\"%s\",\"ticket\":%d,\"symbol\":\"%s\",\"action\":\"%s\",\"volume\":%.2f,\"open_price\":%.5f,\"sl\":%.5f,\"tp\":%.5f,\"status\":\"OPEN\"}", InpAPI_Key, sigId, ticket, symbol, action, volume, price, sl, tp ); char post[], result[]; string result_headers; StringToCharArray(payload, post, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(post, ArraySize(post)-1); int res = WebRequest("POST", url, headers, 3000, post, result, result_headers); if(res == 200) { Print("📡 ChartX Execution Logged Successfully in SaaS DB! Ticket #", ticket); } else { Print("⚠️ WebRequest to execute-callback returned code: ", res); } } //+------------------------------------------------------------------+ //| Send trade close callback back to ChartX | //+------------------------------------------------------------------+ void SendCloseCallback(ulong ticket, double closePrice, double pnl, string outcome) { string url = GetBaseUrl() + "/api/ea/close-callback"; string headers = "Content-Type: application/json\r\n"; string payload = StringFormat( "{\"api_key\":\"%s\",\"ticket\":%I64d,\"close_price\":%.5f,\"pnl\":%.2f,\"outcome\":\"%s\",\"close_reason\":\"Closed in MT5\"}", InpAPI_Key, ticket, closePrice, pnl, outcome ); char post[], result[]; string result_headers; StringToCharArray(payload, post, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(post, ArraySize(post)-1); int res = WebRequest("POST", url, headers, 3000, post, result, result_headers); if(res == 200) { Print("📊 ChartX Close Callback Recorded in SaaS DB! Ticket #", ticket, " PnL: $", DoubleToString(pnl, 2), " (", outcome, ")"); } else { Print("⚠️ WebRequest to close-callback returned code: ", res); } } //--- Helper String Parsers string ExtractJsonString(string json, string key) { string pattern = "\"" + key + "\":\""; int pos = StringFind(json, pattern); if(pos < 0) return ""; pos += StringLen(pattern); int end = StringFind(json, "\"", pos); if(end < 0) return ""; return StringSubstr(json, pos, end - pos); } double ExtractJsonDouble(string json, string key) { string pattern = "\"" + key + "\":"; int pos = StringFind(json, pattern); if(pos < 0) return 0.0; pos += StringLen(pattern); int end = StringFind(json, ",", pos); if(end < 0) end = StringFind(json, "}", pos); if(end < 0) return 0.0; return StringToDouble(StringSubstr(json, pos, end - pos)); }