Strategies and the Tester
How to write a strategy in LootScript and verify it on historical data with the tester: declaring strategy(), orders, the fill model, costs, report tabs and backtest limits.
A strategy is a script that, besides drawing lines on the chart, submits orders. Orders are not executed by an exchange but by a simulator inside the terminal: it runs the strategy over chart history with commissions, slippage and funding rate, and shows the result in the Strategy Tester window.
Declaring strategy()
A strategy is distinguished from an indicator by the first line: strategy(...) instead of indicator(...). Everything else — settings, drawing, market data — works the same. strategy.* functions in an indicator do not compile: “strategy.* is available only in strategy(…)”.
| Parameter | Default | What it sets |
|---|---|---|
initialCapital |
10000 |
Starting capital |
defaultQtyType, defaultQtyValue |
strategy.qty.fixed, 1 |
Order size when qty is not specified |
commissionType, commissionValue |
percent, none | Commission: strategy.commission.percent — percent of turnover (0.05 = 0.05 %), strategy.commission.cash_per_contract — cash per unit, strategy.commission.cash_per_order — cash per execution |
slippageTicks |
0 |
Slippage in ticks |
pyramiding |
0 |
How many entries in the same direction can be open; 0 — one |
positionModel |
strategy.model.netting |
Single net position or separate long and short (strategy.model.hedging) |
processOrdersOnClose |
false |
Execute market orders on the current bar close |
calcOnEveryTick |
false |
Allow orders on the forming bar |
marginLong, marginShort, leverage |
0, 0, 1 |
Margin in percent or leverage; at 0 liquidation is not modelled |
applyFunding |
true |
Apply funding rate |
fillModel |
strategy.fill.conservative |
Assumption about price path inside a bar |
Parameter values must be numbers or constants written directly. An expression or input.* inside the declaration is a compilation error. The parameter calcOnOrderFills is accepted but inactive in this release: on the tester’s “Properties” tab it is marked “inactive in this build”.
Order size when qty = na is determined by defaultQtyType:
| Type | What defaultQtyValue means |
|---|---|
strategy.qty.fixed |
Quantity in coins (base currency) |
strategy.qty.cash |
Amount in quote currency |
strategy.qty.percent_of_equity |
Percent of current equity |
strategy.qty.risk_percent |
Percent of equity you risk up to a protective stop |
strategy.qty.risk_percent requires a strategy.exit with a stop (loss or stop), otherwise the compiler stops with: “requires a stop to calculate size”. Quantity is rounded down to the instrument’s quantity increment. If rounding yields zero, the order is rejected and counts toward the tester counter “Rejected orders (size)”.
Orders
| Function | What it does |
|---|---|
strategy.entry(id, direction, qty, limit, stop, …) |
Opens a position or reverses it. Respects the position model and pyramiding |
strategy.order(id, direction, qty, limit, stop, …) |
Same without position model and pyramiding |
strategy.exit(id, from_entry, qty, qty_percent, profit, limit, loss, stop, trail_price, trail_points, trail_offset, …) |
Places an exit — take, stop or trailing — for an open entry |
strategy.close(id, comment, qty, qty_percent) |
Closes an entry with a market order |
strategy.close_all(comment) |
Closes everything |
strategy.cancel(id), strategy.cancel_all() |
Cancels unfilled orders |
Direction is strategy.long or strategy.short. All functions accept an optional when: when = false skips the call. Entries, exits and closes also accept comment — a text label attached to the order.
- Type of entry. Without
limitandstop— a market order; withlimit— a limit order; withstop— a stop order; with both — a stop-limit. Re-submittingstrategy.entrywith the sameidreplaces an unfilled order. - Reversal. In
nettingmodel an entry in the opposite direction closes the current position and opens a new one in a single action. - Exit units.
profitandlossare in ticks from the entry price;limitandstopare absolute prices. Trailing is specified as a pair:trail_offsetplustrail_priceortrail_points. - An exit with no levels does nothing. The compiler warns: “strategy.exit without any level (profit/loss/stop/limit/trail) — no-op”.
- Moving a stop. Re-submitting
strategy.exitwith the sameidreplaces the exit — that’s how stops are moved.
Strategy state is read via fields: strategy.position_size (signed; short is negative), strategy.position_avg_price, strategy.equity, strategy.netprofit, strategy.openprofit, strategy.opentrades, strategy.closedtrades, strategy.funding_paid and others — full list in the reference.
How the tester fills orders
An order submitted on a bar cannot execute earlier than the next bar. Decisions are made at bar close, and filling at the same price would imply peeking forward.
| Order | Execution |
|---|---|
| Market | At the open of the next bar plus slippage. With processOrdersOnClose = true — at the current bar close |
| Limit | When price reaches the limit, at the limit price. If the bar opened beyond the limit — at the open |
| Stop | When price reaches the stop, at the stop price plus slippage. On a gap — at the open plus slippage |
Price path inside the bar. The tester does not know the real intra-bar path and assumes one. The strategy.fill.conservative model (default) assumes price first reached the worst extreme: on a bullish bar — open → low → high → close; on a bearish bar — the reverse. If a stop and a take occur within the same bar, the stop fires. The strategy.fill.optimistic model assumes the opposite — useful only for comparison. There is no lower-timeframe replay in this release: the “Path” column marks each trade as assumed by the model.
Costs.
- Commission. If
commissionValueis not set, the tester uses a default tariff: 0.02% for limit fills and 0.05% for market and stop fills. Your account tariff and any rebates to the terminal are unknown. A configured number applies to all fills. - Slippage —
slippageTicksagainst you on market and stop fills. Limit fills do not slip. - Funding rate is charged at every real exchange settlement — using the actual funding rate and the bar close price for the settlement. This requires funding data: futures funding on exchanges from the coverage table.
- Margin. With
marginLong/marginShortor leverage > 1 the tester models a simplified isolated margin. A long position liquidates at entry × (1 − m/2), a short at entry × (1 + m/2), where m is the fraction of initial margin. A liquidated trade loses the entire initial margin.
Trades not closed by the end of history are closed at the final bar close with reason “end of data”.
Strategy Tester
Add the strategy to the chart
In the script Editor press Add to chart (Ctrl+Enter). A script with errors will not be added to the chart.
Open the tester
In the Editor toolbar press Strategy Tester… — the button is active only for a script with strategy(...). The alternative path is strategy settings from the indicator legend (gear), button Strategy Tester….
Wait for the run
The tester computes on the candles loaded on the chart. While there are no candles the window shows “Strategy has not been run yet: wait for candles to load”. When new candles arrive the result recalculates automatically; the Recalculate button runs a full pass manually.
If multiple strategies are on the chart, pick the one you want directly in the tester window.
| Tab | What’s on it |
|---|---|
| Summary | Tiles “Net profit”, “Profit factor”, “Max. drawdown (by closes)”, “Percent profitable”, “SQN”; a strip “What this backtest doesn’t know”; equity curves, buy-and-hold, equity without funding and two drawdowns |
| Metrics | Results, risk (drawdown by closes and intra-bar separately, Sharpe, Sortino, Calmar), trades (percent profitable, averages and largest, run lengths), costs (commission and funding) |
| Trades | Trades table: entry, exit, reason, size, P&L, run-up, drawdown, commission, funding. Click a row to scroll the chart to the entry bar, Ctrl+C copies selected rows |
| Properties | Declaration parameters, bar range, price step, quantity step, minimum lot, commission, run counters: rejected orders, skipped due to pyramiding, liquidations, funding settlements |
The window title shows mode tags: conservative or optimistic for the fill model and close-fill when processOrdersOnClose = true. Tags optimistic and close-fill mean the live result will most likely be worse.
What a strategy cannot read
Live order book and time and sales. A strategy that reads book.* or tape.* does not compile: “Strategy reads … order book and time and sales have no history, the backtest would be fiction”. For book history use depth.*, for trades use delta.* and cluster.*.
Screener. screener.* is also only available on the latest bar, but the compiler allows such a strategy. On history screener values are na, so conditions on them never fire and there will be no trades in the report.
Peeking forward. request.security(..., lookahead = true) in a strategy shows a warning in the Editor: “Strategy peeks forward … report marked as unreachable in real time”. Treat such a report as inflated: on history the strategy sees values that would not exist live. More in Market data.
Example
A channel breakout confirmed by delta. Entry on close outside the channel over the last len bars if the bar delta is in the same direction. Stop in ATR, take twice as far.
strategy("Пробой канала с дельтой", overlay = true,
initialCapital = 10000,
defaultQtyType = strategy.qty.percent_of_equity, defaultQtyValue = 20,
commissionValue = 0.05, slippageTicks = 2)
len = input.int(20, "Канал, баров", minval = 5, maxval = 200)
stopAtr = input.float(1.5, "Стоп, ATR", minval = 0.5, maxval = 10, step = 0.1)
needFlow = input.bool(true, "Подтверждать дельтой")
upper = ta.highest(high, len)[1]
lower = ta.lowest(low, len)[1]
atr = ta.atr(14)
flowLong = not needFlow or (delta.available and delta.value > 0)
flowShort = not needFlow or (delta.available and delta.value < 0)
if close > upper and flowLong and strategy.position_size <= 0 {
strategy.entry("L", strategy.long, comment = "пробой вверх")
}
if close < lower and flowShort and strategy.position_size >= 0 {
strategy.entry("S", strategy.short, comment = "пробой вниз")
}
stopTicks = math.round(atr * stopAtr / sym.tick_size)
strategy.exit("L-x", from_entry = "L", loss = stopTicks, profit = stopTicks * 2)
strategy.exit("S-x", from_entry = "S", loss = stopTicks, profit = stopTicks * 2)
plot(upper, "Верх канала", color = color.teal)
plot(lower, "Низ канала", color = color.red)On an exchange without delta (for example, Binance Alpha) with “Подтверждать дельтой” enabled the strategy will make no trades: delta.available is false there. This is na at work, not an error.
Limitations
- Simulation only: no real orders.
- Limit orders execute as if you were first in queue. For maker strategies this is the main source of inflated results.
- Your trades do not move price; no partial fills: an order either fills entirely or not at all.
- The intra-bar price path is assumed by a model, not reconstructed from a lower timeframe.
- Liquidation is simplified: the exchange’s liquidation engine, ADL and insurance fund are not modelled.
- Commission is the default tariff, not your account tariff.
- Missing data is not missing movement: a bar without clusters still moved.
- Results depend on how much history is loaded on the chart.