How to Convert a Pine Script Indicator to a Strategy
Step-by-step guide to converting a Pine Script indicator to a strategy. Covers code changes, backtesting, alerts, and automating execution.
13 min read
In this article ▾
- What actually changes when you convert an indicator to a strategy
- Before you touch the code: pick the right indicator
- Converting a Pine Script indicator to a strategy, step by step
- Your strategy works on the chart. Now what?
- From strategy to live trades: alerts, webhooks, and execution
- What breaks between your backtest and live execution
- Converting a Pine Script indicator to a strategy for prop firm evaluations
- Automating your converted strategy with FillEdge
- FAQ
You found an indicator that nails entries. Maybe it's an EMA crossover with a volatility filter, maybe it's a custom oscillator you pulled from TradingView's public library. It draws arrows on the chart exactly where you'd want to buy and sell. So you think: why not make it trade for me?
That's the conversion. Indicator to strategy. And the code changes are honestly the easy part. The tricky part is everything that follows: understanding what the Strategy Tester is actually showing you, setting up alerts that carry trade data, and getting those signals to a broker without something breaking along the way.
This guide covers the full sequence. We'll start with the code, move through backtesting, and end at live execution.
What actually changes when you convert an indicator to a strategy
An indicator and a strategy look similar in Pine Script. Both run on every bar. Both can calculate moving averages, plot lines, and color backgrounds. The difference is what they're allowed to do with the result.
An indicator observes. It takes price data, runs math on it, and draws something on the chart. That's it. It can't simulate orders. It can't track a position. It has no concept of "I'm currently long 0.5 lots of EURUSD."
A strategy simulates. It does everything an indicator does, but it also talks to TradingView's order engine. It can call strategy.entry() to open a position, strategy.close() to exit, and strategy.exit() to set a stop-loss or take-profit. TradingView tracks those simulated orders, builds an equity curve, and shows you a performance report in the Strategy Tester tab.
The declaration line reflects this:
// Indicator
indicator("My Crossover Signal", overlay=true)
// Strategy
strategy("My Crossover Strategy", overlay=true, initial_capital=10000,
default_qty_type=strategy.percent_of_equity, default_qty_value=100)Notice the strategy version takes extra parameters. initial_capital sets how much simulated money the backtest starts with. default_qty_type and default_qty_value control position sizing. These don't exist in indicators because indicators don't place orders.
This distinction matters more than it looks. An indicator can display a green arrow for every "buy" condition and a red arrow for every "sell" condition. But it has no idea whether you're already in a position. A strategy does. If you call strategy.entry("Long", strategy.long) while you're already long, TradingView won't double your position (unless you explicitly configure pyramiding). The order engine handles state for you.
Before you touch the code: pick the right indicator
Not every indicator is worth converting. Some indicators are informational overlays. They show you a Bollinger Band, a volume profile, or a pivot level. They add context, but they don't produce a clear "buy here, sell here" signal.
For a conversion to work, your indicator needs two things: an unambiguous entry condition and an unambiguous exit condition. "Price crosses above the 20 EMA while RSI is below 30" is a clean entry. "The trend looks bullish based on the color of the background" is not.
Look at your indicator's source code. Search for the variables that control signal plotting. You'll typically find something like this:
longCondition = ta.crossover(shortEMA, longEMA) and rsi < 30
shortCondition = ta.crossunder(shortEMA, longEMA) and rsi > 70
plotshape(longCondition, style=shape.triangleup,
location=location.belowbar, color=color.green)
plotshape(shortCondition, style=shape.triangledown,
location=location.abovebar, color=color.red)Those longCondition and shortCondition booleans are what you need. If your indicator has them (or something equivalent), the conversion is straightforward. If your indicator just draws lines and leaves interpretation to you, you'll need to define those conditions yourself before converting.
One more filter: check if the indicator repaints. An indicator that changes its past signals after the bar closes will produce a strategy that looks amazing in backtest but trades differently in real time. Common culprits include indicators using security() calls on higher timeframes without lookahead=barmerge.lookahead_off, or indicators that reference close on the current bar during an intrabar calculation. If you're not sure whether your indicator repaints, add it to a chart and watch it on a 1-minute timeframe for 30 minutes. If arrows appear and then vanish, it repaints.
Converting a Pine Script indicator to a strategy, step by step
Here's a complete before-and-after. We'll take a basic EMA crossover indicator with an RSI filter and convert it into a strategy.
Before (indicator):
//@version=6
indicator("EMA Cross + RSI Filter", overlay=true)
// Inputs
emaFast = input.int(9, "Fast EMA")
emaSlow = input.int(21, "Slow EMA")
rsiLen = input.int(14, "RSI Length")
rsiOB = input.int(70, "RSI Overbought")
rsiOS = input.int(30, "RSI Oversold")
// Calculations
fastLine = ta.ema(close, emaFast)
slowLine = ta.ema(close, emaSlow)
rsiVal = ta.rsi(close, rsiLen)
// Conditions
longCondition = ta.crossover(fastLine, slowLine) and rsiVal < rsiOS
shortCondition = ta.crossunder(fastLine, slowLine) and rsiVal > rsiOB
// Plots
plot(fastLine, color=color.blue)
plot(slowLine, color=color.orange)
plotshape(longCondition, style=shape.triangleup,
location=location.belowbar, color=color.green, size=size.small)
plotshape(shortCondition, style=shape.triangledown,
location=location.abovebar, color=color.red, size=size.small)After (strategy):
//@version=6
strategy("EMA Cross + RSI Filter", overlay=true, initial_capital=10000,
default_qty_type=strategy.percent_of_equity, default_qty_value=100,
commission_type=strategy.commission.percent, commission_value=0.01,
slippage=2)
// Inputs
emaFast = input.int(9, "Fast EMA")
emaSlow = input.int(21, "Slow EMA")
rsiLen = input.int(14, "RSI Length")
rsiOB = input.int(70, "RSI Overbought")
rsiOS = input.int(30, "RSI Oversold")
slPoints = input.int(50, "Stop-Loss (points)")
tpPoints = input.int(100, "Take-Profit (points)")
// Calculations
fastLine = ta.ema(close, emaFast)
slowLine = ta.ema(close, emaSlow)
rsiVal = ta.rsi(close, rsiLen)
// Conditions
longCondition = ta.crossover(fastLine, slowLine) and rsiVal < rsiOS
shortCondition = ta.crossunder(fastLine, slowLine) and rsiVal > rsiOB
// Entries
if longCondition
strategy.entry("Long", strategy.long)
if shortCondition
strategy.entry("Short", strategy.short)
// Exits with SL/TP
strategy.exit("Long Exit", "Long",
stop=strategy.position_avg_price - slPoints * syminfo.mintick,
limit=strategy.position_avg_price + tpPoints * syminfo.mintick)
strategy.exit("Short Exit", "Short",
stop=strategy.position_avg_price + slPoints * syminfo.mintick,
limit=strategy.position_avg_price - tpPoints * syminfo.mintick)
// Keep the plots for visual reference
plot(fastLine, color=color.blue)
plot(slowLine, color=color.orange)
plotshape(longCondition, style=shape.triangleup,
location=location.belowbar, color=color.green, size=size.small)
plotshape(shortCondition, style=shape.triangledown,
location=location.abovebar, color=color.red, size=size.small)Let's walk through what changed.
indicator() became strategy(). The extra parameters matter. commission_type and commission_value tell the backtester to deduct commission on each trade, so your equity curve isn't unrealistically clean. slippage=2 simulates 2 ticks of slippage per fill. Without these, your backtest will show results you'll never see in a live account.
The plotshape() calls stayed, but we added strategy.entry() calls. When longCondition fires, the strategy opens a long. When shortCondition fires, it opens a short. Because we didn't enable pyramiding, a new strategy.entry("Long", ...) while already long won't add to the position.
strategy.exit() sets the stop-loss and take-profit as actual price levels relative to the average entry price. This is where most conversions get interesting, because your indicator probably didn't have SL/TP logic at all. You're adding risk management that didn't exist in the original.
What about the plotshape() calls? You can keep them. They still work inside a strategy. They're useful for visual confirmation that the strategy is entering where you expect. Some traders remove them to reduce chart clutter, but there's no technical reason to.
Common conversion gotchas
calc_on_every_tick: By default, strategies calculate only on bar close. Your indicator might have been evaluating conditions on every tick. If your entry conditions depend on intrabar price movements, add calc_on_every_tick=true to the strategy() declaration. Be aware that this slows backtesting and can produce results different from those in bar-close-only mode.
process_orders_on_close: This setting lets the strategy fill orders at the close of the bar where the signal fires, instead of waiting for the next bar's open. It produces more optimistic backtests. Keep it false unless you have a specific reason.
max_bars_back: If your indicator uses ta.valuewhen() or references bars far in the past, you might hit a runtime error after conversion. Strategies sometimes need more historical bars than indicators. Add max_bars_back=500 (or whatever your lookback requires) to the strategy() call.
Alerts in the indicator version: If your indicator used alertcondition(), you'll need to replace it. Strategies don't support alertcondition(). Use alert() instead, or attach your signal data to alert_message within the strategy.entry() call. More on this in the alerts section below.
Your strategy works on the chart. Now what?
You've converted the code. The Strategy Tester tab shows trades, an equity curve, and a performance summary. Before you do anything else, spend some time understanding what you're looking at.
The Strategy Tester's default settings are generous. It assumes zero slippage unless you specified it. It fills limit orders at the exact price, which doesn't happen in real markets. And it calculates on bar close, meaning every entry in the backtest happened at a price that was already confirmed when the signal fired. Live trading doesn't work that way.
A few settings to adjust before trusting the numbers:
Set commission_value to match your broker. IC Markets charges around $3.50 per lot per side on raw-spread accounts. Pepperstone is similar. If you're on a standard account with wider spreads, bump it up. Even 0.01% commission changes your net profit significantly over hundreds of trades.
Set slippage to at least 1–2 ticks. On liquid pairs like EURUSD, you might get away with 1. On something like XAUUSD during a news spike, 5+ ticks isn't unusual.
Switch between timeframes. Your strategy might look profitable on 15-minute bars but fall apart on 5-minute bars where the noise overwhelms the signal. Or it might work on 1-hour bars but produce too few trades to be statistically meaningful.
Check the trade list, not just the equity curve. The equity curve averages everything into a smooth line. The trade list shows you individual winners and losers. Look for trades where the entry price is suspiciously perfect, or where the win happened because the backtest assumed a fill at the bar's open when in reality the price gapped past your level.
Backtest results on daily bars are especially misleading. The bar's open-to-close range hides the intrabar path, and the backtester picks the most favorable fill sequence. Intraday bars give you a more honest picture of how your converted Pine Script indicator-to-strategy will perform.
From strategy to live trades: alerts, webhooks, and execution
A strategy sitting on a TradingView chart doesn't do anything in the real world. It simulates. To make it trade on a live account, you need three things: an alert, a webhook, and something on the other end to receive it.
Alerts. TradingView's alert system runs server-side. Once you create an alert on your strategy, it fires even if your browser is closed. You don't need a VPS just to keep alerts running.
There are two ways to attach trade data to a strategy alert. The first is alert(), which you call anywhere in your script:
if longCondition
strategy.entry("Long", strategy.long)
alert("BUY EURUSD 0.5", alert.freq_once_per_bar)The second is alert_message, which you pass directly into strategy.entry():
strategy.entry("Long", strategy.long,
alert_message="BUY EURUSD 0.5 SL=1.0820 TP=1.0890")With alert_message, the text fires the moment strategy.entry() is called. This is the method most bridges use. It's also where problems start, which we'll cover in the next section.
Webhooks. A webhook is just an HTTP POST. When your alert fires, TradingView sends the alert message as the body of a POST request to a URL you specify. You configure this in the alert dialog under "Notifications" by checking the "Webhook URL" box and pasting your endpoint.
Webhooks require a TradingView Essential plan or higher. The free plan doesn't support them. This is a hard prerequisite for any form of TradingView automated trading setup. If you're on the free plan, you'll need to upgrade before any of the automation steps work.
The receiving end. Something needs to accept that webhook, parse the payload, and translate it into an order on your broker's platform. You have a few options. You could build your own receiver with a Python script on a VPS. You could convert Pine Script to MQL5 and skip webhooks entirely, running everything natively in MetaTrader. Or you can use a bridge service that handles the translation for you.
Each approach has tradeoffs. Building your own means full control, but ongoing maintenance. Converting to MQL5 means you can't use Pine Script or TradingView's charting tools anymore. A bridge keeps your strategy in Pine Script and handles execution separately.
What breaks between your backtest and live execution
This is where most guides stop. You've converted your indicator, you've backtested it, you've set up alerts and a webhook. The strategy is "live." And then the trades on your broker don't match what TradingView shows.
Three problems account for most of these mismatches.
Ghost positions. The alert_message parameter fires the instant strategy.entry() is called, not when the fill is confirmed. If the market moves before TradingView fills the simulated order, or if the order gets rejected, the webhook has already been sent. Your broker opens a position that doesn't exist on TradingView. On XAUUSD during London open, a 3-second delay between the entry call and the actual fill is enough to create a ghost trade sitting at a price nobody intended.
Signal reordering. Your strategy flips from long to short. Two webhooks fire within milliseconds, "close long" and "open short." But webhooks are HTTP requests. They travel over the internet. They don't arrive in guaranteed order. If "open short" arrives first, the EA opens the short. Both positions sit on the account briefly. When "close long" arrives a moment later, the EA looks for a long to close and finds two positions tagged with its magic number. If its close logic picks the most recently opened one, it closes the freshly opened short. The long stays. You end up holding the position your strategy was trying to exit.
Stop-loss drift. Your strategy calculates a stop at 43,250. The webhook carries the SL as a distance from entry: 50 points. But you fill at 43,302 instead of 43,300. The broker calculates 43,302 minus 50 and places the stop at 43,252. Two points above your intended level. On EURUSD, that's 2 pips, or $20 per lot. Over 200 trades, that's $4,000 in accumulated drift that didn't show up in your backtest.
None of these are strategy problems. The indicator-to-strategy conversion was correct. The conditions fire where they should. The problem is in the pipeline between TradingView and your broker. And if you're running multiple strategies on the same account, there's a fourth problem: webhook alerts from one strategy can interfere with positions opened by another. A close signal meant for your trend system might close a position belonging to your mean-reversion system.
Converting a Pine Script indicator to a strategy for prop firm evaluations
If you're converting an indicator to run on a prop firm account, the stakes on execution problems go up fast.
A typical FTMO challenge gives you a $100,000 account with a 5% maximum daily drawdown ($5,000) and a 10% overall drawdown ($10,000). Fail either limit, and you lose the evaluation fee, usually $300 to $500.
A single ghost position on US30 at 1 lot moving 50 points against you costs $500. That's 10% of your daily drawdown limit, consumed by a trade that shouldn't exist. Two ghost positions in the same session, and you've eaten 20% of your daily allowance before your actual strategy even had a chance to perform.
SL drift compounds the problem. If your strategy places stops at key technical levels (support zones, swing lows), a 2-point shift can mean the difference between a stop that holds and one that triggers prematurely. On a prop account, premature stops don't just cost you the trade. They cost you the evaluation.
Most traders who create trading bots in TradingView for prop evaluations focus entirely on strategy optimization: better entries, tighter risk, and more favorable risk-reward ratios. That work matters. But if your execution pipeline introduces phantom trades or mispriced stops, the strategy edge becomes irrelevant. You're failing evaluations because of infrastructure, not because of logic.
The conversion from indicator to strategy is the first link in this chain. Getting the code right is necessary. But it's not sufficient.
Automating your converted strategy with FillEdge
FillEdge is a webhook bridge built specifically to solve the execution problems described above. After you've converted your indicator into a strategy, FillEdge handles the path from the TradingView alert to the broker order.
Here's how it addresses each failure mode.
Ghost positions. FillEdge doesn't use the standard alert_message approach. The signal pipeline is designed so that a webhook only fires after a fill is confirmed. If TradingView doesn't fill the simulated order, nothing gets sent to your broker. You won't find positions on your account that don't correspond to something on your chart.
The setup involves wrapping your strategy's entry and exit logic in a FillEdge Pine Script template. You don't rewrite your conditions. The template wraps around your existing strategy.entry() and strategy.close() calls and changes how the alert payload is generated.
Signal reordering. When your strategy reverses (close long, open short), FillEdge's server ensures the close processes before the open, regardless of which HTTP request arrives first. This is handled at the server level, not in the EA or Pine Script. You won't end up holding the position your strategy was trying to exit.
SL/TP drift. FillEdge lets you choose per strategy: exact-price mode transmits SL and TP as absolute prices, so a stop at 43,250 stays at 43,250 regardless of fill slippage. Distance mode preserves the offset from actual entry, which ATR-based and volatility-scaled strategies need. Either way, your stops match your logic.
Strategy isolation. If you're running two strategies on the same account (say, an EMA crossover on EURUSD and a breakout system on XAUUSD), FillEdge keeps them in separate lanes. Signals from one strategy never touch positions opened by the other. Most bridges don't isolate strategy signals. FillEdge does.
The setup takes about 15 minutes. You install the FillEdge EA on your broker's terminal, add the Pine Script template to your strategy, and point a TradingView alert at your FillEdge webhook URL. The bridge handles everything between the alert and the order.
A dashboard shows every signal received from TradingView, along with every trade placed with your broker. If something doesn't match, you see it immediately, not three days later when your automated trading strategy has already drifted off course. Every signal carries a status badge: ✓MATCHED means TradingView and your broker agree on the trade, 👻CAUGHT means a ghost was intercepted, 🔀REORDERED means a reversal was sequenced correctly. You don't scan rows looking for mismatches. The badge surfaces them.
FillEdge also monitors the pipeline on its own. Synthetic test signals travel the full path on a regular cadence, without placing a trade. If any leg stops responding, you get an email or Telegram alert within minutes.
One thing FillEdge doesn't fix: your strategy itself. If your indicator-to-strategy conversion introduced a repainting condition, or if your backtest is unrealistically clean because you set commission and slippage to zero, the bridge will faithfully execute bad trades. It matches your chart. If your chart is wrong, the trades will be wrong too.
FAQ
Do I lose my indicator's visual elements when I convert it to a strategy?
No. plot(), plotshape(), bgcolor(), and other drawing functions work inside strategy scripts the same way they do in indicators. You can keep all your visual signals for chart reference while the strategy handles order simulation in the background.
Can I run the indicator and the strategy on the same chart?
TradingView doesn't allow two scripts of the same type in the same pane, but you can run an indicator and a strategy together on one chart. After conversion, some traders keep the original indicator active as a visual cross-check against the strategy's entries. This is useful during the testing phase, though it doubles the computation load on your chart.
What TradingView plan do I need to automate a converted strategy?
You need at least the Essential plan ($14.95/mo). The free plan doesn't support webhooks, which are required to send signals from TradingView to any external service. You also get more alerts on paid plans. The Essential plan gives you 20 active alerts, which is enough for a couple of strategies but tight if you're running multiple symbols.
Does converting an indicator to a strategy change how it calculates signals?
The math stays the same. Your EMAs, RSI values, and crossover conditions produce identical results. What changes is timing: strategies default to calculating on bar close (calc_on_every_tick=false), while your indicator may have been evaluating on every tick. If your entry conditions depend on intrabar price movements, set calc_on_every_tick=true in the strategy() declaration to match the indicator's behavior.
More from FillEdge