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