//+------------------------------------------------------------------+ //| AITelegramSignalCopier.mq5 | //| AI-powered Telegram Signal Copier for MT5 | //+------------------------------------------------------------------+ #property copyright "Copyright 2026" #property version "1.00" #property strict #include //--- external inputs input string InpApiBaseUrl = "http://127.0.0.1:5000"; // Flask API base URL input double InpLotSize = 0.01; // Fixed lot size input int InpTimerSec = 5; // Polling interval in seconds input bool InpVerboseLog = true; // Enable detailed log messages //--- trading object CTrade trade; //--- structure used to store one signal received from the Flask API struct SSignal { long id; // database signal ID string symbol; // canonical symbol returned by the backend string direction; // BUY or SELL string intent; // AI-parsed trading intent string orderType; // executable order type, if provided by backend double entryPrice; // requested entry price for pending orders double stopLoss; // stop-loss price double takeProfit; // take-profit price double confidence; // AI confidence score }; //--- stores the last processed signal ID to prevent duplicate execution long g_lastSignalId = 0; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- validate timer input before starting the EA if(InpTimerSec <= 0) { Print("[INIT] Invalid timer interval. InpTimerSec must be greater than zero."); return(INIT_PARAMETERS_INCORRECT); } //--- start periodic polling of the Flask backend EventSetTimer(InpTimerSec); //--- provide immediate visual feedback in the Experts tab Print("[INIT] AI Telegram Signal Copier initialized."); Print("[INIT] API Base URL: ", InpApiBaseUrl); Print("[INIT] Polling interval: ", InpTimerSec, " seconds."); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- stop timer events when the EA is removed or the terminal closes EventKillTimer(); Print("[INIT] AI Telegram Signal Copier stopped."); } //+------------------------------------------------------------------+ //| Timer event | //+------------------------------------------------------------------+ void OnTimer() { //--- keep the event handler short and delegate the workflow ProcessPendingSignal(); } //+------------------------------------------------------------------+ //| Process one pending signal | //+------------------------------------------------------------------+ void ProcessPendingSignal() { string response = ""; //--- request the next pending signal from the Flask backend if(!HttpGet(BuildUrl("/api/v1/signals/pending"), response)) return; //--- the backend explicitly returns null when no signal is pending if(StringFind(response, "\"signal\": null") >= 0) { if(InpVerboseLog) Print("[API] No pending signals available."); return; } SSignal signal; //--- convert the JSON response into the local SSignal structure if(!ParsePendingSignal(response, signal)) { Print("[SIGNAL] Failed to parse pending signal response."); Print(response); return; } //--- prevent repeated execution of the same signal during timer cycles if(signal.id == g_lastSignalId) return; g_lastSignalId = signal.id; //--- show the parsed signal before attempting execution PrintSignal(signal); ulong ticket = 0; string message = ""; //--- execute the signal and report the outcome back to the backend bool executed = ExecuteSignal(signal, ticket, message); if(executed) SendExecutionStatus(signal.id, "EXECUTED", ticket, message); else SendExecutionStatus(signal.id, "FAILED", 0, message); } //+------------------------------------------------------------------+ //| Execute signal | //+------------------------------------------------------------------+ bool ExecuteSignal(const SSignal &signal, ulong &ticket, string &message) { //--- initialize output ticket before execution starts ticket = 0; //--- a signal cannot be executed without a symbol if(signal.symbol == "") { message = "Signal symbol is missing."; Print(message); return(false); } //--- map the canonical AI/backend symbol to the broker-specific symbol string brokerSymbol = ResolveBrokerSymbol(signal.symbol, message); if(brokerSymbol == "") { Print(message); return(false); } Print("[SYMBOL] Canonical symbol: ", signal.symbol); Print("[SYMBOL] Resolved broker symbol: ", brokerSymbol); ENUM_ORDER_TYPE orderType; //--- convert AI intent into an executable MetaTrader 5 order type if(!ResolveOrderType(signal, brokerSymbol, orderType, message)) { Print(message); return(false); } Print("[TRADE] Resolved order type: ", OrderTypeToString(orderType)); //--- normalize prices according to the broker symbol digits double entry = NormalizeSignalPrice(brokerSymbol, signal.entryPrice); double sl = NormalizeSignalPrice(brokerSymbol, signal.stopLoss); double tp = NormalizeSignalPrice(brokerSymbol, signal.takeProfit); bool result = false; //--- execute the appropriate market or pending order if(orderType == ORDER_TYPE_BUY) result = trade.Buy(InpLotSize, brokerSymbol, 0.0, sl, tp, "AI Telegram Signal"); else if(orderType == ORDER_TYPE_SELL) result = trade.Sell(InpLotSize, brokerSymbol, 0.0, sl, tp, "AI Telegram Signal"); else if(orderType == ORDER_TYPE_BUY_LIMIT) result = trade.BuyLimit(InpLotSize, entry, brokerSymbol, sl, tp, ORDER_TIME_GTC, 0, "AI Telegram Signal"); else if(orderType == ORDER_TYPE_SELL_LIMIT) result = trade.SellLimit(InpLotSize, entry, brokerSymbol, sl, tp, ORDER_TIME_GTC, 0, "AI Telegram Signal"); else if(orderType == ORDER_TYPE_BUY_STOP) result = trade.BuyStop(InpLotSize, entry, brokerSymbol, sl, tp, ORDER_TIME_GTC, 0, "AI Telegram Signal"); else if(orderType == ORDER_TYPE_SELL_STOP) result = trade.SellStop(InpLotSize, entry, brokerSymbol, sl, tp, ORDER_TIME_GTC, 0, "AI Telegram Signal"); else { message = "Unsupported resolved order type."; Print(message); return(false); } //--- handle broker-side rejection and preserve the retcode message if(!result) { message = "Trade failed. Retcode: " + IntegerToString((int)trade.ResultRetcode()) + ". " + trade.ResultRetcodeDescription(); Print(message); return(false); } //--- store broker order ticket and notify the user ticket = trade.ResultOrder(); message = "Trade opened successfully."; Print("[TRADE] ", message); Print("[TRADE] Broker ticket/order: ", ticket); NotifyExecutionSuccess(signal, brokerSymbol, orderType, ticket); return(true); } //+------------------------------------------------------------------+ //| Resolve AI intent into executable MT5 order type | //+------------------------------------------------------------------+ bool ResolveOrderType(const SSignal &signal, const string brokerSymbol, ENUM_ORDER_TYPE &orderType, string &message) { //--- read current prices for market and pending order decisions double ask = SymbolInfoDouble(brokerSymbol, SYMBOL_ASK); double bid = SymbolInfoDouble(brokerSymbol, SYMBOL_BID); if(ask <= 0.0 || bid <= 0.0) { message = "Failed to read market prices for symbol: " + brokerSymbol; return(false); } Print("[TRADE] Current Ask: ", DoubleToString(ask, (int)SymbolInfoInteger(brokerSymbol, SYMBOL_DIGITS))); Print("[TRADE] Current Bid: ", DoubleToString(bid, (int)SymbolInfoInteger(brokerSymbol, SYMBOL_DIGITS))); //--- direct market intents do not require an entry price if(signal.intent == "BUY_MARKET") { orderType = ORDER_TYPE_BUY; return(true); } if(signal.intent == "SELL_MARKET") { orderType = ORDER_TYPE_SELL; return(true); } //--- pending order intents require a valid entry price if(signal.entryPrice <= 0.0) { message = "Entry price is required for pending orders."; return(false); } //--- buy-at-price becomes Buy Limit below Ask or Buy Stop above Ask if(signal.intent == "BUY_AT_PRICE") { orderType = (signal.entryPrice < ask) ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP; return(true); } //--- sell-at-price becomes Sell Limit above Bid or Sell Stop below Bid if(signal.intent == "SELL_AT_PRICE") { orderType = (signal.entryPrice > bid) ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP; return(true); } //--- breakout-style buy above current price if(signal.intent == "BUY_ABOVE") { orderType = ORDER_TYPE_BUY_STOP; return(true); } //--- breakout-style sell below current price if(signal.intent == "SELL_BELOW") { orderType = ORDER_TYPE_SELL_STOP; return(true); } message = "Unsupported signal intent: " + signal.intent; return(false); } //+------------------------------------------------------------------+ //| Resolve canonical symbol to broker symbol | //+------------------------------------------------------------------+ string ResolveBrokerSymbol(const string canonicalSymbol, string &message) { //--- reject empty canonical symbols before searching the terminal if(canonicalSymbol == "") { message = "Canonical symbol is empty."; return(""); } //--- first try the canonical symbol exactly as received if(SymbolSelect(canonicalSymbol, true)) return(canonicalSymbol); //--- if the EA is attached to a matching chart symbol, prefer it if(StringFind(_Symbol, canonicalSymbol) == 0) { if(SymbolSelect(_Symbol, true)) return(_Symbol); } //--- search visible Market Watch symbols first int marketWatchTotal = SymbolsTotal(true); for(int i = 0; i < marketWatchTotal; i++) { string symbolName = SymbolName(i, true); if(StringFind(symbolName, canonicalSymbol) == 0) { if(SymbolSelect(symbolName, true)) return(symbolName); } } //--- search all broker symbols if Market Watch search fails int allSymbolsTotal = SymbolsTotal(false); for(int j = 0; j < allSymbolsTotal; j++) { string symbolName = SymbolName(j, false); if(StringFind(symbolName, canonicalSymbol) == 0) { if(SymbolSelect(symbolName, true)) return(symbolName); } } message = "No broker symbol found for canonical symbol: " + canonicalSymbol; return(""); } //+------------------------------------------------------------------+ //| Convert MT5 order type to readable text | //+------------------------------------------------------------------+ string OrderTypeToString(const ENUM_ORDER_TYPE orderType) { //--- convert internal MetaTrader 5 order type constants to log text if(orderType == ORDER_TYPE_BUY) return("BUY_MARKET"); if(orderType == ORDER_TYPE_SELL) return("SELL_MARKET"); if(orderType == ORDER_TYPE_BUY_LIMIT) return("BUY_LIMIT"); if(orderType == ORDER_TYPE_SELL_LIMIT) return("SELL_LIMIT"); if(orderType == ORDER_TYPE_BUY_STOP) return("BUY_STOP"); if(orderType == ORDER_TYPE_SELL_STOP) return("SELL_STOP"); return("UNKNOWN"); } //+------------------------------------------------------------------+ //| Build API URL | //+------------------------------------------------------------------+ string BuildUrl(const string endpoint) { string baseUrl = InpApiBaseUrl; //--- avoid double slashes when joining base URL and endpoint if(StringLen(baseUrl) > 0 && StringSubstr(baseUrl, StringLen(baseUrl) - 1, 1) == "/") baseUrl = StringSubstr(baseUrl, 0, StringLen(baseUrl) - 1); return(baseUrl + endpoint); } //+------------------------------------------------------------------+ //| Send HTTP GET request | //+------------------------------------------------------------------+ bool HttpGet(const string url, string &response) { char data[]; char result[]; string headers = ""; string resultHeaders = ""; int timeout = 5000; //--- GET requests do not need a request body ArrayResize(data, 0); ResetLastError(); //--- send HTTP GET request to the Flask backend int statusCode = WebRequest("GET", url, headers, timeout, data, result, resultHeaders); if(statusCode == -1) { Print("WebRequest GET failed. Error: ", GetLastError()); Print("Allow this URL in MT5: ", InpApiBaseUrl); return(false); } //--- convert byte response into readable UTF-8 string response = CharArrayToString(result, 0, -1, CP_UTF8); //--- non-2xx responses are treated as failed API requests if(statusCode < 200 || statusCode >= 300) { Print("HTTP GET failed. Status: ", statusCode); Print("Response: ", response); return(false); } return(true); } //+------------------------------------------------------------------+ //| Send execution status to Flask | //+------------------------------------------------------------------+ bool SendExecutionStatus(const long signalId, const string status, const ulong ticket, const string message) { //--- build endpoint URL for the specific signal being updated string url = BuildUrl("/api/v1/signals/" + IntegerToString(signalId) + "/status"); //--- build compact JSON body expected by the Flask endpoint string body = "{"; body += "\"status\":\"" + status + "\","; body += "\"ticket\":" + IntegerToString((long)ticket) + ","; body += "\"message\":\"" + JsonEscape(message) + "\""; body += "}"; char data[]; char result[]; string headers = "Content-Type: application/json\r\n"; string resultHeaders = ""; int timeout = 5000; //--- convert JSON string into UTF-8 char array for WebRequest int copied = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8); //--- remove the terminal null character added by StringToCharArray if(copied > 0) ArrayResize(data, copied - 1); ResetLastError(); //--- send execution result to the backend int statusCode = WebRequest("POST", url, headers, timeout, data, result, resultHeaders); string response = CharArrayToString(result, 0, -1, CP_UTF8); if(statusCode == -1) { Print("WebRequest POST failed. Error: ", GetLastError()); return(false); } //--- report failed status update attempts for debugging if(statusCode < 200 || statusCode >= 300) { Print("Status update failed. HTTP: ", statusCode); Print("Response: ", response); return(false); } Print("[STATUS] Execution status sent to Flask: ", status); return(true); } //+------------------------------------------------------------------+ //| Parse pending signal response | //+------------------------------------------------------------------+ bool ParsePendingSignal(const string json, SSignal &signal) { //--- reset all fields before parsing the new response signal.id = 0; signal.symbol = ""; signal.direction = ""; signal.intent = ""; signal.orderType = ""; signal.entryPrice = 0.0; signal.stopLoss = 0.0; signal.takeProfit = 0.0; signal.confidence = 0.0; //--- signal ID is required because it is used for status updates if(!JsonGetLong(json, "id", signal.id)) return(false); //--- extract text fields returned by the Flask API JsonGetString(json, "symbol", signal.symbol); JsonGetString(json, "direction", signal.direction); JsonGetString(json, "intent", signal.intent); JsonGetString(json, "order_type", signal.orderType); //--- extract numeric fields returned by the Flask API JsonGetDouble(json, "entry_price", signal.entryPrice); JsonGetDouble(json, "stop_loss", signal.stopLoss); JsonGetDouble(json, "take_profit", signal.takeProfit); JsonGetDouble(json, "confidence", signal.confidence); return(true); } //+------------------------------------------------------------------+ //| Extract long value from simple JSON | //+------------------------------------------------------------------+ bool JsonGetLong(const string json, const string key, long &value) { string raw = ""; //--- locate raw JSON value by key if(!JsonGetRawValue(json, key, raw)) return(false); //--- integer fields cannot be empty or null if(raw == "" || raw == "null") return(false); value = StringToInteger(raw); return(true); } //+------------------------------------------------------------------+ //| Extract double value from simple JSON | //+------------------------------------------------------------------+ bool JsonGetDouble(const string json, const string key, double &value) { string raw = ""; //--- locate raw JSON value by key if(!JsonGetRawValue(json, key, raw)) return(false); //--- optional numeric fields are treated as zero when null if(raw == "" || raw == "null") { value = 0.0; return(true); } value = StringToDouble(raw); return(true); } //+------------------------------------------------------------------+ //| Extract string value from simple JSON | //+------------------------------------------------------------------+ bool JsonGetString(const string json, const string key, string &value) { string pattern = "\"" + key + "\":"; int pos = StringFind(json, pattern); //--- key is not present in the JSON response if(pos < 0) return(false); //--- move cursor to the start of the value pos += StringLen(pattern); while(pos < StringLen(json) && IsWhitespace(StringGetCharacter(json, pos))) pos++; if(pos >= StringLen(json)) return(false); //--- null string fields are converted to empty strings if(StringSubstr(json, pos, 4) == "null") { value = ""; return(true); } //--- string values must begin with a double quote if(StringGetCharacter(json, pos) != '"') return(false); pos++; int end = StringFind(json, "\"", pos); if(end < 0) return(false); value = StringSubstr(json, pos, end - pos); return(true); } //+------------------------------------------------------------------+ //| Extract raw JSON value | //+------------------------------------------------------------------+ bool JsonGetRawValue(const string json, const string key, string &value) { string pattern = "\"" + key + "\":"; int pos = StringFind(json, pattern); //--- key must exist before a value can be extracted if(pos < 0) return(false); //--- move cursor from key name to value start pos += StringLen(pattern); while(pos < StringLen(json) && IsWhitespace(StringGetCharacter(json, pos))) pos++; int end = pos; //--- raw value ends at comma, object close, or line break while(end < StringLen(json)) { ushort ch = StringGetCharacter(json, end); if(ch == ',' || ch == '}' || ch == '\r' || ch == '\n') break; end++; } value = StringSubstr(json, pos, end - pos); StringTrimLeft(value); StringTrimRight(value); return(true); } //+------------------------------------------------------------------+ //| Check whitespace character | //+------------------------------------------------------------------+ bool IsWhitespace(const ushort ch) { //--- helper used by the lightweight JSON extraction functions return(ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); } //+------------------------------------------------------------------+ //| Normalize signal price | //+------------------------------------------------------------------+ double NormalizeSignalPrice(const string symbol, const double price) { //--- zero means the signal did not provide this price level if(price <= 0.0) return(0.0); //--- use broker symbol digits before sending prices to the server int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); return(NormalizeDouble(price, digits)); } //+------------------------------------------------------------------+ //| Escape JSON string | //+------------------------------------------------------------------+ string JsonEscape(string value) { //--- escape characters that can break the JSON status payload StringReplace(value, "\\", "\\\\"); StringReplace(value, "\"", "\\\""); StringReplace(value, "\r", "\\r"); StringReplace(value, "\n", "\\n"); return(value); } //+------------------------------------------------------------------+ //| Send successful execution notification | //+------------------------------------------------------------------+ void NotifyExecutionSuccess(const SSignal &signal, const string brokerSymbol, const ENUM_ORDER_TYPE orderType, const ulong ticket) { string slText = "Not provided"; string tpText = "Not provided"; int digits = (int)SymbolInfoInteger(brokerSymbol, SYMBOL_DIGITS); //--- show missing SL or TP clearly in the notification if(signal.stopLoss > 0.0) slText = DoubleToString(signal.stopLoss, digits); if(signal.takeProfit > 0.0) tpText = DoubleToString(signal.takeProfit, digits); //--- prepare a concise execution message for logs, alerts, and mobile push string notification = ""; notification += "Signal executed: " + OrderTypeToString(orderType) + " " + brokerSymbol + "\n"; notification += "Lot: " + DoubleToString(InpLotSize, 2) + "\n"; notification += "SL: " + slText + "\n"; notification += "TP: " + tpText + "\n"; notification += "Ticket: " + IntegerToString((long)ticket); Print(notification); Alert(notification); SendNotification(notification); } //+------------------------------------------------------------------+ //| Print parsed signal | //+------------------------------------------------------------------+ void PrintSignal(const SSignal &signal) { //--- print parsed signal fields for article-friendly visual feedback Print("[SIGNAL] Pending signal received."); Print("[SIGNAL] ID: ", signal.id); Print("[SIGNAL] Symbol: ", signal.symbol); Print("[SIGNAL] Direction: ", signal.direction); Print("[SIGNAL] Intent: ", signal.intent); Print("[SIGNAL] Order Type: ", signal.orderType); Print("[SIGNAL] Entry: ", DoubleToString(signal.entryPrice, 5)); Print("[SIGNAL] SL: ", DoubleToString(signal.stopLoss, 5)); Print("[SIGNAL] TP: ", DoubleToString(signal.takeProfit, 5)); Print("[SIGNAL] Confidence: ", DoubleToString(signal.confidence, 2)); } //+------------------------------------------------------------------+