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 type | Dashboard control |
|---|---|
i64 | Integer input |
f64 | Decimal input |
bool | On/off control |
Numeric field + options | Fixed-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
| Field | Type | Meaning |
|---|---|---|
time | Time | Bar open timestamp |
open, high, low, close | f64 | OHLC prices |
volume | f64 | Bar volume |
Tick
| Field | Type | Meaning |
|---|---|---|
time | Time | Trade timestamp |
price | f64 | Trade price |
size | f64 | Trade size |
side | Aggressor | Buy, 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
| Method | Description |
|---|---|
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(...):
| Group | Constructors |
|---|---|
| Moving averages | Sma::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) |
| Momentum | Rsi::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/range | Atr::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) |
| Volume | Obv::new(), Vwap::new(), Ad::new(), Adosc::new(fast, slow) |
| Statistics/trend | Zscore::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
| Method | Return | Description |
|---|---|---|
ctx.bar(back) | Option<Bar> | 0 is current, 1 previous |
ctx.bar_index() | usize | Current zero-based bar index |
ctx.time() | Time | Current 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() | i64 | Whole lots: positive long, negative short, zero flat; a fractional position rounds away from zero (0.15 long reads 1) |
ctx.position_lots() | f64 | The exact position in lots: engine order units × symbol().lots_per_unit |
ctx.equity() | f64 | Cash plus unrealized P&L, in the account currency |
ctx.cash() | f64 | Realized 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() | SymbolInfo | Instrument 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 method | Effect |
|---|---|
.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:
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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 };
| Method | Return |
|---|---|
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.
Nonein 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 fromon_bar,on_tick, oron_timer. It isNoneuntil 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();
| Method | Description |
|---|---|
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)"));
| Modifier | Applies to | Values |
|---|---|---|
.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 kind | 0.25..16 |
.border_color(c) / .fill_color(c) | rectangle | any 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
f64windows:sum()(0.0 while empty), andmean(),max(),min()returningOption. They are recomputed over the window on each call, so results match a hand-written loop exactly. - A
Rollingfield left at its default (never built withRolling::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:
| Field | Type |
|---|---|
kind | TxnKind |
time | Time |
price | Option<f64> |
size | Option<i64> |
pnl | Option<f64> |
reason | Option<CloseReason> |
order_id | Option<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 oneOrderFilledper order. - After a broker reconcile — at a restart, or when the engine corrects the position after it has disagreed with the broker — an
OrderFilledcan arrive with noPosition*event after it: the fill is already included inctx.position(). order_idisNonefor 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 };
// ...
}
| Method | Description |
|---|---|
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
| Hook | ctx.bar(0) |
|---|---|
on_bar | Just-closed bar |
on_tick | Forming bar as known at the current tick |
on_timer | Forming bar as known at the timer event |
on_trade | Forming 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.
Related docs
- AI strategy authoring reference — compact compile and semantic checklist
- Strategies — complete authoring workflow
- Backtesting — fill and data behavior
- Live deployment — live capability and safety matrix