# Program strategies in Rust

> Write a strategy as a Rust program: the editor, Build and Test, the on_bar function and the host interface, the Program node's settings, fuel and limits, and how Claude or an agent builds one.

Source: https://orbitalaip.com/docs/backtesting/program-strategies

> [!IMPORTANT] Disclaimer
> Backtest results are hypothetical and do not predict future performance. Nothing here is investment advice: Orbital computes the rules you supplied and recommends nothing. Orbital is not a registered investment adviser or broker-dealer. Costs are as modelled by the chosen cost profile and may differ from live trading.

## What a Program is

A [Program](https://orbitalaip.com/docs/glossary#program) is a [strategy](https://orbitalaip.com/docs/glossary#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](https://orbitalaip.com/docs/backtesting/rule-card-strategies), 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](https://orbitalaip.com/docs/trade/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

```rust title="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](https://orbitalaip.com/docs/glossary#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

| Field | What it sets |
| --- | --- |
| Module reference | The compiled module. A build fills it. |
| Host interface | The interface the module was built against: Version 4 (current) after a build. An older module keeps running under its own. |
| Decision bar | Daily 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 bar | Blank for the default, 10,000,000 instructions per bar. |
| Parameters | Values the program reads at start, as a JSON object such as { "lookback": 20 }. |
| Source | The 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.**

> [!NOTE] The check runs at your decision bar
> The check uses the saved node's **Decision bar** and **Parameters**. Each scenario decides only 8 bars, so a selective program can show **No intents** without being wrong.

## Fuel and limits

| Limit | Value |
| --- | --- |
| Source | At most 256 KiB, with no NUL byte |
| History the program reads | 0 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](https://orbitalaip.com/docs/backtesting/manage-and-delete-strategies#what-a-running-deployment-locks).
