Skip to content
GetProfitable
Search

indicator() versus strategy()

Lesson 2 · about 10 min

The declaration statement decides what kind of thing your script is. An indicator calculates and draws. A strategy calculates, draws and also places simulated orders that TradingView's broker emulator fills, producing the Strategy Tester report at the bottom of the chart. A library exports functions for other scripts to import and draws nothing on its own. You will write mostly indicators, convert a few into strategies to test them, and maybe never write a library. Understanding the difference early stops a lot of confusion about why a script "does not trade".

The same idea, both ways

An RSI in its own pane, as an indicator:

//@version=6
indicator("RSI pane", overlay=false)
length = 14
rsi = ta.rsi(close, length)
plot(rsi, "RSI", color=color.purple, linewidth=2)
hline(70, "Upper", color=color.gray)
hline(30, "Lower", color=color.gray)

A moving-average cross that actually takes trades, as a strategy:

//@version=6
strategy("SMA cross demo", overlay=true, initial_capital=10000)
fast = ta.sma(close, 10)
slow = ta.sma(close, 30)
if ta.crossover(fast, slow)
    strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
    strategy.close("Long")
plot(fast, "Fast", color=color.orange)
plot(slow, "Slow", color=color.blue)

Add the second one to a chart and a "Strategy Tester" tab appears with a list of trades, a net profit figure and an equity curve. Add the first one and nothing of the sort happens, however many if statements you write, because an indicator has no strategy.* functions available.

What each declaration controls

Option indicator() strategy()
overlay price pane or own pane same
shorttitle short name on the chart same
initial_capital not available starting equity for the backtest
default_qty_type not available fixed, cash, or percent of equity
commission_type not available percent, cash per order, cash per contract
slippage not available ticks added against you on every fill
pyramiding not available max entries in the same direction
calc_on_every_tick n/a (indicators always update) recalculate on every live tick, not bar close
process_orders_on_close not available fill market orders on the bar close

The strategy options that matter for honest testing (commission, slippage, sizing) get their own lesson in module 6. For now, notice that a strategy defaults to zero commission and zero slippage. A backtest run with the defaults is a backtest of a world where trading is free.

Where the orders go

strategy.entry("Long", strategy.long) does not buy on the bar where the condition is true. It submits a market order, and by default the broker emulator fills it at the open of the next bar. That is the honest choice: on the signal bar you only know the close after the bar has ended, and you could not have traded at it. strategy.close("Long") submits an order to flatten the position named "Long", again filled at the next open. The "Long" string is an ID, not a direction; strategy.long is the direction.

Key idea: Indicators draw, strategies draw and place simulated orders. The declaration line, not the body, decides which one you have, and strategy defaults assume free trading until you tell them otherwise.

Converting between the two

Most working traders keep one script that can be both. Develop as an indicator because it compiles faster and does not clutter the chart with trade markers. When the logic is stable, change the declaration to strategy(...), replace the signal plots with strategy.entry and strategy.close, and read the tester. The signal logic itself does not change, which is exactly the point: the tester is checking the same rules you were eyeballing.

Two practical differences to remember when you convert:

  1. A strategy's overlay=true puts trade arrows on the price pane. If you also want an oscillator, you need a second script or force_overlay tricks; a strategy cannot be in two panes at once.
  2. Indicators can use alertcondition(). Strategies cannot, but they get order-fill alerts instead, which module 5 covers.

Libraries in one paragraph

library("Name") declares a script whose export functions other scripts can import. It cannot plot and is published separately. If you find yourself pasting the same twenty-line helper into every script, that helper belongs in a library. Module 8 shows a small one.

Try it: Paste the strategy above onto a daily chart of any liquid index ETF or futures contract and open the Strategy Tester. Note the net profit. Now add commission_type=strategy.commission.percent, commission_value=0.1 to the declaration and see how much the number moves. That gap is the cost of the defaults.

Recap

  • indicator() calculates and draws; strategy() also submits simulated orders and produces the Strategy Tester report.
  • Strategy-only options include initial capital, quantity type, commission, slippage and pyramiding; the defaults model free trading.
  • strategy.entry and strategy.close fill at the next bar's open by default.
  • Develop as an indicator, convert to a strategy to test, and keep the signal logic identical.
  • library() packages reusable functions and draws nothing.

See it drawn

Original diagrams for the ideas on this page. Illustrative, not real market data.

Slippage on a market orderA buy order clears four price levels, so the average price paid is worse than the price first quoted.Buy 1,000 shares at marketpricesell orders resting (bar length = size)20.04300 shares20.03200 shares20.01200 shares20.00300 sharesnothing resting at 20.02order sweeps up the bookaverage fill 20.02SLIPPAGE0.02 a share$20.00 in totalintended 20.00Each level fills at its own price; the average is what you really paid.
Slippage on a market order. You click at 20.00, but only 300 shares are resting there, so the rest of the order fills at 20.01, 20.03 and 20.04. The average price paid is 20.02, and that two-cent gap is slippage.