One line decides what your script can do
Every Pine script declares itself as one of a few kinds, and the declaration is the second line of the file [2]. Two of those kinds matter here:
//@version=6
indicator("My indicator") // draws on the chart
//@version=6
strategy("My strategy") // draws, and also trades
The usual explanation is "strategies can be backtested". That is true and it misses the mechanism, which is more useful to know.
strategy() switches on the broker emulator
strategy() (opens in a new tab) does not add a backtest button. It attaches a simulated broker
to your script. Everything people associate with a strategy exists because that
emulator is running:
| What you get | Because |
|---|---|
| the Strategy Tester panel | there is now a simulated account to report on |
strategy.entry(), strategy.exit(), strategy.close() | there is something to send orders to |
| an equity curve and a drawdown figure | the emulator tracks a balance |
| the List of Trades | the emulator records its own fills |
position state and strategy.closedtrades | the emulator holds positions |
That is why you cannot bolt a backtest onto an indicator. An indicator() (opens in a new tab)
script has no account, no orders and no position — not because the feature was
withheld, but because there is no broker on the other end.
The emulator is a model, and it is worth knowing its assumptions
Because your results come from a simulation, the simulation's rules are part of your results. The two most consequential are documented:
- On a bar whose range contains both your stop and your target, the emulator infers which came first from where the open sits relative to the high and the low [1]. It is a deterministic rule, not a measurement — the detail is in risk management.
- If price crosses an order's level inside a gap between bars, the emulator assumes intrabar data does not exist in the gap [1]. So a stop inside a weekend gap does not fill at your stop price.
Neither is a flaw. They are what a simulator has to do when it only has bars. But they mean "the backtest says" is always shorthand for "the emulator, under these assumptions, says".
Two practical consequences
An indicator() marketed as a bot is not one. It can draw arrows and fire
alerts; it cannot hold a position or report a drawdown. If a published script's
second line says indicator(, no equity curve it shows came from that script.
Converting an indicator to a strategy is not a rename. You have to decide
things the indicator never needed: when to enter, when to exit, how much, what
happens to an open position when the signal repeats. Pine defaults
pyramiding (opens in a new tab) to 1 [1], so that last one has an answer whether or not you chose
it.
Those decisions are the actual work, and they are the same ones converting a manual strategy forces into the open.
How to turn an indicator into a strategy
The section above says converting an indicator is not a rename. This one shows what the conversion is, step by step, with one indicator carried all the way through: an RSI script that marks the bar where RSI crosses back above 30.
//@version=6
indicator("RSI cross", overlay = false)
rsiLen = input.int(14, "RSI length")
r = ta.rsi(close, rsiLen)
plot(r, "RSI")
crossUp = ta.crossover(r, 30)
plotshape(crossUp, "Cross up", shape.triangleup, location.bottom)
alertcondition(crossUp, "RSI cross up", "RSI crossed above 30")
This script answers one question: when does the condition happen? A strategy has to answer four more.
1. Change the declaration, and write down its defaults
Replace indicator( with strategy(. That switches on the broker emulator,
and it also hands you settings you did not choose. Unless you set them, the
commission is zero, the slippage is zero, every order is one contract or unit,
and pyramiding is 1. A report built on those defaults describes a market
with no costs. So write them out, with your broker's figures in place of the
examples:
strategy("RSI cross strategy", overlay = false,
initial_capital = 10000,
default_qty_type = strategy.cash, default_qty_value = 1000,
commission_type = strategy.commission.percent, commission_value = 0.05,
slippage = 2,
pyramiding = 1)
A fixed cash amount per trade keeps the test free of compounding, which makes a mediocre rule look better the longer the test runs.
2. Turn the signal into an entry
The triangle becomes an order:
if crossUp and strategy.position_size == 0
strategy.entry("Long", strategy.long)
Two things change meaning here. The order is decided at the bar's close and, by
default, fills at the next bar's open, not at the price where the triangle
was drawn. And strategy.position_size == 0 answers the question the
indicator never had: what happens when the signal repeats while a trade is
open. Here, nothing.
Keep the ta.rsi() and ta.crossover() calls where they were, on their own
lines at the top level, so they run on every bar.
3. Decide the exit
The indicator never needed one, and it is where most of the result comes from.
The common choices are a stop and a target with strategy.exit(), an exit on
an opposite signal with strategy.close(), or a time limit. A stop and a
target, as a percentage of the entry price:
stopPct = input.float(1.0, "Stop loss %")
targetPct = input.float(2.0, "Take profit %")
if strategy.position_size > 0
entryPrice = strategy.position_avg_price
strategy.exit("Exit", "Long",
stop = entryPrice * (1 - stopPct / 100),
limit = entryPrice * (1 + targetPct / 100))
Note what is not here: RSI crossing back below 70 does not close anything unless you write that rule. An arrow on the indicator was never an exit.
4. Read the size and the costs into the result
Net profit in the report is money, so the size and the costs from step 1 move it directly. Before you read any figure, check that the commission and slippage match your broker; the details are in the cost parameters. Then read the report in the order set out in the Strategy Tester guide: the trade count first, then the drawdown.
What does not carry over
| In the indicator | In the strategy |
|---|---|
alertcondition() | has no effect in a strategy, and the Pine Editor warns you so; only the strategy.* calls trade |
plot(), plotshape() | still draw, and change no result |
| a value from a higher timeframe | may not have been known at the bar's close; check repainting and lookahead before you trust the test |
| "it looks right on the chart" | is now a trade count you can check |
The same rules without writing Pine
In the AlfaTactix Strategy Builder you choose TradingView as the platform in
Step 1 of its 7 steps. The indicator rule goes into Step 3, Entry rules; the
stop and target into Step 5, Exits; the size into Step 6, Risk and money
management. You get a Pine Script v6 strategy() file in which every
indicator is written out as code you can read.
The limit is real: the form offers 47 built-in indicator types and does not import Pine code, so a custom indicator cannot be pasted in. If your indicator is one of those types, you rebuild its rule in the form; if it is not, the code route above is the way. To start from your rule, build it in the Pine Script generator.
What a TradingView strategy is not
The word strategy is doing three different jobs in this subject, and running them together is expensive:
- a Pine script whose declaration is
strategy() - a trading plan — rules, sizing, when you will not trade
- an automated system that places orders with a broker
A strategy() script is the first. It is frequently sold as the third.
It does not place orders
This is the boundary worth being precise about. A strategy can raise alerts, and an alert created from a strategy can be set so that order fill events also trigger it [5]. But an alert is a notification. Getting from a fired alert to a filled position needs a broker integration, and the latency between them is yours — it is not in the emulator, which assumed the fill happened at the price it modelled.
And a detail that surprises almost everyone:
Alerts only trigger in the realtime bar. [5]
So a backtest showing two hundred trades produces zero alerts. Every fill in the report is a replay of history; an alert is a realtime event. Nothing on the historical part of the chart ever fired one, and no setting changes that. The full behaviour, including the frequency constant that defaults to firing intrabar rather than on close, is in running and maintaining a strategy.
It is not a trading plan
A strategy() script contains entries, exits and sizing. A trading plan also
contains the parts that never become code: how much of your capital this idea
gets, what you do when it is in drawdown, when you would abandon it, and what
else you are running alongside it.
The script cannot hold those, and a backtest cannot test them. This is why a strategy with a good equity curve and no plan around it still fails — not because the code was wrong.
It is not evidence on its own
A strategy() script that compiles, runs and reports a profit has demonstrated
that the emulator, under its assumptions, on this slice of history, with these
parameters, produced this curve. Each clause in that sentence is a
qualification:
| Clause | Why it limits the claim |
|---|---|
| the emulator's assumptions | intrabar order is inferred, gaps are treated as empty [1] |
| this slice of history | how much you get depends on your account plan [6] |
| these parameters | if you tried ten sets, the best is a biased estimate |
| this instrument | slippage is in ticks, and a tick is not the same size elsewhere |
None of that makes backtesting useless. It makes a backtest a filter rather than a forecast: it can tell you an idea is broken, and it cannot tell you an idea is good.
So what is it good for?
Specifying an idea precisely enough that it can be wrong. That sounds modest and is the entire value. A rule you can state, test and audit is one you can improve; a discretionary feel is one you can only argue about.

