Strategies & Backtesting
Rune strategy scripts use the same language surface as indicators, but add execution semantics through trade(...), protect(...), position-state builtins, and optional top-level risk: controls.
Strategy Mode
To create a strategy, set mode=strategy in the script header:
script(name="EMA Strategy", version=1, mode=strategy, overlay=true)Strategy scripts can still draw visuals, emit signals, panels, and alerts. They simply add execution behavior on top.
trade(...)
trade(...) is the entry and exit statement.
Market Entry
trade(when=setup, enter=long)
trade(when=setup, enter=long, stop=stop_price, take=take_price, size=pct(50))
trade(when=setup, enter=long, stop=stop_price, risk=cash(100))Pending Entry
trade(
when=retest,
enter=long,
order=limit,
price=entry_price,
stop=stop_price,
risk=equity_pct(1),
cancel_when=close < ema(close, 50),
expire_after=3,
tag="core"
)Exit
trade(exit=long, when=exit_signal)
trade(exit=long, when=take_partial, size=pct(50), target="core")
trade(exit=long, order=limit, price=target_price, when=runner_open, target="runner")Key trade(...) Arguments
| Argument | Meaning |
|---|---|
when | Condition that places the order |
enter | Open a long or short position |
exit | Close a long or short position |
order | market, limit, or stop |
price | Required for limit and stop orders |
stop | Stop-loss level attached to the entry |
take | Take-profit level attached to the entry |
size | Size helper such as pct(50) |
risk | Risk-based sizing via cash(...) or equity_pct(...) |
cancel_when | Cancel a pending order when this becomes true |
expire_after | Cancel a pending order after N bars |
tag | Tag the entry leg |
target | Target tagged legs on exit or protection |
Rules:
- a
trade(...)call must use eitherenter=orexit=, not both price=is invalid onmarketorders and required onlimit/stoprisk=requires a resolvedstop=...- pending orders are created after the signal bar closes; fills start on the next bar
protect(...)
protect(...) updates managed stop/take levels for open positions.
protect(side=long, when=trail_ready, stop=trail_stop)
protect(side=long, target="runner", when=lock_profit, stop=position_avg_entry(long, target="runner"))Rules:
- protection updates become active on the next bar
- later
protect(...)declarations on the same bar win target=applies only to matching tagged legs- untargeted
protect(...)applies to the whole side
Tags and Targeted Exits
Entry tags let you separate a position into logical legs such as core and runner.
trade(when=setup, enter=long, tag="core", stop=core_stop, risk=cash(100))
trade(when=setup, enter=long, tag="runner", stop=runner_stop, risk=cash(50))
trade(exit=long, when=take_core, target="core")
protect(side=long, target="runner", when=runner_trail, stop=runner_stop)Use this when you want one leg to take profit while another leg trails.
Position-State Builtins
Rune strategies can inspect live position state:
position_qty(long)
position_avg_entry(long, target="runner")
position_stop(long)
position_take(long)
position_bars(long)
position_initial_stop(long)
position_initial_risk(long)
position_open_r(long, target="runner")These are most commonly used for:
- breakeven moves
- fee-covered breakeven
- explicit trailing logic
- tag-aware management
Example: Breakeven After 1R
protect(
side=long,
when=position_open_r(long) >= 1,
stop=position_avg_entry(long)
)Top-Level risk:
Strategies can declare portfolio-level limits:
risk:
max_open_positions = 1
max_open_notional = equity_pct(100)
max_open_risk = equity_pct(2)
daily_loss_limit = equity_pct(3)
daily_trade_limit = 4
reset_tz = "UTC"
halt_on_breach = daySupported keys:
max_open_positionsmax_open_positions_longmax_open_positions_shortmax_open_notionalmax_open_riskdaily_loss_limitdaily_trade_limitreset_tzhalt_on_breach
Backtest Defaults in script(...)
The script header can still include backtest defaults such as:
initial_capitalcommission_pctfee_discount_pctsize_pctreversehedgingpyramidingmax_entries
These act as defaults. Users can still override them in the backtest UI.
Execution Model
Keep the runtime split in mind:
- Chart preview is authoritative for previewed series, draws, and preview markers
- Local Backtester is authoritative for fills, trade logs, and execution-grade results
That is why strategy scripts should be validated against real backtests, not only chart preview.
Example Strategy
script(name="Breakout Retest", version=1, mode=strategy, overlay=true)
param higher_tf = timeframe("Higher TF", "15m", options=["15m", "1H"])
param risk_cash = float("Risk ($)", 100, min=1, group="Risk")
stream market = ohlcv()
stream htf = ohlcv(timeframe=higher_tf)
risk:
max_open_positions = 1
daily_trade_limit = 3
reset_tz = "UTC"
bar:
let fast = ema(market.close, 20)
let slow = ema(market.close, 50)
let swing_low = lowest(market.low, 10)
let retest_price = fast
guard regime_ok = market.close > slow and htf.close > htf.open
signal setup = regime_ok and market.low <= retest_price
trade(
when=setup,
enter=long,
order=limit,
price=retest_price,
stop=swing_low,
risk=cash(risk_cash),
tag="core",
expire_after=2
)
protect(
side=long,
target="core",
when=position_open_r(long, target="core") >= 1,
stop=position_avg_entry(long, target="core")
)
draw(type=line, series=fast, color=signal_long)
draw(type=line, series=slow, color=signal_short)