Two people run the same Pine Script strategy on the same chart and get very different reports. Nine times out of ten the code isn't the difference: the settings are. Commission, slippage, order size, margin and the way fills are simulated inside a bar can turn a smooth equity curve into a flat one, or the other way round.
This guide goes through the Strategy Tester's properties in the order you'll meet them, explains what each one does to fills and profit, and shows how to put them in the strategy() declaration so the report doesn't depend on someone remembering to click. Then it gives a routine for a backtest you can actually learn from. It's about testing honestly, not about finding a strategy that wins.
Where the settings live
Every setting exists in two places:
- In the code, as parameters of
strategy(). These are the defaults anyone gets when they add the script. - In the Properties tab of the strategy's settings on the chart. Changing a value there overrides the code for that chart only.
That split causes a lot of confusion. You tweak commission in the Properties tab, the report improves, and the code you share still carries the old default. Treat the declaration as the record of your assumptions, and use the Properties tab for quick experiments only.
The properties, one by one
| Property | strategy() parameter | What it changes | Sensible starting point |
|---|---|---|---|
| Initial capital | initial_capital | The base for percentage sizing and returns | The account size you'd really trade |
| Base currency | currency | The currency the report is in | Your account currency |
| Order size | default_qty_value + default_qty_type | Size when strategy.entry() has no qty: contracts, cash or % of equity | A modest % of equity, or risk-based qty in code |
| Pyramiding | pyramiding | How many entries in the same direction can stack | The default (a single position) unless you scale in on purpose |
| Commission | commission_type + commission_value | Cost per order: a percentage, cash per contract or cash per order | Your broker's real cost, charged on entry and exit |
| Verify price for limit orders | backtest_fill_limits_assumption | A limit order only fills if price trades through it by this many ticks | 1–2 ticks on liquid markets |
| Slippage | slippage | Ticks added against you on market and stop fills | A few ticks; more on thin markets and around news |
| Margin for long / short positions | margin_long, margin_short | Share of position value that equity must cover; below it, simulated margin calls | 100 without leverage; 100 ÷ leverage otherwise |
| Recalculate: after order is filled | calc_on_order_fills | Runs the script again inside the bar after each fill | Off, unless your logic needs it |
| Recalculate: on every tick | calc_on_every_tick | Recalculates on live ticks; history only has closed bars | Off for backtests |
| Fill orders: using bar magnifier | use_bar_magnifier | Uses lower-timeframe data to decide what filled first inside a bar | On, where your plan includes it |
| Fill orders: on bar close | process_orders_on_close | Market orders fill at the signal bar's close instead of the next open | Off, unless you can really trade the close |
| Fill orders: using standard OHLC | fill_orders_on_standard_ohlc | On Heikin Ashi and similar charts, fills use real prices | On whenever the chart type isn't standard candles |
Labels in the Properties tab change from time to time; the parameter names in the reference manual are the stable reference.
How TradingView decides what filled inside a bar
On historical bars the simulated broker only knows each bar's open, high, low and close, not the path in between. It assumes one: if the high is closer to the open than the low is, price went open → high → low → close; otherwise open → low → high → close.
That assumption matters whenever a stop and a target both sit inside the same bar's range. The report shows one of them filled, but the real market might have hit the other first. The bar magnifier replaces the guess with data from a lower timeframe, which makes those bars much closer to reality.
A useful test: run the strategy with the magnifier on and off. If the results change a lot, many of your trades are decided inside a single bar, which usually means stops and targets that are tight relative to the bar size. Either accept the magnifier's version or test on a lower timeframe.
The other default rules are worth knowing too:
- Market orders placed when a bar closes fill at the next bar's open.
- Limit orders fill at the limit price or better; if price gaps through, at the open.
- Stop orders fill at the stop price or worse; if price gaps through, at the open, plus slippage.
- Slippage applies to market and stop fills, not to limit fills.
Putting the settings in code
Here's a declaration with every assumption written down, plus a simple strategy so the script runs. The strategy is a placeholder for testing the settings, not a recommendation:
//@version=6
strategy("Backtest settings demo", overlay = true,
initial_capital = 10000,
currency = currency.USD,
default_qty_type = strategy.percent_of_equity,
default_qty_value = 10,
pyramiding = 0,
commission_type = strategy.commission.percent,
commission_value = 0.05,
slippage = 2,
backtest_fill_limits_assumption = 1,
margin_long = 100,
margin_short = 100,
process_orders_on_close = false,
calc_on_every_tick = false,
calc_on_order_fills = false,
use_bar_magnifier = true,
fill_orders_on_standard_ohlc = true)
// Test window: entries are only allowed between these dates
startTime = input.time(timestamp("2020-01-01T00:00:00"), "Start", group = "Test window")
endTime = input.time(timestamp("2025-12-31T23:59:59"), "End", group = "Test window")
inWindow = time >= startTime and time <= endTime
fast = ta.ema(close, 20)
slow = ta.ema(close, 50)
atr = ta.atr(14)
crossUp = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)
var float stopPrice = na
if inWindow and crossUp and strategy.position_size == 0
stopPrice := close - 2 * atr
strategy.entry("Long", strategy.long)
if crossDown
strategy.close("Long")
strategy.exit("Stop", "Long", stop = stopPrice)
// Close anything still open once the window ends, so the report covers the window only
if not inWindow and strategy.position_size != 0
strategy.close_all()
plot(fast, "Fast EMA", color.teal)
plot(slow, "Slow EMA", color.orange)
A few notes:
commission_value = 0.05withstrategy.commission.percentcharges 0.05% of the order value on every order, so a round trip costs about 0.1%. For futures,strategy.commission.cash_per_contractis usually closer to how you're billed.slippage = 2is in ticks, so its money value depends on the symbol's tick size.- TradingView's forex and CFD data is a single price stream, so the spread isn't modelled. Add it through slippage or commission, or the backtest trades at prices you'd never get.
use_bar_magnifier = trueonly has an effect on plans that include the feature; elsewhere it's ignored.
Why a test window in code helps
The Strategy Tester lets you pick a date range, and that's fine for a quick look. The inWindow filter above does something extra: the indicators still calculate on all the bars before startTime, so a 200-period average is fully warmed up when the first trade can happen. It also lets you run the same code on two separate periods by changing two inputs, which is the basis of an out-of-sample test.
Reading the report
Start with these, roughly in this order:
- Total trades. With fewer than a few dozen, the other numbers are mostly noise.
- Net profit after costs, compared with buy-and-hold over the same window.
- Maximum drawdown. Ask whether you'd have kept trading through it. The drawdown calculator shows what it takes to recover.
- Average trade versus costs. If the average trade is barely larger than commission plus slippage, a small change in costs flips the result.
- Win rate together with average win and loss. Either alone says little. Put them into the expectancy calculator.
- Longs versus shorts, and results over time in the list of trades: one great year can carry a weak strategy.
- Margin calls in the list of trades. If you see them, the position sizes don't fit the margin setting.
A backtesting routine that holds up
- Write costs, size and margin into the declaration, as above.
- Compare with the bar magnifier on and off, and keep the more realistic result.
- Split the history. Choose settings on an older window, then run them unchanged on a recent one.
- Check for repainting. Signals that move after the bar closes make every number meaningless; see Pine Script repainting explained.
- Look at neighbouring settings. If a 20-period length works and 18 and 22 don't, you've found noise.
- Try other symbols in the same asset class.
- Forward-test on paper trading or a demo account before any real money is involved.
The traps this routine guards against are explained in more depth in 8 backtesting mistakes that make a strategy look better than it is.
What the Strategy Tester can't tell you
It can't see liquidity, partial fills or spreads that widen at the open and around news. Its data can differ slightly from your broker's. And it can't tell you whether the market will behave next year the way it did in the test. A careful backtest narrows the guesswork; it doesn't remove it.
Sources and further reading
- Pine Script User Manual: Strategies — the broker emulator, order fills, commission, slippage and margin.
- Pine Script v6 reference: strategy() — every declaration parameter used above.
- Pine Script User Manual: Repainting — why live and historical results can differ.
Try it with Edgoro
When you ask Edgoro for a TradingView strategy, the declaration comes with initial capital, position size and commission already set, ready for the Strategy Tester. For crypto, the Backtest Lab runs strategies on real exchange data with fees and slippage you choose. Either way, a backtest describes the past, so forward-test before you trust it.
Backtested results are hypothetical and don't account for every real-world cost or condition. Nothing here is investment advice, and trading involves substantial risk of loss.