Pine Script v6: indicator() vs strategy() — Core Functional Differences Explained


In Pine Script v6, every script must declare its type using either indicator() or strategy() as its first executable statement. Understanding the precise functional boundary between these two declarations is essential for building reliable tools — one is designed purely for visual analysis, while the other drives a full backtesting and order-execution engine.

1. Architectural Overview

At the compiler level, indicator() and strategy() are mutually exclusive script-type declarations. They share the same bar-by-bar execution model but expose entirely different namespaces and capabilities. The table below summarizes the top-level differences:

Feature indicator() strategy()
Primary Purpose Visual overlays & signal drawing Backtesting & order simulation
Order Functions ❌ Not available strategy.entry(), strategy.exit(), strategy.close()
Strategy Tester Panel ❌ Not available ✅ Automatically enabled
plot(), hline(), shapes ✅ Full support ✅ Full support
Overlay on Price Chart Controlled by overlay param Controlled by overlay param
Capital & Commission Settings ❌ Not applicable initial_capital, commission_type, commission_value
Lookahead / Recalculation max_bars_back only calc_on_every_tick, calc_on_order_fills

2. The indicator() Declaration

The indicator() function registers the script as a study. It renders computed values as visual elements — lines, histograms, shapes, labels — but has zero access to order-management namespaces. Any call to strategy.entry() inside an indicator() script will produce a compile-time error.

Key Parameters of indicator()

Parameter Type Description
title string Display name in the indicator panel
overlay bool If true, renders on the main price pane
max_bars_back int Maximum historical bars accessible via [] operator
precision int Number of decimal places for displayed values
scale scale.* Defines the price scale used (scale.right, scale.left, scale.none)
🔽 [Click to expand] View Full Pine Script Code — indicator() Example
//@version=6
// Declare this script as an indicator (study)
// overlay=true places it directly on the candlestick chart
indicator(title="EMA Crossover Signals", overlay=true, max_bars_back=500)

// --- Inputs ---
fastLen = input.int(9,  title="Fast EMA Length", minval=1)
slowLen = input.int(21, title="Slow EMA Length", minval=1)

// --- Calculations ---
// ta.ema() computes the Exponential Moving Average
fastEMA = ta.ema(close, fastLen)
slowEMA = ta.ema(close, slowLen)

// --- Crossover Detection ---
// ta.crossover returns true on the exact bar where fast crosses above slow
bullCross = ta.crossover(fastEMA, slowEMA)
// ta.crossunder returns true on the exact bar where fast crosses below slow
bearCross = ta.crossunder(fastEMA, slowEMA)

// --- Visuals ---
// Plot both EMAs as lines on the price chart
plot(fastEMA, title="Fast EMA", color=color.new(color.blue, 0),  linewidth=2)
plot(slowEMA, title="Slow EMA", color=color.new(color.orange, 0), linewidth=2)

// Draw triangle shapes at crossover points
plotshape(bullCross, title="Bull Cross", style=shape.triangleup,
          location=location.belowbar, color=color.new(color.green, 0), size=size.small)
plotshape(bearCross, title="Bear Cross", style=shape.triangledown,
          location=location.abovebar, color=color.new(color.red, 0),   size=size.small)

// NOTE: strategy.entry() or any strategy.* call here would cause a compile error.

3. The strategy() Declaration

The strategy() function registers the script as a backtesting engine. It unlocks the strategy.* namespace, which includes order placement, position tracking, and performance reporting. The Strategy Tester panel is automatically activated in the TradingView UI whenever a strategy() script is compiled successfully.

Key Parameters of strategy()

Parameter Type Description
title string Display name in the Strategy Tester
overlay bool If true, renders on the main price pane
initial_capital float Starting capital for the backtest simulation
commission_type strategy.commission.* Commission calculation method (e.g., strategy.commission.percent)
commission_value float Commission amount per trade
default_qty_type strategy.fixed / strategy.percent_of_equity / strategy.cash Defines how position size is calculated
default_qty_value float Quantity value corresponding to default_qty_type
calc_on_every_tick bool If true, recalculates on every real-time tick (not just bar close)
calc_on_order_fills bool If true, recalculates immediately after an order is filled
🔽 [Click to expand] View Full Pine Script Code — strategy() Example
//@version=6
// Declare this script as a strategy (backtesting engine)
// initial_capital: starting equity for simulation
// commission_type/value: 0.05% per trade to model realistic costs
// default_qty_type: size each trade as a percentage of current equity
strategy(
  title             = "EMA Crossover Strategy",
  overlay           = true,
  initial_capital   = 10000,
  commission_type   = strategy.commission.percent,
  commission_value  = 0.05,
  default_qty_type  = strategy.percent_of_equity,
  default_qty_value = 10
)

// --- Inputs ---
fastLen = input.int(9,  title="Fast EMA Length", minval=1)
slowLen = input.int(21, title="Slow EMA Length", minval=1)

// --- Calculations ---
fastEMA = ta.ema(close, fastLen)
slowEMA = ta.ema(close, slowLen)

// --- Signal Detection ---
bullCross = ta.crossover(fastEMA, slowEMA)   // Fast crosses above slow → long signal
bearCross = ta.crossunder(fastEMA, slowEMA)  // Fast crosses below slow → exit/short signal

// --- Order Execution ---
// strategy.entry() places a simulated market order
// "Long" is the trade ID; direction=strategy.long opens a long position
if bullCross
    strategy.entry(id="Long", direction=strategy.long)

// strategy.close() closes the open position identified by its trade ID
if bearCross
    strategy.close(id="Long")

// --- Visuals (available in both indicator and strategy) ---
plot(fastEMA, title="Fast EMA", color=color.new(color.blue, 0),   linewidth=2)
plot(slowEMA, title="Slow EMA", color=color.new(color.orange, 0), linewidth=2)

// strategy.position_size is only accessible inside a strategy() script
// It returns the current open position size (positive=long, negative=short, 0=flat)
bgColor = strategy.position_size > 0 ? color.new(color.green, 90) :
          strategy.position_size < 0 ? color.new(color.red,   90) :
          na
bgcolor(bgColor, title="Position Background")

4. Execution Model: How Both Scripts Process Bars

Both indicator() and strategy() share the same bar-by-bar sequential execution model. The script is evaluated once per closed historical bar, and once per tick on the real-time bar. The critical difference is what happens after the evaluation on each bar:

graph TD A["New Bar / Tick Arrives"] --> B["Script Executes Bar-by-Bar"] B --> C{"Script Type?"} C -->|"indicator()"| D["Compute Values"] D --> E["Render Visuals (plot, shape, label)"] E --> F["Wait for Next Bar/Tick"] C -->|"strategy()"| G["Compute Values"] G --> H["Render Visuals (plot, shape, label)"] H --> I["Evaluate Order Conditions"] I --> J{"Order Triggered?"} J -->|"Yes"| K["Simulate Order Fill Update Equity & Position"] J -->|"No"| L["No Trade Action"] K --> F L --> F

For strategy(), the parameter calc_on_every_tick controls whether the script re-evaluates on every incoming tick during the real-time bar. When set to true, orders can be triggered intrabar. When false (the default behavior), orders are only processed at bar close. This distinction has a direct impact on backtest realism and potential look-ahead bias.

5. Namespace Exclusivity — What Each Script Can Access

The most critical architectural rule is namespace exclusivity. The strategy.* namespace — including all order, position, and performance variables — is only compiled and accessible inside a strategy() script. Attempting to reference strategy.position_size or call strategy.entry() inside an indicator() script will produce a compile-time error.

Namespace / Function indicator() strategy()
plot(), plotshape(), hline()
label.new(), line.new(), box.new()
ta.* (indicators)
request.security()
strategy.entry() ❌ Compile error
strategy.exit() ❌ Compile error
strategy.close() ❌ Compile error
strategy.position_size ❌ Compile error
strategy.netprofit ❌ Compile error

6. Mathematical Relationship: Position Sizing

One of the most important quantitative aspects of strategy() is its position sizing model. When default_qty_type = strategy.percent_of_equity, the number of units purchased on each trade is computed as:

$$\text{Units} = \frac{\text{Equity} \times \frac{\text{qty\_value}}{100}}{\text{Entry Price}}$$

For example, with Equity = $10,000, qty_value = 10, and Entry Price = $250:

$$\text{Units} = \frac{10{,}000 \times 0.10}{250} = \frac{1{,}000}{250} = 4 \text{ units}$$

This formula is applied on every strategy.entry() call, using the current equity at the time of entry, which means position sizes compound (or decay) with the portfolio's performance over time. The indicator() script has no equivalent concept — it performs no capital arithmetic whatsoever.

7. Common Architectural Mistake: Mixing Concerns

A frequent error among Pine Script developers is attempting to build a single script that both draws signals visually and executes backtested orders using indicator(). The correct architecture is to use strategy() for backtesting (which also supports all visual functions), and reserve indicator() for pure signal/overlay tools that will be used in live chart analysis or alerts. The diagram below illustrates the correct decision path:

graph TD Start(["New Pine Script Project"]) --> Q1{"Do you need backtesting or order simulation?"} Q1 -->|"Yes"| S1["Use strategy()"] Q1 -->|"No"| S2["Use indicator()"] S1 --> S1A["Access strategy.entry() strategy.exit() strategy.close()"] S1 --> S1B["Access strategy.position_size strategy.netprofit Strategy Tester Panel"] S1 --> S1C["Also supports all plot() / label / line visuals"] S2 --> S2A["Access plot() plotshape() hline() / label / line"] S2 --> S2B["Use alertcondition() for live signal alerts"] S2 --> S2C["strategy.* namespace NOT available — compile error"] style S1 fill:#ff9900,color:#000000 style S2 fill:#0066cc,color:#ffffff style S2C fill:#ff4444,color:#ffffff

8. Conclusion

  • 🔵 indicator() is a read-only visual engine: it computes values and renders them as chart objects, but has zero access to order management, capital simulation, or the strategy.* namespace.
  • 🟠 strategy() is a full backtesting engine: it inherits all visual capabilities of indicator() while additionally exposing the complete strategy.* namespace for order placement, position tracking, and performance metrics.
  • ⚙️ The namespace exclusivity rule is enforced at compile time — any strategy.* call inside an indicator() script produces an immediate compile error, making the boundary between the two types a hard architectural constraint, not a convention.

Ideas for Script Advancement

  1. Hybrid Alert Architecture: Build the signal logic once inside an indicator() script with alertcondition() calls, then mirror the identical logic inside a strategy() script for backtesting. This separation of concerns keeps each script focused on its single responsibility.
  2. Dynamic Position Sizing in strategy(): Replace default_qty_value with a volatility-adjusted quantity computed from ATR — for example, sizing each trade so that a 1-ATR move equals a fixed dollar risk — to build a more mathematically rigorous backtesting framework.

Comments

Popular posts from this blog

Pine Script Variable Declaration: Mastering var, varip, and Proper Initialization Across Historical Bars

Pine Script v6 plot() Function: Complete Guide to Lines, Histograms, and Circles