RoboQuant
RoboQuant engine

Runtime Reference

Quick reference for compiled Roboquant .rq strategies. Import the complete strategy surface with:

use rq_sdk::prelude::*;

Strategy declaration

#[strategy(name = "My Strategy")]
pub struct MyStrategy {
    #[param(default = 14, min = 2, max = 100, title = "Period")]
    period: i64,
    indicator: Ind,
}

impl Strategy for MyStrategy {
    fn on_init(&mut self, init: &mut Init) {
        self.indicator = init.add(Rsi::new(self.period.max(2) as usize));
    }

    fn on_bar(&mut self, ctx: &mut Ctx, bar: Bar) {
        let Some(rsi) = ctx.val(self.indicator) else { return };
        if ctx.position() == 0 && rsi < 30.0 {
            ctx.buy(1).sl(bar.close - 10.0).tp(bar.close + 20.0).send();
        }
    }
}

#[strategy] generates the parameter schema, default state, and runtime exports. Every non-parameter field is reset to its default value for a new run.

Parameters

#[param(default = 20, min = 5, max = 200, step = 5, title = "Lookback")]
lookback: i64,

#[param(default = 0.01, min = 0.001, max = 0.05, step = 0.001, title = "Risk")]
risk: f64,

#[param(default = true, title = "Allow shorts")]
allow_shorts: bool,

#[param(default = 20, options = [10, 20, 50], title = "Preset")]
preset: i64,
Field typeDashboard control
i64Integer input
f64Decimal input
boolOn/off control
Numeric field + optionsFixed-choice control

options is mutually exclusive with min, max, and step.

Single-symbol hooks

fn on_init(&mut self, init: &mut Init) { ... }
fn on_bar(&mut self, ctx: &mut Ctx, bar: Bar) { ... }
fn on_tick(&mut self, ctx: &mut Ctx, tick: Tick) { ... }
fn on_timer(&mut self, ctx: &mut Ctx, now: Time) { ... }
fn on_trade(&mut self, ctx: &mut Ctx, txn: TradeTxn) { ... }
fn wants_ticks(&self) -> bool { true }

Only on_init is required by the trait. Most strategies also implement on_bar. A strategy using on_tick must implement wants_ticks and return true.

Use init.set_timer(seconds) in on_init to enable on_timer.

Market types

Bar

FieldTypeMeaning
timeTimeBar open timestamp
open, high, low, closef64OHLC prices
volumef64Bar volume

Tick

FieldTypeMeaning
timeTimeTrade timestamp
pricef64Trade price
sizef64Trade size
sideAggressorBuy, Sell, or Unknown

Time

time.hour()                 // u32
time.minute()               // u32
time.second()               // u32
time.hhmm()                 // u32 — 1330 for 13:30
time.date_num()             // u32 — 20260806
time.weekday()              // Weekday::Mon, ...
time.minutes_since(9, 30)   // i64
time.et()                   // US Eastern wall-clock Time (EST/EDT aware)
time.et_hhmm()              // u32 — 930 for 09:30 US Eastern
time.et_date_num()          // u32 — Eastern session date

Engine timestamps are UTC. All clock-component helpers return u32 (only minutes_since() returns i64); cast i64 params with as u32 at the comparison and type session-date state fields as u32.

Use et_hhmm()/et_date_num() for New York and CME session rules. For a deliberate fixed offset, construct a shifted wall clock with Time::from_us(time.us() + offset_hours * 3_600_000_000). Shifted values are for civil-clock comparisons only; retain the original UTC Time for drawing anchors, ordering, and durations. The chart-header timezone changes display labels only and is not inherited by strategy logic.

Init

MethodDescription
init.add(spec)Register one indicator and return an Ind handle
init.add_bbands(period, ndev)Return upper/middle/lower handles
init.add_macd(fast, slow, signal)Return MACD/signal/hist handles
init.add_stoch(k, d, smooth)Return %K/%D handles
init.add_dmi(period)Return ADX/+DI/−DI handles
init.add_donchian(period)Return upper/middle/lower handles
init.add_keltner(period, multiplier)Return upper/middle/lower handles
init.add_supertrend(period, multiplier)Return line/direction handles
init.add_aroon(period)Return up/down/oscillator handles
init.add_stochrsi(rsi_period, stoch_period, d)Return %K/%D handles
init.add_chandelier(period, multiplier)Return long/short stop handles
init.add_heikin_ashi()Return open/high/low/close handles
init.set_timer(seconds)Enable timer callbacks

Registering an indicator does not draw it. In compiled .rq strategies and indicators, only ctx.plot* and ctx.draw output reaches the chart; an init.add(...) series is computed and readable with ctx.val, but not drawn. To show a series, register a plot in on_init and feed it per bar:

self.ema = init.add(Ema::new(20));
self.ema_line = init.plot_line("EMA 20").color("#A78BFA").pane("price").register();
// in on_bar
if let Some(v) = ctx.val(self.ema) { ctx.plot(self.ema_line, v); }

Do not use drawing tools to trace indicator lines. Legacy interpreted Python strategies keep auto-drawn add_indicator lines.

Plot budget: a run keeps at most 24 plots and 1,000,000 plot points in total across them; past that, whole plots are dropped (last registered first) and counted as plots_truncated. The backtest chart recomputes plots per bar for the visible range with a 2,000,000-point budget for runs up to 500,000 bars (plot_max_points on run_wasm_backtest_py; omitted keeps the default). Over that range the chart shows the saved lines and says they are simplified.

Built-in indicators

Single-output specs

Register with init.add(...):

GroupConstructors
Moving averagesSma::new(n), Ema::new(n), Wma::new(n), Trima::new(n), Dema::new(n), Tema::new(n), Kama::new(n), T3::new(n).vfactor(x)
MomentumRsi::new(n), Roc::new(n), Mom::new(n), Cmo::new(n), Trix::new(n), Cci::new(n), WilliamsR::new(n), Mfi::new(n), Ppo::new(fast, slow), Apo::new(fast, slow), Ultosc::new(p1, p2, p3)
Volatility/rangeAtr::new(n), Natr::new(n), Trange::new(), StdDev::new(n).nbdev(x), Variance::new(n), Highest::new(n), Lowest::new(n), Midpoint::new(n), Midprice::new(n)
VolumeObv::new(), Vwap::new(), Ad::new(), Adosc::new(fast, slow)
Statistics/trendZscore::new(n), Linreg::new(n), LinregSlope::new(n), LinregAngle::new(n), LinregIntercept::new(n), Tsf::new(n), Sar::new(accel, max), Bop::new()

Indicator price sources

Price sources: supported single-price indicators default to close. Use Ema::new(period).source(PriceSource::High) to change the input. PriceSource is in the prelude: Open, High, Low, Close, Hl2 = (H+L)/2, Hlc3 = (H+L+C)/3, Ohlc4 = (O+H+L+C)/4, and Hlcc4 = (H+L+C+C)/4.

.source(...) is available on Sma, Ema, Wma, Dema, Tema, Trima, Kama, T3, Rsi, Roc, Mom, Cmo, Trix, Ppo, Apo, StdDev, Variance, Zscore, Midpoint, Linreg, LinregSlope, LinregAngle, LinregIntercept, and Tsf. Set other parameters before source, e.g. T3::new(5).vfactor(0.4).source(PriceSource::Hl2) or StdDev::new(20).nbdev(2.0).source(PriceSource::Open).

self.ema_high = init.add(Ema::new(9).source(PriceSource::High));
self.ema_low = init.add(Ema::new(9).source(PriceSource::Low));
self.ema_close = init.add(Ema::new(9));
self.rsi = init.add(Rsi::new(14).source(PriceSource::Hlc3));
self.sma = init.add(Sma::new(20).source(PriceSource::Open));
let bb = init.add_bbands_with_source(20, 2.0, PriceSource::Hl2);
let macd = init.add_macd_with_source(12, 26, 9, PriceSource::High);

MACD and Bollinger Bands source helpers apply the selected input to every output. Store their handles as usual (bb.upper/middle/lower, macd.macd/signal/hist). Existing add_bbands and add_macd use close. The helpers register immediately: do not call .source(...) on their returned handles. These multi-output helpers remain base-timeframe, single-symbol only.

Read each handle with ctx.val / ctx.val_back; registration automatically plots separate series (ema_9_high, ema_9_low, ema_9, rsi_14_hlc3). Do not substitute close or hand-code a supported indicator when another source is requested. ATR, ADX/DMI, Stochastic, Donchian, and other indicators with defined OHLC/volume inputs do not expose this modifier.

For single-output specs, chain source before timeframe: Rsi::new(14).source(PriceSource::Hlc3).timeframe("1D"). Sources are derived from aggregated OHLC on that timeframe, exposing only completed buckets. Multi-symbol registration also accepts it: init.add("ES", Sma::new(20).source(PriceSource::High)). Every indicator retains its existing warmup and initialization. EMA retains its SMA seed and warmup of period - 1 bars; matching source and length alone does not guarantee identical platform initialization.

Multi-output registrations

let bb = init.add_bbands(20, 2.0);       // bb.upper, bb.middle, bb.lower
let m = init.add_macd(12, 26, 9);        // m.macd, m.signal, m.hist
let st = init.add_stoch(14, 3, 3);       // st.k, st.d
let dmi = init.add_dmi(14);              // dmi.adx, dmi.plus_di, dmi.minus_di
let dc = init.add_donchian(20);          // dc.upper, dc.middle, dc.lower
let kc = init.add_keltner(20, 2.0);      // kc.upper, kc.middle, kc.lower
let sup = init.add_supertrend(10, 3.0);  // sup.line, sup.direction
let ar = init.add_aroon(14);             // ar.up, ar.down, ar.osc
let sr = init.add_stochrsi(14, 14, 3);   // sr.k, sr.d
let ch = init.add_chandelier(22, 3.0);   // ch.long, ch.short
let ha = init.add_heikin_ashi();         // ha.open, ha.high, ha.low, ha.close

Store the individual Ind handles you need on the strategy struct.

Higher-timeframe indicators

Every single-output spec supports .timeframe(...):

self.daily_ema = init.add(Ema::new(20).timeframe("1D"));
self.four_hour_atr = init.add(Atr::new(14).timeframe("4H"));

Accepted forms include seconds, minutes, hours, days, and weeks such as 30s, 15m, 4H, 1D, and 1W (seconds only make sense on a sub-minute base timeframe). Values use the previous completed higher-timeframe bucket and are forward-filled onto base bars. Multi-output helper registrations do not currently accept .timeframe().

Ctx market and account state

MethodReturnDescription
ctx.bar(back)Option<Bar>0 is current, 1 previous
ctx.bar_index()usizeCurrent zero-based bar index
ctx.time()TimeCurrent engine time
ctx.val(ind)Option<f64>Current indicator value
ctx.val_back(ind, back)Option<f64>Historical indicator value
ctx.book()Option<BookView>L2 snapshot in Order Book backtests and live deployments; None otherwise
ctx.position()i64Whole lots: positive long, negative short, zero flat; a fractional position rounds away from zero (0.15 long reads 1)
ctx.position_lots()f64The exact position in lots: engine order units × symbol().lots_per_unit
ctx.equity()f64Cash plus unrealized P&L, in the account currency
ctx.cash()f64Realized cash, in the account currency
ctx.entry_price()Option<f64>Average open-position entry
ctx.contract()ContractSpec.multiplier (account-currency value of a 1.0 price move per LOT — per contract on futures; may change bar to bar) and .tick_size
ctx.symbol()SymbolInfoInstrument metadata, see below

ctx.symbol() returns SymbolInfo with digits, point, pip_size, tick_size, tick_value (account-currency value of one tick for ONE lot at the current bar), contract_size, lot_min, lot_step, lot_max, lots_per_unit (lots in one engine order unit: the lot step), and base_currency, profit_currency, account_currency. pip_size is the instrument's pip size as its pinned instrument spec defines it; on futures it equals tick_size. The values come from the instrument spec pinned for the run. Bar indicators read the same metadata through ChartCtx::symbol() (and the shared Chart trait) for the chart's instrument, and ctx.contract() on a chart is then the instrument's real contract (value of a 1.0 price move per lot, i.e. contract_size, and its tick). Futures charts use the registered contract. A chart whose instrument has no registered or pinned spec sees the neutral default of the fixed indicator contract (tick 0.01, multiplier 1, USD). Sizing snippets: size risk per lot (tick_value per tick) and round the size down to lot_step. On charts, which have no account, account_currency is the instrument's profit currency and tick_value is in that currency. Futures, with no pinned spec, use the contract: point = pip_size = tick_size, tick_value = tick_size × multiplier, contract_size = multiplier, one order unit = one lot = one contract (lot_min = lot_step = 1, lot_max unbounded) and USD for every currency. Multi-symbol strategies have neither contract() nor symbol(). Old compiled artifacts do not call it and keep running unchanged; an artifact that calls it needs an engine with the rq_h_symbol_info import.

Orders

ctx.buy(size).send();
ctx.sell(size).send();

ctx.buy(size).limit(price).send();
ctx.sell(size).stop(price).send();

ctx.buy(size)
    .sl(stop_price)
    .tp(target_price)
    .trailing(Trail::offset(10.0).activate_after(20.0))
    .oco(group)
    .send();

Order builders support:

Builder methodEffect
.limit(price)Rest as a limit order
.stop(price)Trigger as a stop order
.sl(price)Attach an absolute-price stop loss
.tp(price)Attach an absolute-price take profit
.trailing(trail)Attach a trailing stop
.oco(group)Join an OCO group
.reduce_only()Allow only position reduction
.send()Submit the order

Sizes are LOTS, typed impl OrderSize (every integer type, f32/f64, Lots): ctx.buy(1), ctx.buy(qty_i64), ctx.buy(2.0) and ctx.buy(0.15) all compile. The engine (host) converts lots to engine order units (lots / lots_per_unit, the lot step) and checks lot_min/lot_max. A size is never rounded: a size off the lot step or outside the limits is rejected, send() returns OrderId(0) and the run log records the reason, e.g. order rejected: size 0.155 lots is not a multiple of the lot step 0.01, order rejected: size 0.05 lots is below the minimum lot size 0.1, or, where the lot step is 1, order rejected: size 0.15 is not a whole number; this instrument trades whole lots only. With a lot step of 1 (futures) a decimal that is a whole number trades exactly like the integer and integer sizes behave exactly as before. close_partial follows the same rule and returns None. Integer sizes use the existing order entry and the size_* helpers read the per-lot contract().multiplier, so integer sizes and the sizing helpers run on any engine; a decimal size (f32/f64), ctx.symbol() or ctx.position_lots() needs a current engine (additive rq_h_submit_lots / rq_h_close_partial_lots / rq_h_position_lots imports), and an older engine refuses such an artifact at load. Each distinct rejection reason is logged once per run. Any integer type converts (u64/usize/isize beyond i64 are rejected, never wrapped) as does f32; write a cast size as as i64, not as _, which does not compile, and round a computed size down to the step with (lots / s.lot_step).floor() * s.lot_step. Trade reports keep integer engine units (quantity); the run's pinned instrument spec lot_size is the lots per unit. Multi-symbol buy(symbol, size) stays whole engine units.

Account/order methods:

MethodDescription
ctx.close_position()Close the full position at market
ctx.close_partial(size)Partially close at market (same size rules as buy)
ctx.cancel_order(id)Cancel one pending order
ctx.cancel_all()Cancel all pending orders
ctx.new_oco_group()Create an OCO group
ctx.set_trailing_stop(trail)Attach or replace the position trail
ctx.set_position_sl_tp(sl, tp)Replace position stop/target; use None to clear a side

Trailing stop forms:

Trail::offset(10.0)
Trail::offset(10.0).activate_after(20.0)
Trail::percent(0.02)
Trail::percent(0.02).activate_after(5.0)

Sizing

MethodDescription
ctx.size_pct_equity(fraction)Whole lots (floored, at least 1) with approximately that notional fraction of equity
ctx.size_risk_pct(fraction, stop_distance)Whole lots (floored, at least 1) whose stop loss is approximately that fraction of equity
ctx.size_vol_target(fraction, atr)Whole lots (floored, at least 1) whose one-ATR move is approximately that fraction of equity

Fractions use decimals: 0.01 means 1%.

For a fixed dollar-risk input, include the contract multiplier and round down to whole contracts:

let loss_per_contract = stop_distance.abs() * ctx.contract().multiplier;
let contracts = (risk_usd / loss_per_contract).floor() as i64;

Return zero when one contract exceeds the risk budget; do not silently force a minimum contract and exceed the requested risk.

L2 order book

let Some(book) = ctx.book() else { return };
MethodReturn
book.spread()Option<f64>
book.mid()Option<f64>
book.microprice()Option<f64>
book.imbalance()Option<f64> over the top five levels
book.best_bid() / book.best_ask()Option<f64>
book.best_bid_size() / book.best_ask_size()u32
book.bid(level) / book.ask(level)Option<BookLevel> for levels 0..9
book.depth_bid(n) / book.depth_ask(n)Aggregate visible size
book.walk_buy(size) / book.walk_sell(size)Option<(average_price, levels_walked)>

BookLevel exposes px, sz, and ct.

ctx.book() works in backtests with Order Book fills and in live deployments:

  • Backtests — the book for the current bar. None in bar-only or tick-only backtests.
  • Live — the latest MBP-10 snapshot from Roboquant's own CME feed (not the broker), so it is the same on Tradovate Demo and Live. Snapshots are published at most every 250 ms (up to 4 per second). The live book is subscribed only when the compiled strategy's code reads ctx.book(). There is no book callback; read it from on_bar, on_tick, or on_timer. It is None until the first snapshot arrives after the deployment starts, and while the feed is stale for more than 10 seconds (the book is cleared rather than served frozen).

Always handle None.

Drawings

Creation methods return a builder and must end in .send():

let zone = ctx.plot_zone(high, low)
    .starting_at(start_time)
    .ending_at(end_time)
    .label("range")
    .border("#FFD700")
    .fill("rgba(255,215,0,0.20)")
    .send();

let level = ctx.plot_level(price).label("support").color("#00C853").send();
let hline = ctx.plot_hline(price).label("VWAP").send();
let label = ctx.plot_text("HH", price).starting_at(pivot_time).color("#FFFFFF").send();
MethodDescription
ctx.plot_zone(high, low)Auto-extending price box
ctx.plot_level(price)Bounded horizontal level
ctx.plot_hline(price)Full-width horizontal line
ctx.plot_text(text, price)Text anchored at a time and price
.starting_at(time)Set a historical left edge
.ending_at(time)Set a fixed right edge instead of auto-extension
.label(text)Set a short label
.color(css)Set one color for the drawing
.border(css) / .fill(css)Set independent outline and fill colors
.no_border()Fill-only box (equivalent to .border("transparent"))
ctx.update(id, high, low)Re-price an active zone/level
ctx.freeze(id)Stop extending and keep it
ctx.invalidate(id)Stop extending, fade, and keep history
ctx.delete(id)Erase it from chart history

For a level update, pass the same price twice to ctx.update.

A non-finite price (NaN, INFINITY, NEG_INFINITY) draws nothing: the create is skipped (its handle matches no drawing, no id is consumed) and an update keeps the previous valid anchors. Each skip is reported in the backtest result's drawings_summary.warnings with the call, reason and bar time. Gate drawing on is_finite() (or a "range set" flag) rather than drawing a reset value such as f64::NEG_INFINITY.

Keep every active drawing's DrawingId until it is frozen, invalidated, or deleted. Dropping the handle does not close the drawing; active zones and levels continue extending. Use plot_text for Pine labels—using plot_level as a label creates an unintended horizontal line.

DrawingId does not implement Default. Strategy structs should store active handles as Option<DrawingId>, assign Some(id), and clear them with .take() or = None. Drawing calls take &ctx, so reading ctx inside a drawing builder chain is fine:

self.zone = Some(ctx.plot_zone(high, low).starting_at(ctx.time()).send());

Order builders are different: ctx.buy(n) holds ctx mutably for the whole chain, so hoist any ctx read into a local before ctx.buy/ctx.sell.

For evolving geometry, call ctx.update(id, high, low) while it forms, then ctx.freeze(id) at the actual boundary. For entry/stop/target drawings, close the lifecycle from on_trade when txn.kind == TxnKind::PositionClosed.

Advanced primitives use ctx.draw(DrawingSpecV2::…). Beyond the line/shape constructors, single-point markers and style modifiers are available:

ctx.draw(DrawingSpecV2::dot(t, price).size("large").color("#22c55e"));
ctx.draw(DrawingSpecV2::triangle(t, price).size("tiny").direction("down").color("#ef4444"));
ctx.draw(DrawingSpecV2::vline(t).line_style("dotted"));
ctx.draw(DrawingSpecV2::trendline((t1, p1), (t2, p2)).line_style("dashed"));
ctx.draw(DrawingSpecV2::rectangle((t1, p1), (t2, p2))
    .border_color("transparent")
    .fill_color("rgba(255,215,0,0.25)"));
ModifierApplies toValues
.size(s)dot, triangle"tiny" / "normal" / "large"
.direction(d)triangle"up" (default) / "down"
.line_style(s)any line kind"solid" / "dashed" / "dotted"
.line_width(w)any line kind0.25..16
.border_color(c) / .fill_color(c)rectangleany CSS color; "transparent" border = borderless

Rolling windows

Rolling<T> is a fixed-capacity ring buffer for history a strategy keeps itself (custom averages, pivot windows, lookbacks). Build it in on_init; push is O(1) and never allocates, overwriting the oldest value once full.

#[strategy(name = "Range Breakout")]
pub struct RangeBreakout {
    #[param(default = 20, min = 5, max = 200, title = "Lookback")]
    lookback: i64,
    highs: Rolling<f64>,
}

impl Strategy for RangeBreakout {
    fn on_init(&mut self, _init: &mut Init) {
        self.highs = Rolling::new(self.lookback.max(1) as usize);
    }

    fn on_bar(&mut self, ctx: &mut Ctx, bar: Bar) {
        if let (true, Some(prior_high)) = (self.highs.is_full(), self.highs.max()) {
            if ctx.position() == 0 && bar.close > prior_high {
                ctx.buy(1).send();
            }
        }
        self.highs.push(bar.high);
    }
}
  • Indexing is newest-first, like ctx.bar(back): get(0) is the last value pushed.
  • len, cap, is_empty, is_full, newest, oldest, iter_oldest_first, iter_newest_first, clear.
  • For f64 windows: sum() (0.0 while empty), and mean(), max(), min() returning Option. They are recomputed over the window on each call, so results match a hand-written loop exactly.
  • A Rolling field left at its default (never built with Rolling::new) has capacity 0; pushing into it stops the strategy with an error.

Logging

ctx.log("entered long");
ctx.log(format!("filled {} contracts at {:.2}", qty, price));

Logs are timestamped by the engine and appear in backtest Replay/Logs and live Deployment Logs.

Trade transactions

on_trade receives TradeTxn:

FieldType
kindTxnKind
timeTime
priceOption<f64>
sizeOption<i64>
pnlOption<f64>
reasonOption<CloseReason>
order_idOption<OrderId>

TradeTxn may carry additional optional fields.

size is in engine order units, not lots: lots = size × symbol().lots_per_unit (identical on futures, where one unit is one lot).

TxnKind values: OrderPlaced, OrderFilled, OrderCancelled, PositionOpened, PositionModified, PositionClosed.

Live OrderFilled differs from backtests:

  • It can fire more than once for one order — once per partial fill, each with that fill's size. Backtests fill an order in full, so they emit one OrderFilled per order.
  • After a broker reconcile — at a restart, or when the engine corrects the position after it has disagreed with the broker — an OrderFilled can arrive with no Position* event after it: the fill is already included in ctx.position().
  • order_id is None for fills of bracket stop-loss/take-profit legs and of orders placed manually at the broker.

CloseReason values: Sl, Tp, OppositeFill, Manual, ForceClose.

Multi-symbol reference

Implement MultiStrategy instead of Strategy:

fn on_init(&mut self, init: &mut MultiInit) {
    self.es_ema = init.add("ES", Ema::new(20));
}

fn on_bars(&mut self, ctx: &mut MultiCtx) {
    let Some(es) = ctx.close("ES") else { return };
    // ...
}
MethodDescription
ctx.bar(symbol, back)Bar for one leg
ctx.close(symbol)Latest close for one leg
ctx.position(symbol)Net position for one leg
ctx.entry_price(symbol)Entry price for one leg
ctx.equity()Aggregate account equity
ctx.time()Current merged timestamp
ctx.val(ind) / ctx.val_back(ind, back)Per-symbol indicator value
ctx.buy(symbol, size) / ctx.sell(symbol, size)Market-order shortcuts
ctx.order_buy(symbol, size) / ctx.order_sell(symbol, size)Full per-leg order builders
ctx.close_position(symbol) / ctx.close_partial(symbol, size)Per-leg exits
ctx.cancel_all(symbol)Cancel a leg's resting orders
ctx.set_position_sl_tp(symbol, sl, tp)Replace a leg's stop/target
ctx.set_trailing_stop(symbol, trail)Replace a leg's trailing stop
ctx.log(message)Timestamped log

Multi-symbol strategies run on OHLCV bars only and cannot currently deploy live.

Causality

Hookctx.bar(0)
on_barJust-closed bar
on_tickForming bar as known at the current tick
on_timerForming bar as known at the timer event
on_tradeForming bar as known at the transaction event

Use ctx.bar(1) for the previous completed bar inside tick, timer, and trade hooks. Indicators read during these hooks remain anchored to the last completed bar.

Quote side and account-currency valuation

When the data source quotes bars on the bid side and supplies a per-bar spread, the backtest follows the broker's rules: buys and short covers fill at the ask (bid + that bar's spread), and buy stop/limit orders and short stop-loss/take-profit levels trigger against the ask; sells, long exits and long stops use the bid. For timeframes above one minute the bar's spread is the widest of its one-minute spreads.

When an instrument settles in a currency other than the account's, P&L is valued in the account currency per bar. Fills during a bar's open phase (market orders at the open, intra-bar stops and targets) use the last closed conversion rate; on_bar and the equity mark use the bar's own close. ctx.contract().multiplier, ctx.equity() and ctx.cash() are therefore already in the account currency — never divide by an exchange rate yourself. For instruments without a spread series or conversion, nothing changes.

Contract rolls

On continuous CME series the backtest switches to the next contract at 00:00 UTC on the published roll date. At that point the compiled engine shifts open positions, SL/TP, trailing levels and resting orders by the published price gap, so the roll jump neither fills orders nor books P&L; a held trade's entry is shown at the new-contract-equivalent price and the result includes an account warning. Dated contracts are unaffected. Prices your strategy stored itself are not shifted: re-placing orders from stored absolute levels puts them back at old-contract levels. See Backtesting for the known limits.

Legacy interpreted API

This reference covers the compiled .rq runtime. Existing .py strategies use the legacy rq_backtest.Strategy API and should be maintained in their current format rather than mixing both runtimes in one strategy.