◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / How to Convert a TradingView Indicator into a Strategy
Pine Script

How to Convert a TradingView Indicator into a Strategy

A practical guide to converting an indicator() into a strategy() in Pine Script v6: what each script type actually does, how to replace visual plotshape markers with strategy.entry and strategy.exit, and why any repaint hiding in your original indicator carries straight into the backtest. This is educational material about Pine Script mechanics, not financial advice, and nothing here implies any approach is profitable; trading carries a real risk of loss.

Indicator vs Strategy: Two Different Jobs

The first thing to understand is that indicator() and strategy() are not interchangeable wrappers around the same code. They declare two different kinds of script with two different runtimes, and TradingView treats them differently the moment you change the first line.

An indicator() script is a visualization tool. It computes values and draws them, plotting lines, filling bands, or stamping markers on the chart with calls like plot() and plotshape(). It has no concept of a position, a trade, or money. When your indicator paints an up-arrow, that arrow is a picture and nothing more, the script does not know whether you are long, flat, or short.

A strategy() script is a backtesting and order-simulation engine. In addition to everything an indicator can draw, it maintains a simulated account, tracks an open position, fills orders against historical bars, and produces the Strategy Tester report with its equity curve and trade list. The functions strategy.entry(), strategy.exit(), and strategy.close() only exist in this context. So converting an indicator into a strategy is fundamentally the task of taking the conditions that used to draw a marker and instead wiring them into these order functions, so a visual hint becomes a simulated trade.

This distinction matters for everything that follows. Your indicator's logic, the moving averages, the crossovers, the thresholds, can usually be reused almost verbatim. What changes is the output layer: markers stop being decoration and start being instructions.

The Before: A Clean v6 Indicator

Let's start with a deliberately simple, correct Pine v6 indicator so the conversion is easy to follow. It marks where a fast EMA crosses a slow EMA, the classic two-line crossover, using plotshape() to stamp the chart. This is purely an illustration of the mechanics; it is not a setup that anyone should expect to be profitable.

Notice the small but important details: the script starts with //@version=6, every built-in is namespaced (ta.ema, not a bare ema()), and the cross conditions are gated on barstate.isconfirmed before any alert fires. That gate is what keeps the marker honest on the live bar, it won't flicker into existence mid-bar and then vanish once the bar closes.

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

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

fastEma = ta.ema(close, fastLen)
slowEma = ta.ema(close, slowLen)

plot(fastEma, "Fast EMA", color.aqua)
plot(slowEma, "Slow EMA", color.orange)

longCross  = ta.crossover(fastEma, slowEma)
shortCross = ta.crossunder(fastEma, slowEma)

plotshape(longCross,  "Long",  shape.triangleup,   location.belowbar, color.green, size = size.small)
plotshape(shortCross, "Short", shape.triangledown, location.abovebar, color.red,   size = size.small)

// Alerts only on a closed bar so the marker can't repaint intrabar
if longCross and barstate.isconfirmed
    alert("EMA cross up",   alert.freq_once_per_bar_close)
if shortCross and barstate.isconfirmed
    alert("EMA cross down", alert.freq_once_per_bar_close)

This is the kind of indicator many traders keep on their charts: it shows where a crossover happened, but it never simulates acting on it. Everything we need for a strategy, the entry and exit conditions, is already here in longCross and shortCross. The conversion is mostly about changing what we do with them.

The After: The Same Logic as a Strategy

Now the conversion. The mechanical steps are smaller than people expect:

  1. Change the declaration. Swap indicator(...) for strategy(...). The strategy() call takes account-related parameters like initial_capital, default_qty_type, and commission_type that have no meaning in an indicator.
  2. Replace the visual outputs with orders. Each plotshape() that represented a condition to act on becomes a strategy.entry() or strategy.exit() / strategy.close() call.
  3. Decide how positions interact. In a simple always-in-market reversal system, a long entry automatically closes any open short because strategy.entry() reverses the position by default. If you want flat periods instead, you manage exits explicitly.

Here is the EMA-cross indicator above, rebuilt as a strategy. The plotting of the EMAs stays (a strategy can still draw), but the triangle markers are gone, their job now belongs to the order calls.

//@version=6
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.04)

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

fastEma = ta.ema(close, fastLen)
slowEma = ta.ema(close, slowLen)

plot(fastEma, "Fast EMA", color.aqua)
plot(slowEma, "Slow EMA", color.orange)

longCross  = ta.crossover(fastEma, slowEma)
shortCross = ta.crossunder(fastEma, slowEma)

// Conditions become orders. entry() reverses an opposite position by default.
if longCross
    strategy.entry("Long", strategy.long)
if shortCross
    strategy.entry("Short", strategy.short)

That is a complete, compilable v6 strategy. Note what we did not do: we did not add when= to the strategy.entry calls (that v5-era argument is gone, gate with an if instead), and we did not gate the entries on barstate.isconfirmed. We'll come back to that second point, because whether you need it depends on a subtle behavior of the backtester.

If you prefer flat periods between trades rather than a constant reversal, replace one side with an exit. For example, go long on the cross up and simply close on the cross down: use strategy.close("Long") inside the shortCross block instead of opening a short. For protective stops and targets, attach a strategy.exit("Exit", from_entry = "Long", stop = ..., limit = ...) keyed to the entry's ID.

The Trap: Repaint Travels With the Logic

Here is the warning that catches almost everyone who tries to convert indicator to strategy in Pine Script: any repainting in your original indicator does not get fixed by the conversion, it gets baked into a backtest that now looks like real trades. An innocent-looking marker becomes a fictional fill, and the Strategy Tester will report it with total confidence.

The most common source is multi-timeframe data. If your indicator pulls a higher-timeframe value with request.security() and does not offset it, the current, still-forming higher-timeframe bar can leak into history during a backtest but updates live in real time. The values you backtested against were never actually available at that moment. The fix is the same in a strategy as in an indicator: request the series offset by [1] and explicitly disable look-ahead.

//@version=6
strategy("HTF-Gated Cross", overlay = true)

htfClose = request.security(syminfo.tickerid, "D", close[1],
     lookahead = barmerge.lookahead_off)

fastEma = ta.ema(close, 9)
slowEma = ta.ema(close, 21)

// Only take longs while the prior daily close is above the slow EMA
bullRegime = htfClose > slowEma

if ta.crossover(fastEma, slowEma) and bullRegime
    strategy.entry("Long", strategy.long)
if ta.crossunder(fastEma, slowEma)
    strategy.close("Long")

The close[1] with lookahead = barmerge.lookahead_off is the canonical non-repainting request: it asks for the confirmed higher-timeframe value, never the in-progress one. Get this wrong and your equity curve can look pristine in the tester and behave very differently live, not because the idea was sound or unsound, but because the backtest was quietly using information that would not have been available in real time.

A second, more subtle issue shows up at the order level. By default a strategy calculates only on closed bars (calc_on_every_tick = false), and an order placed when a condition is true on bar N fills at the open of bar N+1. That default is reasonably safe. But if you flip on calc_on_every_tick (or your condition references intrabar values), an order can be evaluated against a live, unconfirmed bar, and a condition that is true mid-bar may no longer be true once the bar closes. If your indicator's marker was only ever meant to be valid on a closed bar, mirror that intent by gating the order, for example if longCross and barstate.isconfirmed. This keeps the strategy's fills tied to the same confirmed conditions your indicator's markers represented, so the backtest and the live chart tell the same story. Tooling such as the ForexCodes Audit checker can help flag these look-ahead and repaint patterns, but the discipline of reading every request.security and every gate yourself is what really protects you.

A Conversion Checklist You Can Reuse

Once you've done a few of these, the process becomes a short checklist. Run through it every time you convert an indicator to a strategy in Pine Script and you'll avoid the usual surprises.

  • Line one: //@version=6, then indicator(...) becomes strategy(...). Add initial_capital, default_qty_type, and a realistic commission_type / commission_value so the backtest isn't fantasy-cheap.
  • Markers to orders: every plotshape() / plot() that meant "a condition to enter on fired here" becomes strategy.entry(); every "exit here" becomes strategy.close() or strategy.exit(). Keep the underlying ta.* and math.* logic unchanged, that is the part you carry over directly.
  • Position model: decide up front whether the system is always-in-market (reversing) or returns to flat. strategy.entry() reverses by default; use strategy.close() / strategy.exit() for flat periods and protective stops.
  • No v5 leftovers: remove any when= arguments from order calls, make sure there is no bare security() (it must be request.security), and confirm there is no stray study() or transp=.
  • Hunt the repaint: audit every request.security() for the [1] offset and lookahead = barmerge.lookahead_off. Gate close-only conditions with barstate.isconfirmed. Compare the strategy's markers on historical bars to a fresh live session, if a trade location shifts after the bar closes, you have a repaint.

The deeper point is one of mindset. An indicator is allowed to be a little loose, it is a hint for a human who still makes the decision. A strategy removes the human from the loop and trusts the code literally, which means every approximation and every look-ahead becomes a measurable, compounding error in the backtest. Converting the syntax takes ten minutes; understanding what the resulting equity curve does and does not tell you takes the careful work above.

Finally, a necessary reminder: this article is educational and explains Pine Script mechanics only. It is not financial advice, and nothing here, the EMA cross included, implies any setup is profitable or has any particular win rate. A clean, non-repainting backtest tells you what would have happened on past data under your assumptions; it is not a prediction. Trading involves a genuine risk of loss, so treat any strategy you build as a hypothesis to be tested skeptically, not a promise.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro