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 |
Every registered indicator is plotted on the backtest chart automatically: price-scale series (EMA, VWAP, bands) render as overlays on the candles, and oscillators (RSI, MACD, stochastic) get their own pane below the chart with their threshold lines. Registration is the plot — there is no separate plot call, and drawing tools should not be used to trace indicator lines.
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 when the run includes book data |
ctx.position() | i64 | Positive long, negative short, zero flat |
ctx.equity() | f64 | Cash plus unrealized P&L |
ctx.cash() | f64 | Realized cash |
ctx.entry_price() | Option<f64> | Average open-position entry |
ctx.contract() | ContractSpec | .multiplier and .tick_size |
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 |
Account/order methods:
| Method | Description |
|---|---|
ctx.close_position() | Close the full position at market |
ctx.close_partial(size) | Partially close at market |
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) | Contracts with approximately that notional fraction of equity |
ctx.size_risk_pct(fraction, stop_distance) | Contracts whose stop loss is approximately that fraction of equity |
ctx.size_vol_target(fraction, atr) | Contracts 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() returns None unless the backtest includes L2 data.
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.
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 |
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> |
TxnKind values: OrderPlaced, OrderFilled, OrderCancelled, PositionOpened, PositionModified, PositionClosed.
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.
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