Skip to content

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

ArgumentMeaning
whenCondition that places the order
enterOpen a long or short position
exitClose a long or short position
ordermarket, limit, or stop
priceRequired for limit and stop orders
stopStop-loss level attached to the entry
takeTake-profit level attached to the entry
sizeSize helper such as pct(50)
riskRisk-based sizing via cash(...) or equity_pct(...)
cancel_whenCancel a pending order when this becomes true
expire_afterCancel a pending order after N bars
tagTag the entry leg
targetTarget tagged legs on exit or protection

Rules:

  • a trade(...) call must use either enter= or exit=, not both
  • price= is invalid on market orders and required on limit / stop
  • risk= requires a resolved stop=...
  • 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 = day

Supported keys:

  • max_open_positions
  • max_open_positions_long
  • max_open_positions_short
  • max_open_notional
  • max_open_risk
  • daily_loss_limit
  • daily_trade_limit
  • reset_tz
  • halt_on_breach

Backtest Defaults in script(...)

The script header can still include backtest defaults such as:

  • initial_capital
  • commission_pct
  • fee_discount_pct
  • size_pct
  • reverse
  • hedging
  • pyramiding
  • max_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)