How to Convert a Pine Script Indicator to a Strategy for Backtesting

A practical, copy-paste guide to turn any TradingView Pine Script indicator() into a fully backtestable strategy() — with entries, exits, risk management and the checks that prevent repainted, unrealistic results.

Why convert an indicator into a strategy?

An indicator() can plot signals but cannot open, close or size positions, and it does not unlock the TradingView Strategy Tester. Converting it to strategy() lets you measure Net Profit, Max Drawdown, Profit Factor and Win Rate against historical data — the only honest way to know if a setup has an edge before risking capital.

Step 1 — Start from a clean indicator

Here is a minimal EMA-cross indicator. It plots two moving averages and marks crossovers, but it cannot place orders:

//@version=5
indicator("EMA Cross", overlay=true)

fastLen = input.int(9,  "Fast EMA")
slowLen = input.int(21, "Slow EMA")

fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)

plot(fast, color=color.orange)
plot(slow, color=color.blue)

longCond  = ta.crossover(fast,  slow)
shortCond = ta.crossunder(fast, slow)

plotshape(longCond,  style=shape.triangleup,   location=location.belowbar, color=color.green)
plotshape(shortCond, style=shape.triangledown, location=location.abovebar, color=color.red)

Step 2 — Swap indicator() for strategy()

Replace the declaration and add the four settings that make backtests realistic: initial_capital, default_qty_type, commission_value and slippage. Enable process_orders_on_close=true so fills happen on the close of the signal bar, not the open of the next one.

Step 3 — Add entries, exits and risk management

Signal logic stays identical — the difference is calling strategy.entry instead of only plotting shapes, and wrapping each entry with a strategy.exit stop/target defined in ATR units so every trade has a comparable R multiple:

//@version=5
strategy("EMA Cross Strategy",
     overlay              = true,
     initial_capital      = 10000,
     default_qty_type     = strategy.percent_of_equity,
     default_qty_value    = 100,
     commission_type      = strategy.commission.percent,
     commission_value     = 0.05,
     slippage             = 2,
     process_orders_on_close = true)

fastLen = input.int(9,  "Fast EMA")
slowLen = input.int(21, "Slow EMA")

fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)

plot(fast, color=color.orange)
plot(slow, color=color.blue)

longCond  = ta.crossover(fast,  slow)
shortCond = ta.crossunder(fast, slow)

// --- Entries ---
if longCond
    strategy.entry("Long",  strategy.long)
if shortCond
    strategy.entry("Short", strategy.short)

// --- Risk management: ATR stop + 2R target ---
atr    = ta.atr(14)
stop   = 1.5 * atr
target = 3.0 * atr

strategy.exit("X-Long",  from_entry="Long",
     stop   = strategy.position_avg_price - stop,
     limit  = strategy.position_avg_price + target)
strategy.exit("X-Short", from_entry="Short",
     stop   = strategy.position_avg_price + stop,
     limit  = strategy.position_avg_price - target)

Step 4 — Kill repainting before you trust the results

  • Never set calc_on_every_tick=true for backtests — it inflates win rates.
  • Avoid lookahead_on and pass lookahead=barmerge.lookahead_off to any request.security call.
  • Gate signals on barstate.isconfirmed if you also run the strategy live.
  • Verify on the TradingView Bar Replay tool that signals do not shift after a bar closes.

Step 5 — Read the Strategy Tester honestly

Focus on Profit Factor (>1.5 is healthy), Max Drawdown vs Net Profit, and the number of closed trades (at least 100 for statistical relevance). A curve that only works on one symbol or one timeframe is overfit — re-run the tester on 2–3 correlated symbols before drawing conclusions.

FAQ

What is the difference between indicator() and strategy() in Pine Script?

indicator() only draws values on the chart and cannot place orders. strategy() unlocks strategy.entry, strategy.exit, strategy.close and the Strategy Tester tab, letting you simulate positions, P&L, drawdown and win rate against historical data.

Can I keep the same signals when converting from indicator to strategy?

Yes. The signal logic (crossovers, thresholds, filters) is identical. You only replace plotshape/alert lines with strategy.entry / strategy.exit calls and add position sizing plus risk management.

Why do my backtest results look too good?

The two most common culprits are repainting (using future data via lookahead_on or request.security without barmerge) and unrealistic fills. Set process_orders_on_close=true, add commission and slippage, and never use calc_on_every_tick=true for backtests.

How do I avoid repainting when converting an indicator?

Only act on confirmed bars (barstate.isconfirmed), avoid lookahead_on, and if you use request.security pass lookahead=barmerge.lookahead_off. Test the converted strategy on a replay to confirm signals do not shift.

Do I need to rewrite input variables?

No. input.int, input.float, input.bool and input.source all work identically inside strategy(). Just keep the same variable names so the strategy parameters match the original indicator.

Skip the manual conversion

Strategy Decoder extracts rules from trading videos and articles and outputs ready-to-paste Pine Script strategy() code — with entries, exits and risk already wired. Explore extracted strategies.