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-unknownWithout 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 lastnbars oldest first,ctx.bar_back(k)the barkbefore 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()andctx.tick_size();ctx.symbol()is the instrument,"EURUSD", andctx.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)andctx.ema_offor another price.ctx.ind("rsi", &[("length", 14.into())])reads any indicator of the library with one line, asctx.inddoes in Python, andctx.reading("bb", &[("length", 20.into())])every line of one:["upper"], and.valuethe 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.chochand each element as a reading,ms.fvg["bull_top"],ms.fib["level_618"], asctx.market_structuredoes in Python.ctx.buy(sl, tp)andctx.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 andctx.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, andctx.spread_back(k)that of the barkbefore. 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: itsdirection,entry_price,stop_priceandbarsheld, asctx.positionin 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, andctx.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.intandp.flagread the run's parameters.ctx.read("trend.pine", "up")reads a value another file of the workspace gives;ctx.set("name", v)andctx.set_flaggive 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()andOrder::sell()fill at the next bar's open.buy_limit,buy_stop,sell_limitandsell_stoprest at a price until they fill, theirbarsrun out,ctx.cancel(name)orctx.cancel_all()takes them back, or another order of theirocogroup fills. A later order of the samenamereplaces one.- Stop and target:
sl_pips,sl_percent,tp_pips,tp_percent. - A stop that moves:
trail_pips,trail_start_pips,break_even_pipsandbreak_even_lock_pips, each also in percent. - Its size:
lots,units,cash,size_percentorrisk_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))orOpen::sell(lots), with.sl_pips,.tp_pipsand.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,pipsandcommission.ctx.modify_position(id, stop, target)sets both levels, as prices, None clears one;ctx.modify_stopandctx.modify_targetset one.ctx.close_by_id(id)andctx.close_part_by_id(id, lots)close now.ctx.account()hasbalance,equity,marginandfree_margin, andctx.bid()andctx.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.