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.

MethodWhere the message livesUse 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_message
strategy 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 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:

Alert setup
Condition: "Any alert() function call"

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)
Alert setup
Message box: {{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:

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.

Route your Pine Script alerts to a live terminal

Start Free 7-Day Trial   Pine Script Alert Docs →

Next Steps