What a Program is

A Program is a strategy written as a Rust function. Orbital compiles it and calls it once per symbol on every bar of the decision bar you pick.

A Program replaces the Entry Rule and Exit Rules nodes, and a graph with either beside it is refused. A Regime Filter, Risk Guard or Indicator beside it is ignored, with a warning. The symbols still come from the Input node and the size from Position Sizing.

Write a Program when the rule needs memory across bars, the session clock, a conviction score to rank on, or a stop no node offers. Otherwise use rule cards, which need no build. A Program cannot be deployed to alerts, but it can run on paper through Trade once a backtest of it has completed, unless it is a daily Program backtested on minute bars; see Deploy a strategy.

Open the editor

In Backtesting, click New strategy, then Write code. Orbital creates a workflow named Untitled code strategy, with an Input node set to SPY, and opens the editor on its Program node. On a Program strategy's page, the Code ↗ tab reopens it.

The program manual, on the right of the editor, lists every function a Program may call.

The shape of a program

The authoring guide's starter
#![no_std]
use orbital_strategy::{close, enter_long, exit, is_flat, is_waiting, param_f64_or, param_key};

// The panic handler a no_std module needs.
orbital_strategy::trap_on_panic!();

// A node parameter, read by name. The node's warmup_bars must cover the
// largest value it can take: that is the deepest offset this program reads.
const LOOKBACK: u64 = param_key("lookback");

// Settings are read once, on the first call, and kept: the node's values do
// not change inside a run, and a host call on every bar is wasted work.
struct Config {
    loaded: bool,
    lookback: i32,
}

// One instance runs per symbol, on one thread, so a static is its memory.
static mut CONFIG: Config = Config { loaded: false, lookback: 0 };

fn average_close(bars: i32) -> f64 {
    let mut sum = 0.0;
    let mut k = 0;
    while k < bars {
        sum += close(k);
        k += 1;
    }
    sum / bars as f64
}

#[no_mangle]
pub extern "C" fn on_bar() {
    // SAFETY: on_bar is the only caller, and nothing re-enters it.
    let cfg = unsafe { &mut *core::ptr::addr_of_mut!(CONFIG) };
    if !cfg.loaded {
        cfg.lookback = param_f64_or(LOOKBACK, 20.0) as i32;
        cfg.loaded = true;
    }
    let average = average_close(cfg.lookback);
    let above = close(0) > average;
    if is_flat() {
        if above && !is_waiting() {
            // The score is this program's own conviction: how far above the average.
            enter_long(close(0) / average - 1.0);
        }
    } else if !above {
        exit();
    }
}
  • Import every function from the crate orbital_strategy, the only crate a program can link besides Rust's core and std. There is no host:: path, and your own extern block fails at link.
  • Export exactly #[no_mangle] pub extern "C" fn on_bar(). A module without it is refused.
  • Under #![no_std] an f64 has no sqrt, ln, exp, powi, powf or floor. To get them, delete the #![no_std] and trap_on_panic!() lines and change nothing else. sin, cos and tan are refused.

When on_bar runs

on_bar never runs on the warm-up bars before the test period. Read history with close(k), high(k) and the rest, with k up to the node's History the program reads; a deeper read traps.

On the first call your statics still hold their initial values. Compute from history on every bar, or fill your buffers from it then: a program that only learns as it is called sits idle at the start of every test.

An entry fills no earlier than the next bar, so entry_price() is NaN on the entry bar: price an entry-bar stop from close(0).

Decisions, stops and sizes

Call set_stop, set_target or set_time_stop on the bar that calls enter, or while a position is open. Each of these ends the run:

  • any of the three called while flat, with no entry expressed or waiting
  • a NaN, infinite or non-positive price
  • a stop at or through the market, or one moved away from it
  • a target not on the profit side of the entry price
  • set_time_stop(0)

set_size_pct(pct) is legal only under the sizing method The program decides, within a range (Code strategies), exactly once on every bar that calls enter, and never otherwise. The build's check applies no sizing, so a mistake here ends the run instead.

The Program card

FieldWhat it sets
Module referenceThe compiled module. A build fills it.
Host interfaceThe interface the module was built against: Version 4 (current) after a build. An older module keeps running under its own.
Decision barDaily bars — one decision per trading day by default, or an intraday bar from 3-hour down to 1-minute bars — about 391 decisions per session.
History the program reads (bars of the decision bar)The deepest bar back the program reads: 64 on an hourly program is 64 hourly windows, not 64 days. At least what the module was built with.
Instruction budget per barBlank for the default, 10,000,000 instructions per bar.
ParametersValues the program reads at start, as a JSON object such as { "lookback": 20 }.
SourceThe Rust source. Open code editor edits and builds it.

Read Parameters by name with param_f64_or or param_bool_or and param_key("name"); a bare param_f64 on a missing parameter traps. Make the history cover the largest value a parameter can take. Changing Parameters needs no rebuild, since only the source is hashed.

Build and test

  1. Set the history first

    Close the editor, which hides the node's settings, and fill History the program reads (bars of the decision bar) on the Program node. While it is blank, Problems reads Not built — the node is missing a setting.

  2. Build

    Click Build ⌘⇧B, which saves first. On success the status line under the editor reads Built. Otherwise Problems lists each compiler message; click one to jump to its line.

  3. Test

    Test builds and runs the check scenarios: flat into a rise, flat into a fall and already long into a fall. The verdict is Decided, No intents, Not called or Failed, and each scenario shows its Fuel, the heaviest bar's cost.

  4. Run it

    Click Run… on the strategy's page. While the text differs from the last build, the status line reads Changed since the last build — build before running, and a run refuses it by name, as in Program 'program' changed since its last build — build the program before running it.

Fuel and limits

LimitValue
SourceAt most 256 KiB, with no NUL byte
History the program reads0 to 20,000 bars of the decision bar

Leave Instruction budget per bar blank. A budget near or below the check's Fuel figure ends a run on the first bar that costs more. If a run runs out of a budget set below the default, clear the field and run it again.

A Program from Claude or an agent

Claude, through the Orbital AI connector, and an agent use the editor's build in a different order.

  1. Save before the build

    Only a build can set Module reference, so the graph is saved first without it, with Decision bar, History the program reads and Parameters set on the Program node.

  2. Build

    The build reads the history off the saved node and refuses without it. The compiler's diagnostics come back verbatim, with the check and up to three warnings:

    • a budget below the default, when the check's heaviest bar uses more than a quarter of it
    • a program that expressed nothing in either check scenario that starts flat
    • parameters on the node when the module reads none
  3. The source on the node

    Claude's build saves its source on the node; an agent's does so only on a workflow it created. Elsewhere, if the node's source differs from the agent's build, a run refuses the program until a person rebuilds it in the editor.

A build from Claude or an agent is refused while a deployment that is not stopped runs the strategy; see What a running deployment locks.