closes #272 A member fault (no-data, bind, run, or a caught panic) is now a recorded per-cell outcome instead of aborting the whole campaign and discarding every already-computed cell. The incident that motivated this (a 22-instrument campaign lost ~36 healthy cells ~6.7 min in because Copper had an archive gap) now completes: the healthy cells persist, the gap cell is recorded as failed, and the run exits 3. Direction (owner decision 2026-07-14): run to completion and report compromised results; no coverage preflight, no window synthesis. Containment granularity: - The CELL for a sweep-stage member fault (a grid hole structurally compromises winner selection, so the whole cell fails). - The FOLD for a walk_forward member fault (independent time windows): the surviving folds pool into the family, failed folds are recorded as StageRealization.window_faults, and the summary names the ratio. - aura-registry: additive CellFault / CellFaultKind (closed: no_data|bind|run|panic|window) / WindowFault / CellCoverage, plus fault/coverage fields on CellRealization and window_faults on StageRealization — all serde-default-skipped, so pre-#272 campaign_runs lines parse and round-trip byte-identical. - aura-campaign: run_cell returns a fault-annotated CellRealization instead of Err (execute's accumulate-then-append-once tail is unchanged and now persists every healthy cell + the one run record); a `contain` split keeps ExecFault::Registry and doc-shape preflight faults global while Member/Window become per-cell/per-fold. Member panics are caught with catch_unwind(AssertUnwindSafe) at all three member-run sites (sweep IS/OOS) and recorded as MemberFault::Panic — a member panic no longer aborts the process. The wf stage partitions Registry faults (global) from Member/Window (per-fold) and filters faulted-fold placeholders (the "faulted-member-placeholder" broker sentinel) out of the persisted family. - aura-cli: exec_fault_prose gains the Panic arm; CliMemberRunner::window_coverage derives effective bounds + interior gap months from the #264 archive primitives; present_campaign prints per-cell failure notes + a completion summary and threads the failed-cell count; a run with >=1 failed cell exits 3 ("completed with failed cells") uniformly across `aura campaign run` and the dissolved sweep/walkforward/mc/generalize verbs (exit_on_campaign_result). Usage stays 2, refused-before-running stays 1, clean stays 0. Tests: the global-abort pins flip to containment (execute + the two wf fault tests → fold-containment + all-folds-fail-the-cell); new panic-containment tests on both the sweep path (PanicRunner) and the wf path (this commit adds the wf mirror the loop left uncovered); a new gapped-archive e2e (one covered cell + one gap cell → exit 3); the ~14 CLI exit-1 pins move to the exit-3 register; a pre-#272-line byte-identical round-trip guard. Suite: cargo test --workspace green (1309 tests, 0 failed); clippy clean. Decision log: #272 comments (fork rationale, the fold Registry/Member split, the placeholder sentinel, uniform exit-3). Follow-up (minor, not blocking): the plan under-scoped Task 1 to aura-registry though the additive fields also touch aura-campaign's exec.rs literals — the loop absorbed it mechanically; a future plan for a cross-crate additive-field change should scope every crate's construction sites in the first task.
aura
aura is a "game engine for traders." It is a Rust framework — and a playground — for authoring trading strategies as composable dataflow nodes, backtesting them deterministically and massively in parallel, validating them (sweep / Monte-Carlo / walk-forward), and freezing a validated strategy into a standalone bot with a broker connection. Its differentiator is the World: a program that constructs and orchestrates whole families of backtests, not the single run.
This README is a discoverability map of the shipped aura CLI — what commands
exist and what they do. It deliberately keeps per-flag detail light; the two
authoritative sources for exact syntax are always:
aura <command> --help— the exhaustive flag grammar.- The design ledger (
docs/design/INDEX.md) and glossary (docs/glossary.md) — the concepts and invariants behind the surface.
Build & run
aura is a Cargo workspace. Build everything and you get one binary named aura:
cargo build --workspace # binary at target/debug/aura
cargo build --workspace --release # optimized, at target/release/aura
Invoke it as aura <command> … (examples below use the plain name).
Concepts in one breath
- Blueprint — a serialized signal graph as data: a param-generic
price → biasnode graph. A blueprint file (blueprint.json) is the unit a downstream consumer loads and drives verbs over. - Open vs. closed — a blueprint with unbound (free) numeric knobs is open; one with every knob bound is closed. Different verbs want different states (see each verb below).
- Family — the set of runs one verb produces over a blueprint (a sweep grid, a Monte-Carlo seed set, a walk-forward window sequence). Families are persisted in a content-addressed store and can be listed, ranked, and reproduced.
- Axis — one named, sweepable knob of a blueprint (e.g.
graph.fast.length), bound with a comma-separated value list. A gang fuses several sibling knobs into one axis (one value drives all members).
Running & orchestrating a loaded blueprint
These verbs all take the blueprint file as their first positional argument.
Most drive a family of runs over it; graph renders its structure so a mis-wire
is visible before a run. walkforward, mc, and generalize share
sweep's generic grammar — the blueprint positional plus --real and a
repeatable --axis <name>=<csv> (generalize takes --real <SYM1,SYM2,…>, at
least two instruments, with a single value per axis) — see --help.
| Command | Purpose |
|---|---|
aura run <bp.json> |
Run one backtest of a closed blueprint and print its report. An open (free-knob) blueprint is refused with a clean error — bind it, or use sweep. |
aura graph <bp.json> |
Render the blueprint's structure as an interactive HTML DAG so a mis-wire is visible before a run. Omit the file to render the built-in sample; a named-but-unreadable file is a usage error. |
aura sweep <bp.json> --list-axes |
Discover the blueprint's open, sweepable knobs. Prints each as <name>:<kind>. Run this first to learn the axis names. |
aura sweep <bp.json> --axis <name>=<csv> [--axis …] |
Run a grid family over the named axes and persist it. |
aura mc <bp.json> --seeds <n> |
Run a synthetic Monte-Carlo family of n seeded realizations. Wants a closed blueprint (the inverse of sweep). |
aura mc <bp.json> --real <sym> --axis <name>=<csv> [--axis …] |
Run a Monte-Carlo R-bootstrap campaign over recorded data — driven through the same generated-campaign pipeline as sweep; --block-len/--resamples/--seed tune the bootstrap. |
aura walkforward <bp.json> --axis <name>=<csv> [--axis …] |
Run an in-sample-refit walk-forward family — rolling optimize + out-of-sample test across windows, stitched into one verdict + parameter stability. --select chooses the per-window objective. |
aura runs families |
List every persisted family (id, kind, member count). |
aura runs family <id> [rank <metric>] |
List one family's members, optionally ranked best-first by an R metric (e.g. sqn_normalized, expectancy_r). |
aura reproduce <family-id> |
Re-derive every member of a persisted family from the content-addressed store and check it is bit-identical (the C18/C1 determinism guarantee). |
Important contract — every open knob is mandatory. On sweep and
walkforward, the knobs enumerated by --list-axes are all required: you must
supply an --axis for each open knob. There is no default value — pin a knob you
don't want to vary with a single-value axis (--axis name=4). Omitting one is a
clean error naming the missing knob, not a silent default.
Use aura <cmd> --help for the full data-window (--real/--from/--to),
naming (--name), and selection (--select) flags.
Authoring & introspecting topology
A blueprint is authored declaratively from a JSON op-list: an ordered list
of by-identifier construction ops replayed against the standard node vocabulary.
Adding a new node type to that vocabulary is Rust, not JSON — see
docs/authoring-guide.md, §0, "Authoring a new node in Rust".
| Command | Purpose |
|---|---|
aura graph build |
Read an op-list document on stdin, build the blueprint, and print its canonical JSON to stdout (no trailing newline — it is the content-addressed artifact). |
aura graph introspect --vocabulary |
List every node type available to op-lists. |
aura graph introspect --node <T> |
Show one type's input ports, output fields, and param paths (with the bind-value form). |
aura graph introspect --unwired |
Read a partial op-list on stdin and list its still-open interior slots. |
aura graph introspect --content-id |
Read an op-list on stdin and print the SHA-256 content id of the blueprint it would build. |
aura graph introspect --identity-id |
Read an op-list on stdin and print the SHA-256 identity id — the topology's debug-name-blind id, so two documents (or an op-script and a Rust-built blueprint) expressing the same topology print the same id. Combinable with --content-id (both ids, one per line, content id first). |
Op-list shape
Each element is one op, tagged by "op". Node params are bound with the typed
Scalar form — a tagged, capitalized-kind object: {"I64": 5}, {"F64": 1.5}, {"Bool": true}. A worked example (an SMA-crossover → bias signal):
[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"add","type":"Bias"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"connect","from":"fast.value","to":"sub.lhs"},
{"op":"connect","from":"slow.value","to":"sub.rhs"},
{"op":"connect","from":"sub.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"}
]
Piping this document into aura graph build emits a blueprint.json you can
then feed to run / sweep / mc / walkforward above:
aura graph build < crossover.ops.json > crossover.bp.json
aura sweep crossover.bp.json --list-axes
The op kinds are source, input, add, feed, connect, expose, and
gang. See
aura graph introspect --node <T> for a type's exact ports and the op-script
grammar in the design ledger for the full semantics.
Where to go deeper
aura <command> --help— exhaustive, always-current flag grammar.docs/design/INDEX.md— the design ledger: architecture, invariants, the World.docs/glossary.md— canonical terminology (blueprint, family, axis, sweep / Monte-Carlo / walk-forward, R, content-id, …).