Rust

DocumentationWriting strategies

Rust

A strategy can be written in Rust, with the whole language: your own structs, enums and traits, closures, iterators, Vec, HashMap and the rest of the standard collections, and the float maths of f64. It is built to WebAssembly and runs in a sandbox, on the Python engine and on the Rust core, the same compiled strategy on both. It cannot reach out: no files, no network, no clock, no crates beyond orionquant.

Starting one

Open Build, press the plus in the Strategies rail and choose Rust file. A new Rust strategy starts as the built in Moving Average Cross written in Rust, which trades exactly as the Python one. Building needs Rust on this machine, with its WebAssembly target:

rustup target add wasm32-unknown-unknown

Without it, Rust is greyed in the menu with the reason.

Rust strategies are part of Max and Enterprise. A run on the server builds the strategy there, with the same compiler and the same refusals, so it does not need Rust on this machine; the first run of a new source spends a few seconds building it.

A strategy

rustuse orionquant::prelude::*;

pub struct Cross {
    fast: usize,
    slow: usize,
}

impl Strategy for Cross {
    fn new(p: &Params) -> Self {
        Cross { fast: p.int("fast", 10), slow: p.int("slow", 30) }
    }

    fn on_bar(&mut self, ctx: &mut Ctx) {
        let (Some(f), Some(s)) = (ctx.sma(self.fast), ctx.sma(self.slow)) else { return };
        if f > s { ctx.buy(20.0, 40.0) } else { ctx.sell(20.0, 40.0) }
    }

    fn params() -> Vec<Param> {
        vec![Param::new("fast", 10.0, 2.0, 200.0, 1.0), Param::new("slow", 30.0, 5.0, 400.0, 1.0)]
    }
}

export!(Cross);

new runs once a run with its parameters, on_bar once a bar after it closes. What params declares is what the run form shows and the optimizer sweeps, each with its default, minimum, maximum and step. export! names the type that runs.

What a strategy reads and does

  • ctx.bar(), ctx.close(), ctx.now(): the bar that just closed, its close, its open time in microseconds since 1970 UTC.
  • ctx.bars(n) the last n bars oldest first, ctx.bar_back(k) the bar k before the last. At least the last 20,000 bars are always there; ctx.len() is how many are held, not a count of the run's bars.
  • ctx.pip() and ctx.tick_size(); ctx.symbol() is the instrument, "EURUSD", and ctx.contract_size() its units per lot.
  • ctx.sma(p), ctx.ema(p), ctx.rsi(p), ctx.atr(p), the engine's own indicators, None until warmed up; ctx.sma_of(Source::High, p) and ctx.ema_of for another price.
  • ctx.ind("rsi", &[("length", 14.into())]) reads any indicator of the library with one line, as ctx.ind does in Python, and ctx.reading("bb", &[("length", 20.into())]) every line of one: ["upper"], and .value the first. Computed in the strategy, the numbers Python reads to the bit (alma, hv and chop may differ in the last bit). An unknown id or a bad input stops the run and says why.
  • ctx.market_structure(&[("length", 3.into())]) reads the whole market structure group: ms.trend, ms.bos, ms.choch and each element as a reading, ms.fvg["bull_top"], ms.fib["level_618"], as ctx.market_structure does in Python.
  • ctx.buy(sl, tp) and ctx.sell(sl, tp) go long or short, or reverse, with a stop and a target in pips, 0 for none. ctx.close_position() closes.
  • ctx.close_part(0.5) closes a share of the position, here half of it, ctx.close_lots(0.2) that many lots and ctx.close_units(u) that many units. The rest stays open. As much as is held or more closes all of it.
  • ctx.spread() is the ask less the bid at the bar's close, in price, and ctx.spread_back(k) that of the bar k before. A bar without its bid and ask reads the symbol's average spread; without one, reading it refuses the run.
  • ctx.position() is what the engine holds after this bar's fills, None when flat: its direction, entry_price, stop_price and bars held, as ctx.position in Python. ctx.bar().ask_low() and the others, bid open to ask close, are the bar's bid and ask, None on data without them.
  • ctx.news_window(Impact::High, 30, 15) asks whether the bar is inside a news window, as in Python; ctx.news_window_for(impact, &["USD"], 30, 15) names the currencies, and ctx.news_window_of(impact, &[NewsKind::Rate, NewsKind::Jobs], &["USD"], 30, 15) counts only those kinds of release (an empty list of currencies is the instrument's own).
  • p.get("name", default), p.int and p.flag read the run's parameters.
  • ctx.read("trend.pine", "up") reads a value another file of the workspace gives; ctx.set("name", v) and ctx.set_flag give one.

Orders fill at the next bar's open, as every strategy's do, and an entry has the run's lot size unless its order says otherwise.

Orders with everything

ctx.place places an Order with all a Python strategy can say about one:

rustctx.place(Order::buy().sl_percent(0.5).tp_pips(40.0).risk_percent(1.0).trail_pips(15.0));
ctx.place(Order::sell_limit(ctx.bar().high + 5.0 * ctx.pip()).bars(4).name("fade").oco("pair"));
ctx.cancel("fade");
  • Order::buy() and Order::sell() fill at the next bar's open. buy_limit, buy_stop, sell_limit and sell_stop rest at a price until they fill, their bars run out, ctx.cancel(name) or ctx.cancel_all() takes them back, or another order of their oco group fills. A later order of the same name replaces one.
  • Stop and target: sl_pips, sl_percent, tp_pips, tp_percent.
  • A stop that moves: trail_pips, trail_start_pips, break_even_pips and break_even_lock_pips, each also in percent.
  • Its size: lots, units, cash, size_percent or risk_percent, which needs a stop.

Robots: many positions at once

A strategy exported with export_robot! instead of export! is a robot. It trades like a cBot on a hedging account: as many positions as it opens, in both directions, filled the moment it asks and read back at once, so it can open a position and move the basket's target on the same bar.

rustlet held = ctx.positions();
if held.is_empty() {
    ctx.open_position(Open::buy(0.1).tp_pips(15.0).label("buy"));
    ctx.open_position(Open::sell(0.1).tp_pips(15.0).label("sell"));
}
for p in held.iter().filter(|p| p.labelled("buy") && p.pips < -30.0) {
    ctx.close_by_id(p.id);
}
  • ctx.open_position(Open::buy(lots)) or Open::sell(lots), with .sl_pips, .tp_pips and .label, opens now: a long at the ask, a short at the bid. It returns the position, or None when the account refuses it.
  • ctx.positions() is what is open: id, label, direction, lots, entry_price, entry_time, stop_price, target_price, profit, pips and commission.
  • ctx.modify_position(id, stop, target) sets both levels, as prices, None clears one; ctx.modify_stop and ctx.modify_target set one.
  • ctx.close_by_id(id) and ctx.close_part_by_id(id, lots) close now.
  • ctx.account() has balance, equity, margin and free_margin, and ctx.bid() and ctx.ask() are the price it trades at.

The rules are the hedging account's of a Python strategy, on both engines: see Strategies API. A robot does not use buy, sell, place or close_position. A robot runs on the Rust core unless its workspace picks the Python engine, and an optimization of it runs its passes on all the core's threads, which makes thousands of runs an hour.

Rust Grid Robot in the catalog is a whole one to start from: a grid on both sides with one basket target a side.

Several files

The other .rs files of the workspace are modules of the strategy: write mod maths; in the file that runs and use maths::mean; to use what it holds. A module's name is lower case letters, digits and underscores.

The sandbox and what is refused

A strategy that does too much work on one bar, a loop that never ends, stops the run with a sentence saying so. A panic stops it with the panic's message. Memory is capped at 256 MB.

Building runs Rust's compiler on your machine, or on the server for a server run, so what reads the machine while it builds is refused before it starts, with the file and the line: include_str!, include_bytes!, include!, env! and option_env!, and the attributes #[path], #[link] and #[debugger_visualizer], all refused by name anywhere outside a comment or a string, since a macro can make an attribute of a name it is handed. So a variable, a field or a function of your own named path, link or debugger_visualizer is refused too: rename it, to route or chain say. A longer name that contains one, like file_path, is fine. extern crate is refused, and so is lifting the compiler's limits (recursion_limit, type_length_limit, long_running_const_eval). A build may take two minutes on the server, and a strategy is at most 512 KB of source. A strategy someone else wrote, from the marketplace or a repository, cannot read your files.

Not yet

Rust strategies export to no other platform; the MetaTrader 5 conversion by the assistant takes them as it takes Python.

OrionQuant