A TradingView to MT5 bridge has two halves. One half gets the alert from TradingView to the terminal, usually a webhook to a small server that the EA polls. The other half decides what the alert means. Most bridge failures I see are in the second half: a typo in the message template, a symbol that does not exist on the broker, a missing stop loss, or the same alert delivered twice. Each of those can turn into a wrong trade if the EA trusts the text.
This post shows a small parser and validator for a plain key and value message. It does not cover the transport, only what happens once the EA has the string.
The message format
Keep the format simple enough to read in a log. In the TradingView alert message box:
id={{ticker}}_{{timenow}};action={{strategy.order.action}};symbol={{ticker}};risk=0.5;sl={{plot_0}}
Which gives a message like:
id=EURUSD_2026-11-10T06:00:00Z;action=buy;symbol=EURUSD;risk=0.5;sl=1.0820
Fields are separated by semicolons and each field is a key, an equals sign and a value. The id must be unique per alert, which is why it combines the ticker and the alert time.
Parsing
input string SymbolSuffix = ""; // broker suffix, for example ".m"
input double MaxRiskPct = 1.0; // reject alerts asking for more
struct Signal
{
string id;
string action; // buy, sell or close
string symbol;
double riskPct;
double slPrice;
};
string g_lastId = "";
string Trimmed(string s)
{
StringTrimLeft(s);
StringTrimRight(s);
return s;
}
bool ParseSignal(const string msg, Signal &s, string &reason)
{
s.id = ""; s.action = ""; s.symbol = ""; s.riskPct = 0.0; s.slPrice = 0.0;
string parts[];
int n = StringSplit(Trimmed(msg), ';', parts);
if(n < 2)
{
reason = "not a key=value message";
return false;
}
for(int i = 0; i < n; i++)
{
if(Trimmed(parts[i]) == "")
continue; // tolerate a trailing semicolon
string kv[];
if(StringSplit(parts[i], '=', kv) != 2)
{
reason = "bad field '" + parts[i] + "'";
return false;
}
string key = Trimmed(kv[0]);
string val = Trimmed(kv[1]);
StringToLower(key);
if(key == "id")
s.id = val;
else if(key == "action")
{
StringToLower(val);
s.action = val;
}
else if(key == "symbol")
s.symbol = val;
else if(key == "risk")
s.riskPct = StringToDouble(val);
else if(key == "sl")
s.slPrice = StringToDouble(val);
else
PrintFormat("Ignoring unknown key '%s'", key);
}
return true;
}
Note that StringToDouble returns 0 for text it cannot read. That is why the validator below treats zero risk and a zero stop as errors instead of defaults.
Mapping the symbol
TradingView tickers and broker symbols rarely match exactly. Some alerts include an exchange prefix such as FX:EURUSD, and many brokers add a suffix.
string MapSymbol(string tv)
{
int colon = StringFind(tv, ":");
if(colon >= 0)
tv = StringSubstr(tv, colon + 1);
StringToUpper(tv);
return tv + SymbolSuffix;
}
If a broker uses completely different names, such as GOLD for XAUUSD, replace this with a small lookup table loaded from inputs or a file.
Validating
bool ValidateSignal(Signal &s, string &reason)
{
if(s.id == "")
{
reason = "missing id";
return false;
}
if(s.id == g_lastId)
{
reason = "duplicate id";
return false;
}
if(s.action != "buy" && s.action != "sell" && s.action != "close")
{
reason = "unknown action '" + s.action + "'";
return false;
}
string sym = MapSymbol(s.symbol);
if(s.symbol == "" || !SymbolSelect(sym, true))
{
reason = "unknown symbol '" + s.symbol + "' mapped to '" + sym + "'";
return false;
}
s.symbol = sym;
if(s.action == "close")
return true;
if(s.riskPct <= 0.0 || s.riskPct > MaxRiskPct)
{
reason = StringFormat("risk %.2f outside 0 to %.2f", s.riskPct, MaxRiskPct);
return false;
}
if(s.slPrice <= 0.0)
{
reason = "missing or unreadable stop loss";
return false;
}
double bid = SymbolInfoDouble(sym, SYMBOL_BID);
double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
if(s.action == "buy" && s.slPrice >= bid)
{
reason = "buy stop loss is not below price";
return false;
}
if(s.action == "sell" && s.slPrice <= ask)
{
reason = "sell stop loss is not above price";
return false;
}
return true;
}
void HandleAlert(const string msg)
{
Signal s;
string reason = "";
if(!ParseSignal(msg, s, reason) || !ValidateSignal(s, reason))
{
PrintFormat("Alert rejected (%s): %s", reason, msg);
return;
}
g_lastId = s.id;
int digits = (int)SymbolInfoInteger(s.symbol, SYMBOL_DIGITS);
PrintFormat("Alert accepted: %s %s risk %.2f%% sl %s",
s.action, s.symbol, s.riskPct, DoubleToString(s.slPrice, digits));
// pass s to the order code from here
}
Every rejection prints the reason and the raw message, so a broken template shows up in the Experts tab the first time it fires instead of after a week of missed trades.
Practical points
- Remember more than one id. Webhook retries can deliver an older alert after a newer one, so keep the last few dozen ids in an array, and save them to a file so a restart does not forget them.
- Reject stale alerts. Have the server stamp each alert with the time it arrived and let the EA drop anything older than a set number of seconds. An entry signal from ten minutes ago is not the same signal.
- Never put account numbers, passwords or API keys in the alert text. The message passes through TradingView and your server logs.
- Test the parser with a script before connecting it to real orders. Feed it a list of good and deliberately broken messages and check that each broken one is rejected with a clear reason.
- Keep the stop loss in the alert even if the EA also computes one. Comparing the two is a cheap check that the chart and the terminal agree on price.
A parser like this is not glamorous, but it is the part of the bridge that decides whether a typo becomes a log line or a position.
Top comments (0)