Overview
A Pine Script strategy simulates trade fills on historical bars and generates the Strategy Tester report. Use it when you need a PnL curve, win rate, drawdown, or Sharpe ratio. For visual-only analysis, use pine-script-indicators instead. Strategies pay a computation cost: TradingView recalculates the entire backtest on every bar change, so they load more slowly than indicators. This page covers the declaration, the entry and exit functions, and how to read the backtest results honestly.
Declare with strategy() and set capital parameters
The strategy() call defines the simulation environment. Get the parameters right before writing signal logic; they change every performance number.
//@version=5
strategy(
title = "Donchian Breakout",
overlay = true,
initial_capital = 10000,
default_qty_type = strategy.percent_of_equity,
default_qty_value = 10,
commission_type = strategy.commission.percent,
commission_value = 0.1,
slippage = 2,
pyramiding = 0,
calc_on_every_tick = false
)initial_capital and default_qty_type control position sizing. strategy.percent_of_equity sizes positions as a fraction of current equity, so the position grows as equity grows. strategy.fixed uses a fixed number of contracts or shares.
Set commission_value to a realistic value for your broker. A zero-commission backtest on a scalping strategy will look better than live trading by a large margin.
calc_on_every_tick = false means fills compute on bar close only. Combined with bar-close signals, this eliminates intrabar lookahead. Leave it false unless you are testing a tick-level strategy.
Enter positions with strategy.entry
strategy.entry places a simulated market or limit order and automatically closes the opposite position if one is open.
length = input.int(20, title="Donchian Length")
upper = ta.highest(high, length)
lower = ta.lowest(low, length)
// Use [1] to signal on the closed bar, not the forming bar
if close > upper[1]
strategy.entry("long", strategy.long)
if close < lower[1]
strategy.entry("short", strategy.short)Always signal on [1] or a closed higher-timeframe bar unless you have verified there is no lookahead. See pine-script-security for the full repainting explanation.
Use strategy.order when you want unconditional fills regardless of position state. Use strategy.entry for signal-based entries; it handles position reversal automatically.
Exit with strategy.exit for bracket orders
strategy.exit attaches a stop-loss and take-profit to a named entry. It is cleaner and more realistic than watching for price levels in if blocks.
atrVal = ta.atr(14)
strategy.entry("long", strategy.long, when = close > upper[1])
strategy.exit(
id = "long-exit",
from_entry = "long",
stop = strategy.position_avg_price - 2 * atrVal,
limit = strategy.position_avg_price + 3 * atrVal,
comment = "SL/TP"
)stop and limit can also be set as loss and profit in ticks or as trail_offset for a trailing stop. Use strategy.close to close a position by name without an explicit target price.
Set pyramiding intentionally
pyramiding = 0 means only one entry in the same direction is allowed at a time. pyramiding = N allows up to N additional entries. Most strategies should start with pyramiding = 0. Accidentally pyramiding inflates backtest returns by adding to winning positions, which live execution may not replicate at the same fill prices.
Read the Strategy Tester with skepticism
The Strategy Tester reports net profit, win rate, profit factor, max drawdown, and Sharpe ratio. Read each number in context.
- Net profit on a long-only strategy during a bull market is not signal; it is beta.
- Profit factor above 3 on less than 50 trades often reflects overfitting.
- Max drawdown should be within your tolerable loss. A strategy with a 60% drawdown is not usable regardless of net profit.
- Compare against a buy-and-hold benchmark. If the benchmark beats the strategy, the strategy is adding noise.
Validate on out-of-sample data by setting a custom date range in the tester that excludes the period used to develop the parameters. If the out-of-sample results diverge sharply, the strategy is overfit.
Use strategy.risk functions to enforce hard limits
strategy.risk.max_drawdown(20, strategy.percent_of_equity)
strategy.risk.max_position_size(5)max_drawdown stops trading when the simulated drawdown exceeds the threshold. Wire it to the same value you would halt trading at in live use; backtests that ignore drawdown limits are not honest simulations.