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:
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:
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:
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 completestrategy.*namespace for order placement, position tracking, and performance metrics. - ⚙️ The namespace exclusivity rule is enforced at compile time — any
strategy.*call inside anindicator()script produces an immediate compile error, making the boundary between the two types a hard architectural constraint, not a convention.
Ideas for Script Advancement
- Hybrid Alert Architecture: Build the signal logic once inside an
indicator()script withalertcondition()calls, then mirror the identical logic inside astrategy()script for backtesting. This separation of concerns keeps each script focused on its single responsibility. - Dynamic Position Sizing in strategy(): Replace
default_qty_valuewith 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
Post a Comment