Pine Script Webhook Alerts: Send Dynamic Signals from TradingView to MT4 & MT5
The message box in TradingView's alert dialog holds one fixed line of text. That is fine for a 0.1-lot EURUSD buy — but the moment you want the lot size to follow your account risk, or the stop loss to follow ATR, that box becomes the bottleneck. Pine Script can build the message instead, and Trading Router will execute whatever it builds. This guide shows both ways to do it, with working code.
Three Ways to Fire an Order From TradingView
Before touching Pine Script, it is worth knowing which of the three you actually need. Most traders never need more than the first.
| Method | Where the message lives | Use it when |
|---|---|---|
| Alert dialog only no code |
Typed into the alert's Message box | Fixed lot size and fixed stop. Works with any built-in indicator — see the no-code alert guide. |
alert()indicator or strategy |
Built as a string inside the script | Values change bar to bar, or one signal has to send several orders. |
alert_messagestrategy orders only |
An argument on strategy.entry / strategy.close |
You already have a backtested strategy and want the live orders to match its fills. |
All three send the same thing to the same place: a text message to https://webhook.tradingrouter.com, which Trading Router forwards to the EA on your MT4 or MT5 terminal. Webhook alerts require a paid TradingView plan (Essential or above). If you have not connected a terminal yet, start with the MT5 setup guide or the MT4 setup guide and come back here.
What the Message Has to Contain
Whatever builds it, the message is a comma-separated list of key=value pairs. A minimal market buy:
token=YOUR_MASTER_TOKEN, signal=buy, symbol=EURUSD, risk_lots=0.1
Four things are worth committing to memory before you start generating these in code:
- The token is the Master token, not the Client token. The Client token goes in the EA's Inputs tab; the Master token goes in the alert message. Mixing them up is the single most common reason a correct-looking alert does nothing.
symbol=is the broker's name for the instrument. If your broker calls itEURUSDm, either send that or set up symbol mapping in the EA.sl_pipsandtp_pipsare points, not pips — generally 10 points to a pip.sl_pips=300on a 5-digit EURUSD is 30 pips. The pips and points page explains it, and the IdentifyPoints EA on the Downloads page tells you the exact point value for any symbol.- Risk parameters that size by loss need a stop.
risk_lots=is a literal volume, butrisk_pct_bal_loss=andrisk_cncy_amt=calculate the volume from the stop distance, so an order using either of them must also carry a stop loss — from the message or from the EA settings.
The full parameter list is in the alert syntax reference.
Method 1 — alert() Inside an Indicator
alert() fires the moment the script executes that line. You pass it the finished message and a frequency. Here is a complete EMA crossover indicator that closes anything open and reverses, in one message:
//@version=6
indicator("EMA Cross to Trading Router", overlay = true)
fastLen = input.int(20, "Fast EMA")
slowLen = input.int(50, "Slow EMA")
token = input.string("YOUR_MASTER_TOKEN", "Master token")
lots = input.float(0.10, "Lots", step = 0.01)
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
plot(fast, "Fast EMA", color.aqua)
plot(slow, "Slow EMA", color.orange)
sym = syminfo.ticker
head = "token=" + token + ", symbol=" + sym + ", signal=closeall"
size = ", risk_lots_x1=" + str.tostring(lots)
if ta.crossover(fast, slow)
alert(head + ", signal_x1=buy, symbol_x1=" + sym + size, alert.freq_once_per_bar_close)
if ta.crossunder(fast, slow)
alert(head + ", signal_x1=sell, symbol_x1=" + sym + size, alert.freq_once_per_bar_close)
Then create the alert:
Add the script to your chart, open the alert dialog, and set the condition to your script name — then pick Any alert() function call from the second dropdown. Leave the Message box alone; the text your script passes to alert() replaces it. On the Notifications tab, tick Webhook URL and enter https://webhook.tradingrouter.com. One alert covers both directions.
Two details in that script matter more than they look. syminfo.ticker returns the bare symbol — EURUSD rather than OANDA:EURUSD — so the same script works on any chart without editing the message. And alert.freq_once_per_bar_close means the order is only sent once the bar is final, which is what stops a crossover that forms and unforms mid-bar from opening a real position. More on that below.
Method 2 — alert_message on Strategy Orders
If you have a strategy() script you have already backtested, attach the message to the order itself. It is delivered when that order fills, so what your broker does and what the strategy tester shows stay in step:
//@version=6
strategy("RSI Reversion to Trading Router", overlay = true, calc_on_every_tick = false)
token = input.string("YOUR_MASTER_TOKEN", "Master token")
sym = syminfo.ticker
r = ta.rsi(close, 14)
entryMsg = "token=" + token + ", signal=buy, symbol=" + sym + ", risk_pct_bal_loss=1, sl_pips=300"
exitMsg = "token=" + token + ", signal=closelong, symbol=" + sym
if ta.crossover(r, 30)
strategy.entry("Long", strategy.long, alert_message = entryMsg)
if ta.crossunder(r, 70)
strategy.close("Long", alert_message = exitMsg)
{{strategy.order.alert_message}}
Set the condition to the strategy itself. In the Message box, delete the default text and enter exactly {{strategy.order.alert_message}} — that placeholder is what pulls in whichever alert_message belongs to the order that just fired. Webhook URL as before.
Note what risk_pct_bal_loss=1 is doing there: the EA sizes the position so that hitting the 300-point stop costs 1% of the account balance. The volume adjusts to the account, so the same script runs correctly on a $2,000 account and a $200,000 one with no edits.
Making the Numbers Dynamic
This is the whole point of building the message in code. Any Pine series can become an order parameter via str.tostring(). A stop loss that adapts to current volatility:
atrPts = ta.atr(14) / syminfo.mintick // ATR expressed in points
slTxt = str.tostring(atrPts * 1.5, "#") // 1.5 x ATR, rounded to whole points
msg = "token=" + token + ", signal=buy, symbol=" + sym + ", risk_pct_bal_loss=1, sl_pips=" + slTxt
if longCondition
alert(msg, alert.freq_once_per_bar_close)
The "#" format string rounds to a whole number, which matters because the message needs sl_pips=412 and not sl_pips=412.3871. Dividing by syminfo.mintick converts a price distance into points — verify the result against IdentifyPoints for anything other than standard forex pairs, since brokers do not all define a point the same way.
Placeholders belong in the alert dialog, not in your strings. {{close}}, {{ticker}} and friends are substituted by TradingView into the Message box. Inside a script-built string you already hold the real value, so use str.tostring(close) and syminfo.ticker instead. The one placeholder you do type by hand is {{strategy.order.alert_message}}.
Several Orders From One Signal
One webhook message can carry a main command plus up to five extra ones, suffixed _x1 through _x5. They share the single token and execute in order — main first, then _x1, then _x2, and so on. That is how the crossover script above closes and reverses atomically rather than hoping two separate alerts arrive in the right sequence.
It scales to a whole basket. Enter EURUSD, hedge with a GBPUSD short, and leave a stop order resting above:
token=YOUR_MASTER_TOKEN, signal=buy, symbol=EURUSD, risk_lots=0.1,
signal_x1=sell, symbol_x1=GBPUSD, risk_lots_x1=0.05,
signal_x2=buystop, symbol_x2=EURUSD, price_pips_x2=200, risk_lots_x2=0.1
Built in Pine, each of those volumes can be a calculated value. See multi commands for the rules.
Tag Positions So You Can Manage Them Later
By default a closeall closes everything on that symbol — including trades from a different script, or ones you opened by hand. Adding trid= to an entry labels that position, and a close carrying the same trid touches only what it labelled:
id = "ema_" + str.tostring(bar_index)
if ta.crossover(fast, slow)
alert("token=" + token + ", signal=buy, trid=" + id + ", symbol=" + sym + ", risk_lots=0.1", alert.freq_once_per_bar_close)
A later signal=closeall, symbol=EURUSD, trid=ema_1234 then closes that one position and leaves the rest of the account alone. Trade IDs may contain letters, digits and underscores, they are case sensitive, and they are held by the EA in memory — restarting the EA clears them. Full behaviour: trid parameter.
Don't Let Repainting Fire Ghost Trades
On a backtest, a signal that appears mid-bar and vanishes before the close costs nothing. Live, it has already sent a real order to a real broker. Three habits prevent it:
- Use
alert.freq_once_per_bar_closeas the second argument toalert(). The default fires intrabar. - Or guard the condition with
barstate.isconfirmedwhen you need finer control over which line fires. - Leave
calc_on_every_tickoff in strategies — the default — so orders evaluate on closed bars.
Referencing a previous bar rather than the forming one is the same idea applied to the condition itself: close > ta.highest(high, 20)[1] tests a breakout against a high that can no longer change.
Five Things That Stop a Correct-Looking Alert
1. Wrong condition in the dropdown
An alert() script needs Any alert() function call. A strategy needs the strategy itself plus {{strategy.order.alert_message}} in the Message box. Choosing the plotted series by mistake fires an alert with the default text, which Trading Router cannot parse.
2. Client token in the message
Master token in the alert, Client token in the EA Inputs. Both are on your dashboard, and they are case sensitive.
3. Broker symbol suffix
syminfo.ticker gives you TradingView's name. If the broker's differs — EURUSDm, EURUSD.raw — map it once in symbol mapping rather than hard-coding it into every script.
4. Risk parameter without a stop
risk_pct_bal_loss= and risk_cncy_amt= derive the volume from the stop distance. With no stop in the message and none in the EA settings, there is nothing to size against and the order is rejected. risk_lots= has no such requirement.
5. Stray characters in the built string
A missing ", " between two concatenated fragments silently produces symbol=EURUSDsignal=buy. Add label.new(bar_index, high, msg) to the chart while developing so you can read the exact string the script would send.
Test It Before You Trade It
Paste the string your script generates into the Test Alert tool in your dashboard. It sends that exact message to your EA, so you can confirm the parse and the routing without waiting for a signal. The MT4/MT5 Journal tab logs every message the EA receives — if a trade did not appear, the reason is almost always sitting in there.
Next Steps
- Alert syntax reference — every signal, risk, stop, trailing and filter parameter
- 5 TradingView strategies you can automate today — conditions to put the code above to work on
- EA settings explained — what to configure in the terminal rather than the message
- Trade copier — fan one alert out to as many as 20 accounts