◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / strategy() vs indicator() in Pine Script: What Actually Changes
Pine Script

strategy() vs indicator() in Pine Script: What Actually Changes

A precise look at what actually changes when a Pine Script v6 script declares strategy() instead of indicator(): the broker emulator, order fills on the next bar's open, intrabar price-path assumptions, and the extra recalculation modes (calc_on_every_tick, calc_on_order_fills) that only exist for strategies. The same logic can plot identically in both forms yet signal on different bars — this guide explains exactly why. Educational material only, not financial advice; nothing here makes a strategy profitable, and backtest results are never a forecast of live performance.

What is the difference between a strategy and an indicator in Pine Script?

An indicator (declared with indicator()) computes and plots values bar by bar — it can draw lines, shapes, and fire alerts, but it cannot place orders. A strategy (declared with strategy()) does everything an indicator does plus it can call strategy.entry(), strategy.exit(), and strategy.close(), and TradingView runs those calls through a broker emulator that simulates fills, tracks a position, and reports results in the Strategy Tester. That emulator — not the declaration keyword — is where all the meaningful behavioural differences live.

Both script types share the same core execution model: the script runs once per historical bar, left to right, and then once per update on the realtime bar. Your ta.sma(), ta.rsi(), and crossover conditions evaluate to exactly the same series in both forms. This is why a strategy and an indicator built from the same logic will usually plot identically — the calculations have not changed at all.

What changes is what happens after a condition becomes true. In an indicator, a true condition is just a value you plot or alert on. In a strategy, a true condition typically places an order, and that order enters the emulator's world: it is filled at a simulated price on a subsequent tick or bar, it changes strategy.position_size, and — under some settings — its fill triggers an extra recalculation of the entire script. Those downstream effects can shift when signals effectively act by a full bar, which is why the naive mental model of "a strategy is just an indicator with orders bolted on" breaks down exactly when you start comparing signal timing. The rest of this article works through each mechanism in turn.

Why can the same logic plot identically but signal differently?

The short answer: plots reflect when a condition becomes true; trades reflect when the emulator fills the resulting order — and by default those are different bars.

On historical bars, a strategy calculates once, at the bar's close. If your entry condition is true on bar N, strategy.entry() registers a market order at the close of bar N, and the broker emulator fills it at the open of bar N + 1. That is the default, and it is deliberate: at the close of bar N the close price is already known, so pretending you were filled at it would leak information. An indicator plotting the same condition marks bar N; the strategy's trade arrow appears at the open of bar N + 1. Same logic, one-bar difference — and on a higher timeframe, one bar can be a day.

Second, an indicator's alert and a strategy's order can diverge on the realtime bar. An indicator condition gated with barstate.isconfirmed fires once, at bar close. An ungated strategy running with default settings evaluates on the realtime bar's updates and may see a condition flicker true intrabar, then false by the close — the emulator's behaviour then depends on the calculation settings covered below.

Third, strategies carry state that indicators do not. strategy.position_size, strategy.position_avg_price, and pyramiding limits all feed back into your conditions. A signal that an indicator would happily mark can be silently ignored by a strategy because a position is already open, or because pyramiding = 0 (the default) blocks the additional entry. When authors report "the strategy skips signals my indicator shows," this feedback loop — not a Pine bug — is almost always the cause. Comparing the two script types fairly means comparing condition series, not arrows on a chart.

Inside the broker emulator: fills and the intrabar price-path assumption

The broker emulator only sees the data your chart sees: OHLC per bar (unless you enable bar-magnifier features on paid plans). To fill stop and limit orders inside a bar, it must assume a path price took between open and close. The documented assumption is: if the bar's high is closer to its open than the low is, the emulator assumes the path open → high → low → close; otherwise open → low → high → close. Intrabar, price is also assumed to move in mintick increments along that path.

This assumption is invisible when you only trade market orders on bar boundaries, but it directly decides ambiguous cases. Suppose a long position has both a stop-loss and a take-profit inside the same bar's range. Which fills first? On real tick data, either could. In the emulator, the assumed path decides deterministically — and it can systematically flatter or punish your exits relative to what live ticks would have done. A backtest where many stop/target pairs resolve inside single bars is leaning hard on this assumption, and that is worth knowing before trusting the numbers.

Fills themselves are idealised in other ways too. By default there is no slippage and no spread — market orders fill exactly at the next bar's open, stops exactly at the stop price (unless the bar gaps through it, in which case the emulator fills at the open beyond it, which is realistic). The slippage and commission parameters of strategy() exist precisely so you can stop pretending execution is free; using them does not make a backtest predictive, but leaving them at zero makes it optimistic by construction.

None of this machinery exists in an indicator. That is the honest core of the strategy-versus-indicator difference: an indicator shows you a condition; a strategy shows you one simulated interpretation of trading it.

calc_on_every_tick, calc_on_order_fills, and process_orders_on_close

Three strategy() parameters change when the script recalculates or orders execute, and each one widens the gap between backtest and live behaviour in a specific way.

`calc_on_every_tick = true` makes the strategy recalculate on every realtime update instead of only at bar close. The trap is asymmetry: historical bars still calculate once per bar, because there are no ticks in historical data. So the strategy literally runs under different rules in the backtest portion of the chart than in the live portion — a structural source of "works in backtest, fails live" that no amount of parameter tuning fixes. If your logic genuinely needs intrabar granularity, you must accept that the historical results cannot reflect it.

`calc_on_order_fills = true` adds an extra recalculation immediately after each order fill. On historical bars this gives the script up to four calculations per bar (at the fill points implied by OHLC), letting it react to a fill — for example, placing an exit based on the actual strategy.position_avg_price — within the same bar. The cost, again, is divergence: those extra intrabar calculations use price values (like the current close) at moments that live execution would see differently.

`process_orders_on_close = true` shifts market-order execution from the next bar's open to the current bar's close. It makes trade arrows line up with signal bars — cosmetically satisfying — but a real order sent "on close" actually executes at or after the close print, so the fill price the tester assumes is a best case.

A sober default for validation work: leave all three at their defaults, gate signals on barstate.isconfirmed, and change these settings only when you can articulate exactly which live-execution detail you are modelling — and which one you are idealising away.

Converting an indicator into a strategy without changing its meaning

Here is the same SMA-cross logic in both forms. First the indicator, which plots and alerts but places no orders:

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("SMA cross signals", overlay = true)

fast = ta.sma(close, 10)
slow = ta.sma(close, 30)
longSig = ta.crossover(fast, slow)

plot(fast, "Fast SMA", color = color.teal)
plot(slow, "Slow SMA", color = color.orange)
plotshape(longSig and barstate.isconfirmed, "Long signal",
     shape.triangleup, location.belowbar, color.green)

Now the strategy. The condition series is untouched; what is added is order placement, position awareness, and a stop input that is actually wired into the exit:

//@version=6
// Educational only — validate before trading; not financial advice.
strategy("SMA cross strategy", overlay = true)

stopPts = input.int(300, "Stop distance (points)", minval = 1)

fast = ta.sma(close, 10)
slow = ta.sma(close, 30)
longSig = ta.crossover(fast, slow)

plot(fast, "Fast SMA", color = color.teal)
plot(slow, "Slow SMA", color = color.orange)

if longSig and barstate.isconfirmed and strategy.position_size == 0
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0
    strategy.exit("Exit", "Long",
         stop = strategy.position_avg_price - stopPts * syminfo.mintick)

Three deliberate choices keep the conversion honest. The signal is gated on barstate.isconfirmed, so the strategy acts only on closed bars — matching what the indicator's confirmed plot shows and what historical bars already do. The entry checks strategy.position_size == 0, making the position-feedback behaviour explicit instead of implicit. And the declared stop input feeds strategy.exit(stop = ...) — a settings panel that promises risk control the code never applies is exactly the kind of silent defect a validator, or ForexCodes' Audit tooling, exists to catch.

Expect the trade arrows to sit one bar after the indicator's triangles. That offset is the broker emulator being realistic, not a bug to be patched with process_orders_on_close.

Which should you use — and what neither one tells you

Use indicator() when the script's job is to show something: derived series, levels, regime filters, alert conditions feeding an external system. Indicators are lighter, they sidestep the emulator's assumptions entirely, and an alert gated on bar close is a clean, well-defined event. Use strategy() when you specifically want the Strategy Tester's accounting — entries, exits, position sizing, and the trade list — because that is the only thing strategy() adds.

A useful discipline is to develop the logic as an indicator first, confirm the condition series behaves sanely (no repainting inputs, no na leaks, signals only on confirmed bars), and then port it to a strategy knowing that any new discrepancy you see must come from the emulator layer: next-bar fills, position feedback, or a calculation setting. That separation turns "why did my strategy do that?" from a mystery into a short checklist.

Be equally clear about what neither script type gives you. The Strategy Tester is a historical accounting device under stated assumptions — idealised fills, an assumed intrabar path, zero slippage unless you add it. It is not a forecast, and a green equity curve is compatible with a strategy that merely fit past noise. Overfitting, data-snooping across parameter sets, and survivorship in symbol choice all live upstream of anything strategy() can detect; they require validation practices (out-of-sample testing, walk-forward analysis, honest accounting for how many variants you tried) that no declaration statement provides.

So: strategy() versus indicator() changes execution semantics, not truth. Get the mechanics right — confirmed-bar signals, wired-up stops, defaults you can defend — and then treat the tester's output as evidence to be interrogated, not a result to be believed. None of this is financial advice, and trading carries a real risk of loss.

Key takeaways

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

Catch the bug that compiles.Run auditGet Pro