PINE SCRIPT V6 · UPDATED OCTOBER 2026

Convert a Pine Script indicator into a strategy and automate it.

Your indicator paints arrows on the chart, but arrows do not backtest and they do not trade. This guide converts a Pine Script v6 indicator into a strategy line by line, makes the backtest honest, removes the repainting traps and sends every order fill to MetaTrader 5 as a JSON the bridge understands.

BBenjamin SF · Founder Published Oct 4, 2026 Read 9 min

The short answer

Converting an indicator into a strategy is five edits, not a rewrite. Everything below was checked against TradingView's official Pine Script v6 documentation (verified 4 October 2026).

  • Swap the declaration: indicator() becomes strategy(), with capital, position size, commission and slippage.
  • Turn signals into orders: each plotshape() or alertcondition() condition drives a strategy.entry(), strategy.close() or strategy.exit() call.
  • Make the backtest honest: real costs, sensible size and no future data.
  • Remove repainting: no lookahead leaks, no calc_on_every_tick unless you mean it.
  • Alert on order fills: attach a JSON to every order with alert_message and put {{strategy.order.alert_message}} in the alert.

If you have not decided yet whether to convert your logic to MQL5 at all, read Pine Script to EA: the 3 real ways first. This page assumes you keep the logic on TradingView and only convert it from indicator to strategy.

Step 1: indicator() becomes strategy()

The declaration is where a strategy gets everything an indicator never needed: money, size and costs. These are the strategy() parameters that matter for a conversion:

Parameter
What it controls
Sensible start
initial_capital
Starting balance of the backtest.
Your real account size.
default_qty_type / default_qty_value
Order size: strategy.fixed, strategy.percent_of_equity or strategy.cash.
The exposure you will really trade, in TradingView's units.
pyramiding
Maximum entries in the same direction. Default 1.
Keep 1 unless your logic scales in.
commission_type / commission_value
strategy.commission.percent, cash_per_contract or cash_per_order.
Your broker's real commission. Zero flatters every backtest.
slippage
Extra ticks added to market and stop fills.
A few ticks; more on news-driven symbols.
margin_long / margin_short
Margin check. In v6 the default is 100 (in v5 it was 0, no check).
Leave the v6 default unless you know your leverage.
calc_on_every_tick
Recalculate on every realtime tick instead of on bar close.
false (the default). See the repainting section.
process_orders_on_close
Fill orders at the close of the signal bar instead of the next open.
false, to match what a live alert can actually do.

Two v6 details that break old code: the when parameter was removed from strategy.entry(), strategy.close() and the other order functions (use an if block), and alertcondition() still compiles inside a strategy but TradingView will not let you create alerts from it.

Step 2: signals become orders

An indicator says "something happened". A strategy says "do this with the position". The mapping is mechanical:

Indicator → strategy, piece by piece
plotshape(longSignal)
→
strategy.entry("Long", strategy.long). If a short is open, entry reverses it automatically.
plotshape(shortSignal)
→
strategy.entry("Short", strategy.short). Same reversal logic in the other direction.
exit condition
→
strategy.close("Long") closes the position opened by that entry id at market.
fixed SL / TP
→
strategy.exit("XL", "Long", loss = ..., profit = ...), distances in ticks.
scaling, no auto-reverse
→
strategy.order() ignores pyramiding and never reverses on its own. Use it only if you manage the position yourself.
alertcondition(...)
→
alert_message on each order call, read by one order-fill alert.

Here is a minimal indicator: two EMAs, arrows on the crosses and two alert conditions.

Before: the indicatorPINE v6
//@version=6
indicator("EMA cross - indicator", overlay = true)
fastLen = input.int(9, "Fast EMA")
slowLen = input.int(21, "Slow EMA")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
longSignal  = ta.crossover(fast, slow)
shortSignal = ta.crossunder(fast, slow)
plot(fast, "Fast", color.lime)
plot(slow, "Slow", color.orange)
plotshape(longSignal,  "Long",  shape.triangleup,   location.belowbar, color.lime)
plotshape(shortSignal, "Short", shape.triangledown, location.abovebar, color.red)
alertcondition(longSignal,  "Long",  "EMA cross up")
alertcondition(shortSignal, "Short", "EMA cross down")

And the same logic as a strategy. The signal lines are untouched; only the declaration and the last block change.

After: the strategy, with bridge-ready alertsPINE v6
//@version=6
strategy("EMA cross - strategy", overlay = true,
     initial_capital = 10000,
     default_qty_type = strategy.fixed, default_qty_value = 1,
     pyramiding = 1,
     commission_type = strategy.commission.percent, commission_value = 0.01,
     slippage = 2)

fastLen = input.int(9, "Fast EMA")
slowLen = input.int(21, "Slow EMA")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
longSignal  = ta.crossover(fast, slow)
shortSignal = ta.crossunder(fast, slow)
plot(fast, "Fast", color.lime)
plot(slow, "Slow", color.orange)

// JSON for the bridge. SL/TP in pips (dashboard default unit)
sym     = syminfo.ticker
buyMsg  = '{"action":"buy","symbol":"'  + sym + '","sl":30,"tp":60}'
sellMsg = '{"action":"sell","symbol":"' + sym + '","sl":30,"tp":60}'

if longSignal
    strategy.entry("Long", strategy.long, alert_message = buyMsg)
if shortSignal
    strategy.entry("Short", strategy.short, alert_message = sellMsg)

// Same 30/60 pips in the backtest (5-digit FX: 1 pip = 10 ticks)
strategy.exit("XL", "Long",  loss = 300, profit = 600,
     alert_message = '{"action":"close","symbol":"' + sym + '","side":"buy"}')
strategy.exit("XS", "Short", loss = 300, profit = 600,
     alert_message = '{"action":"close","symbol":"' + sym + '","side":"sell"}')

Why it is built this way. The stop and target travel with the entry ("sl":30,"tp":60, read as pips in SignalForge's default unit), so the position is protected at the broker even if a later alert never arrives. The strategy.exit() calls mirror those distances for the backtest: on a 5-digit forex symbol one pip is 10 ticks, so 300 and 600 ticks equal 30 and 60 pips. On other symbols, recalculate. The close messages use "side" so a long exit only closes buys. And there is no lot field: the EA sizes the trade with the position size type of your dashboard.

The EMA cross is a placeholder to show the mechanics. It is not a tested edge, and nothing on this page claims it is profitable.

Step 3: make the backtest honest

  • Fills happen on the next bar. With default settings the broker emulator fills a new order at the open of the following bar. A live webhook leaves seconds after the signal bar closes, so that is the closest match. Turning on process_orders_on_close fills at a price your live alert could never get.
  • Costs are not optional. Set commission_type, commission_value and slippage to your broker's numbers. A strategy that only works at zero cost does not work.
  • Size is in TradingView's units, not MT5 lots. Depending on the symbol, default_qty_value counts contracts, shares or units of the base currency. Pick a value that reproduces your real exposure, and remember that MT5 will use its own lot rules.
  • The spread is yours to model. TradingView's data is not your broker's feed. Check results on MT5 demo before trusting the equity curve.

Step 4: remove repainting before it reaches your account

TradingView's docs define repainting as script behaviour that differs between historical and realtime bars, and estimate that more than 95% of indicators show some form of it. Converting to a strategy does not fix it; it exposes it. Check three things:

  • request.security() with lookahead. Using lookahead = barmerge.lookahead_on without offsetting the series leaks future data into historical bars. The official non-repainting pattern pairs it with [1]:
Higher timeframe value, no future leakPINE v6
// Higher timeframe without future leak (pattern from the Pine docs)
htfClose = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)
  • calc_on_every_tick = true. The strategy then runs on every realtime update while history ran on bar closes, so live orders will most likely differ from the backtest. Keep it off unless your logic truly needs ticks.
  • Once per bar close. Strategies execute on the close of realtime bars by default, and unless calc_on_every_tick is on, every alert() call inside a strategy fires once per bar close whatever frequency you pass. If you automate the indicator instead, add and barstate.isconfirmed to the condition or use alert.freq_once_per_bar_close.

More on alert frequencies and the alert() function in Pine Script alert() for webhooks.

Step 5: order-fill alerts the bridge understands

Create one alert on the strategy. In the condition pick the strategy and its order fill events; in the message box put only the placeholder below; in the Notifications tab tick Webhook URL and paste your SignalForge URL (your token goes in the URL, never in the JSON). Webhooks need a paid TradingView plan (Essential or higher).

Alert dialog, message boxPLACEHOLDER
{{strategy.order.alert_message}}

Each fill now sends the JSON attached to that order. TradingView replaces the placeholder with an empty string for orders that have no alert_message, and an empty body is rejected with HTTP 400, so give every order call its own message.

You can also build the message in the dialog with the generic placeholders, and it is tempting:

Tempting, but riskyJSON
{"action":"{{strategy.order.action}}","symbol":"{{ticker}}","lot":{{strategy.order.contracts}}}
  • {{strategy.order.action}} returns only "buy" or "sell". A fill that closes a long is a sell. Sent as "action":"sell", the EA closes the buy and then opens a sell, while your strategy is flat. Explicit "action":"close" messages avoid that.
  • {{strategy.order.contracts}} is not a lot. It is the strategy quantity in TradingView's units. A value outside 0.01-500 is rejected with HTTP 422, and in fixed-lot mode (the dashboard default, 0.01) the alert's lot is ignored anyway. Use the lot from the signal mode only if you send a lot you calculated for MT5.
  • {{ticker}} is TradingView's name. The EA resolves common broker suffixes by itself, but if your broker uses a different name (GOLD instead of XAUUSD, for example), send the broker's name. Details in wrong symbol, lot size or account on MT5.
  • Never generate "account" with a placeholder. Write the login as a quoted literal; numeric formatting can insert thousands separators and break the JSON.

Other payload shapes (pending orders, partial closes by percent, comments to separate strategies) are in the TradingView webhook JSON examples and the documentation.

Before you go live

  • Validate the JSON. Paste the exact message each order produces into the free webhook tester. A missing quote in a concatenated string is the most common bug.
  • Recreate the alert after every edit. TradingView runs a saved copy of the script, inputs, symbol and timeframe; changes do not reach an existing alert.
  • Run it on a demo account first. In the dashboard every signal shows as EXECUTED, FILTERED, FAILED, NOT_DELIVERED or PENDING with its reason, and the MT5 Experts tab shows [PROC] when the signal arrives and [SEND] when the order goes out.
  • Check your filters. Pyramiding defaults to one trade per direction per symbol, and the time, news and trend filters or Prop Firm Shield can block a valid entry on purpose.
  • Expect seconds, not milliseconds, from the chart. SignalForge measured about 0.5 s median from webhook to confirmed order (19 to 23 September 2026, benchmarks), but TradingView itself takes a few seconds to send the webhook after the bar closes.

FAQ

Can I convert any TradingView indicator into a strategy?

Only if you can edit its source code. Open-source scripts can be copied and changed; protected and invite-only scripts hide their code, so you cannot add strategy() calls to them. In that case keep the indicator and automate its own alerts instead.

Do I need a strategy to automate a Pine Script indicator?

No. An indicator can send webhooks too, through alertcondition() or alert(). A strategy adds two things: a backtest with commission and slippage, and a single order-fill alert that covers every entry and exit of the script.

Why do my live alerts not match the backtest?

Common causes: the backtest fills at the next bar open while the live order fills seconds after the bar closes, repainting logic (request.security with lookahead, calc_on_every_tick), missing commission and slippage, and your broker's real spread. TradingView itself takes a few seconds to send the webhook.

Should I send {{strategy.order.contracts}} as the MT5 lot?

Usually not. TradingView counts strategy quantity in its own units (contracts, shares or currency units depending on the symbol), not MT5 lots. Send a lot you chose, or leave the lot out and let the dashboard size the trade. In fixed-lot mode the lot in the alert is ignored anyway.

What happens on MT5 when the strategy reverses from long to short?

The reversal arrives as one sell order fill. With hedging off, which is the SignalForge default, an opposite signal first closes the open buy and then the sell is executed, so MT5 follows the strategy.

Do I have to recreate the alert after editing the script?

Yes. TradingView saves a copy of the script, inputs, symbol and timeframe when you create the alert, so later edits do not reach a running alert. Delete it and create a new one, with the webhook URL again.

Your strategy on TradingView. Its orders on MT5.

Send every order fill of your Pine strategy to MetaTrader 5, with lot, filters and Prop Firm Shield set per account. 14-day free trial, no card required.

SignalForge AI is an order-execution tool. We do not provide investment advice. The code on this page is an educational example, not a profitable or backtested strategy. Trading involves risk of loss.

Strategy ready? Send its fills to MT5 · from $4.99/moSee pricing →