RoboQuant
RoboQuant engine

RoboQuant Strategies

New RoboQuant strategies are compiled. You write a .rq entry source (plus optional sibling .rs helper modules), RoboQuant verifies it and generates a .rqc artifact, and that artifact runs in backtests, optimization, and live trading.

Minimal strategy

use rq_sdk::prelude::*;

#[strategy(name = "EMA Crossover")]
pub struct EmaCross {
    #[param(default = 12, min = 2, max = 200, title = "Fast EMA")]
    fast: i64,
    #[param(default = 26, min = 2, max = 400, title = "Slow EMA")]
    slow: i64,

    fast_ema: Ind,
    slow_ema: Ind,
}

impl Strategy for EmaCross {
    fn on_init(&mut self, init: &mut Init) {
        self.fast_ema = init.add(Ema::new(self.fast.max(2) as usize));
        self.slow_ema = init.add(Ema::new(self.slow.max(2) as usize));
    }

    fn on_bar(&mut self, ctx: &mut Ctx, _bar: Bar) {
        let (Some(fast), Some(slow)) =
            (ctx.val(self.fast_ema), ctx.val(self.slow_ema))
        else {
            return;
        };

        if ctx.position() == 0 && fast > slow {
            ctx.buy(1).send();
        } else if ctx.position() > 0 && fast < slow {
            ctx.close_position();
        }
    }
}

File lifecycle

A compiled strategy normally has these files:

ema_crossover.rq          source — edit this
ema_crossover.rqc         compiled artifact — generated
ema_crossover.params.py   parameter metadata — generated

Use this workflow in Strategies → Code:

  1. Edit the .rq source.
  2. Click Save.
  3. Click Compile.
  4. Fix any compiler diagnostics and compile again.
  5. Run the resulting artifact from Backtest, Optimize, or Deployments.

The Compile action saves the current editor buffer before building, so the artifact corresponds to the stored source. Do not edit .rqc or .params.py files by hand; the next compile replaces them.

Prefer one .rq file for small strategies. Larger strategies may use sibling .rs modules declared by the entry (mod signals;); helper names must be simple module stems. External packages, filesystem access, network access, async code, and unsafe code are not available inside the strategy sandbox.

Parameters

Put configurable values on the strategy struct with #[param(...)]:

#[strategy(name = "Opening Range Breakout")]
pub struct Orb {
    #[param(default = 30, min = 5, max = 120, step = 5, title = "Range minutes")]
    range_minutes: i64,

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

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

    #[param(default = 20, options = [10, 20, 50], title = "Lookback")]
    lookback: i64,
}

Supported parameter fields are i64, f64, and bool. Numeric options render a fixed-choice control and cannot be combined with min, max, or step.

Parameter metadata is embedded in the artifact. The dashboard uses it to build Backtest, Optimize, and Deployments controls, so the same names and bounds apply everywhere without recompiling for each value.

Saved settings and .set files

The Configs menu beside strategy inputs is shared by Backtest, Optimize, Deployments, live parameter editing, and RoboCharts.

  • Save current as config stores a named cloud config for this strategy.
  • Save .set file writes presets/<name>.set into the strategy file tree and downloads the same portable file.
  • Upload .set file validates the file against the strategy parameter schema, writes it under presets/, and applies its values.
  • Project .set files remain loadable from the Configs menu and are mirrored into the workspace with the rest of the strategy tree.

RoboQuant accepts MetaTrader-style fixed values and optimization-range lines:

range_minutes=30||5||5||120||N
risk_fraction=0.01||0.001||0.001||0.05||N
allow_shorts=true||false||0||true||N

The first field is the value used by a normal run. The remaining fields are the optimization start, step, stop, and Y/N selection flag. Strategy metadata supplies the types because .set files do not carry them. Unknown parameter names are skipped with a warning; invalid types, bounds, or options are rejected before the settings are applied.

Settings files contain strategy inputs only. Symbols, dates, capital, broker accounts, credentials, and platform environment values are intentionally not included.

Lifecycle hooks

Single-symbol strategies

Implement Strategy for one instrument:

HookWhen it runsTypical use
on_init(&mut self, init: &mut Init)Once before executionRegister indicators and timers
on_bar(&mut self, ctx: &mut Ctx, bar: Bar)At each bar closeMost trading logic
on_tick(&mut self, ctx: &mut Ctx, tick: Tick)On each trade print when opted inIntrabar signals and execution
on_timer(&mut self, ctx: &mut Ctx, now: Time)At the timer intervalScheduled tick-mode work
on_trade(&mut self, ctx: &mut Ctx, txn: TradeTxn)Order, fill, position, and close eventsJournaling and state transitions
wants_ticks(&self) -> boolRuntime capability flagReturn true when using on_tick

Tick strategies must opt in explicitly:

impl Strategy for TimeEntry {
    fn on_init(&mut self, init: &mut Init) {
        init.set_timer(60.0);
    }

    fn wants_ticks(&self) -> bool {
        true
    }

    fn on_tick(&mut self, ctx: &mut Ctx, tick: Tick) {
        if ctx.position() == 0 && tick.time.hhmm() == 1559 && tick.time.second() >= 58 {
            ctx.buy(1).send();
        }
    }
}

Defining on_tick without returning true from wants_ticks compiles, but the strategy does not subscribe to ticks.

Multi-symbol strategies

Use MultiStrategy for pairs, spreads, and baskets. on_bars runs once per merged timestamp, and every market or account method is addressed by symbol name.

use rq_sdk::prelude::*;

#[strategy(name = "ES NQ Pair")]
pub struct Pairs {
    #[param(default = 1, min = 1, max = 10, title = "Contracts")]
    qty: i64,
    es_sma: Ind,
}

impl MultiStrategy for Pairs {
    fn on_init(&mut self, init: &mut MultiInit) {
        self.es_sma = init.add("ES", Sma::new(20));
    }

    fn on_bars(&mut self, ctx: &mut MultiCtx) {
        let (Some(es), Some(nq), Some(es_sma)) =
            (ctx.close("ES"), ctx.close("NQ"), ctx.val(self.es_sma))
        else {
            return;
        };

        if ctx.position("ES") == 0 && es > es_sma && nq > 0.0 {
            ctx.order_buy("ES", self.qty).send();
            ctx.order_sell("NQ", self.qty).send();
        } else if ctx.position("ES") != 0 && es < es_sma {
            ctx.close_position("ES");
            ctx.close_position("NQ");
        }
    }
}

Multi-symbol strategies are bar-mode only. They support per-leg indicators, market/limit/stop entries, SL/TP, trailing stops, partial exits, and aggregate equity in backtests and optimization. Live multi-symbol deployment is not available yet.

Indicators and warmup

Register indicators once in on_init, store their Ind handles on the struct, and read values through ctx.val:

fn on_init(&mut self, init: &mut Init) {
    self.atr = init.add(Atr::new(14));
    self.daily_ema = init.add(Ema::new(20).timeframe("1D"));
}

fn on_bar(&mut self, ctx: &mut Ctx, _bar: Bar) {
    let (Some(atr), Some(daily_ema)) =
        (ctx.val(self.atr), ctx.val(self.daily_ema))
    else {
        return;
    };
    // trading logic
}

None means the indicator has not warmed up. Do not unwrap indicator values.

Every registered indicator is also 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 the canonical threshold lines. There is no separate plot call — registration is the plot. Do not draw indicator lines with the drawing tools (plot_hline stamps one static level; per-bar segments hit the drawing cap).

Orders and sizing

Every configured order ends with .send():

ctx.buy(1).send();
ctx.sell(2).limit(6050.0).send();
ctx.buy(1).stop(6100.0).sl(6075.0).tp(6150.0).send();
ctx.buy(1).trailing(Trail::offset(10.0).activate_after(20.0)).send();

ctx.close_position();
ctx.close_partial(1);
ctx.cancel_all();

Use the engine's sizing helpers instead of reproducing contract math:

let qty = ctx.size_risk_pct(0.01, stop_distance); // risk about 1% of equity
let qty = ctx.size_pct_equity(0.10);              // about 10% notional
let qty = ctx.size_vol_target(0.01, atr);          // 1-ATR move about 1%

Sizes are contracts. ctx.contract() exposes the instrument's multiplier and tick_size.

Drawings and logs

Drawings are part of the backtest result and Replay stream:

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

ctx.update(zone, new_high, new_low);
ctx.freeze(zone);      // keep it and stop extending
ctx.invalidate(zone);  // keep it, stop extending, and fade it
ctx.delete(zone);      // erase it from chart history

ctx.log("opening range complete");

Create a drawing once and update it. Creating a new zone every bar produces stacked drawings and makes replay harder to read.

Causality rules

  • In on_bar, bar and ctx.bar(0) are closed.
  • In tick and timer hooks, ctx.bar(0) is the causal forming-bar snapshot; use ctx.bar(1) for the last closed bar.
  • Indicator values in tick hooks are anchored to the last closed bar.
  • Higher-timeframe values are delayed until that higher-timeframe bar has closed.
  • Orders triggered by historical events never see a future price.

Legacy .py strategies

Existing interpreted .py strategies remain supported for maintenance and backtesting. Do not put Python code in a .rq file, create a parallel compiled copy inside the same strategy, or add the former KernelStrategy/numba twin pattern. New strategies should be authored directly in the compiled .rq format.

Common mistakes

MistakeFix
Editing .rqc or .params.pyEdit .rq, then compile again
Running after a source edit without compilingSave and click Compile first
Forgetting .send()Finish every order or drawing builder with .send()
Reading an indicator without a warmup guardMatch Some(value) and return on None
Using i64 as a bar indexConvert validated lookbacks to usize at the call site
Subtracting from bar_index() before checking the rangeGuard the index before subtraction
Implementing on_tick without wants_ticksReturn true from wants_ticks()
Expecting on_bars to run liveMulti-symbol deployment is not available yet
Importing external packages or using I/OKeep strategy logic inside the provided SDK surface