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

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(...):

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 when the run includes book data
ctx.position()i64Positive long, negative short, zero flat
ctx.equity()f64Cash plus unrealized P&L
ctx.cash()f64Realized 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 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

Account/order methods:

MethodDescription
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

MethodDescription
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 };
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() 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();
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.

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

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>

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 };
    // ...
}
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.

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.