rename: retire the stage1-* family for the r-family (r-sma / r-breakout / r-meanrev)

The stage1 ordinal has been dead vocabulary since the C10 reframe dropped
the two-stage research model: a 1 structurally implies a 2 that no longer
exists. The family is renamed by its live discriminator - the R yardstick -
with members named by their signal, uniform with their signal-named
siblings (sma, macd, momentum):

- selectors: stage1-r -> r-sma, stage1-breakout -> r-breakout,
  stage1-meanrev -> r-meanrev (old tokens are usage errors, exit 2 - no
  silent alias)
- identifiers: Strategy::RSma/RBreakout/RMeanRev, HarnessKind::RSma,
  r_sma_*/r_breakout_*/r_meanrev_*, wrap_r, run_signal_r, RGrid, R_SMA_*
- persisted identity: the sma_signal composite (param prefix
  sma_signal.fast.length / .slow.length; fixtures regenerated; content-ids
  shift - no test pins a literal hash, the registry parses no record names)
- e2e test files git-mv'd to r_sma_e2e / r_breakout_e2e / r_meanrev_e2e
- dead Stage-1/Stage-2 prose reworded to post-C10 vocabulary across
  rustdoc, Cargo.tomls, ledger live lines, and the glossary (historical
  entries stay; fieldtests corpus untouched)
- CLAUDE.md invariant 7 rewritten to record the ratified C10 reframe
  faithfully (the token-swap alone would have laundered the retired
  gated-currency/realistic-broker design into unmarked live prose); the
  unbacked account-mode clause dropped

Verification: cargo build/test --workspace green (51 targets), clippy
-D warnings clean, doc build clean, acceptance grep gate leaves exactly
the one resampling-stage false positive (harness.rs), smoke: --harness
r-sma runs, --harness stage1-r exits 2 with the new usage line.

Decision log: forks and rationale recorded on the issue (reconciliation
+ implementation-phase comments).

closes #174
This commit is contained in:
2026-07-02 12:03:09 +02:00
parent e1b04439c0
commit 4de6d5cbad
22 changed files with 582 additions and 583 deletions
+24 -25
View File
@@ -84,31 +84,30 @@ design decision, not a refactor, and belongs in the ledger.
makes a live external call mid-replay. (See `~/.claude/CLAUDE.md` for the
IONOS consent rule: external LLM calls happen at the recording/live-source
edge, with explicit per-session consent, never inside a backtest.)
7. **Strategy output is a directional bias stream; risk-based execution and
realistic brokers are decoupled downstream layers; signal quality is measured
in R.** The DAG expresses one state at t (C8: ≤1 record per `eval`), so a
strategy's primary, backtestable output is a signed, bounded **bias** `f64 ∈
[-1,+1]` per cycle — direction (sign) + conviction (magnitude, optional),
**unsized** (not an equity curve, not a position, not a size). Sizing leaves the
strategy: a decoupled **risk-based execution** layer `bias → stop-rule → Sizer →
Veto → position-management` (the **RiskExecutor** composite, per symbol, nested
in a Broker/Account composite) turns bias + a protective **stop** into a sized
intent. The **stop defines the risk unit R** (1R = the loss if stopped).
**Signal quality is measured in R** (R-multiples / expectancy), the account- and
instrument-agnostic yardstick — **Stage 1**: flat-1R sizing, feed-forward, no
equity feedback, the primary research loop. **Currency P&L is Stage 2** (deploy
viability): fixed-fractional sizing reads equity (compounding) → realistic
brokers apply real frictions → currency equity, entered only after `E[R] > 0`.
The single feedback (equity → Sizer) is cut by a `z⁻¹` register on the **fill
edge** (mark-to-market stays a same-cycle price read), encapsulated in the
executor composite; flat-1R vs compounding is a **structural axis** (C11). The
broker-independent **position-event table** (`event_ts, action[buy/sell/close],
position_id, instrument_id, volume`) remains the **decoupled, derived** Stage-2
audit layer — the first difference of the *book* (`deal = target book
in_flight`), a computed table (not a per-`eval` output, since one decision
instant may yield >1 event) — feeding realistic brokers. Brokers/executors are
ordinary downstream nodes, never part of the strategy; account mode
(netting/hedging) is a composition constraint.
7. **Strategy output is a directional bias stream; risk-based execution is a
decoupled downstream layer; signal quality is measured in R.** The DAG
expresses one state at t (C8: ≤1 record per `eval`), so a strategy's primary,
backtestable output is a signed, bounded **bias** `f64 ∈ [-1,+1]` per cycle —
direction (sign) + conviction (magnitude, optional), **unsized** (not an
equity curve, not a position, not a size). Sizing leaves the strategy: a
decoupled **risk-based execution** layer `bias → stop-rule →
position-management` (the **RiskExecutor** composite, per symbol; the **Veto**
an optional documented pre-trade-gate seam) turns bias + a protective **stop**
into a managed position, in R. The **stop defines the risk unit R** (1R = the
loss if stopped). **Signal quality is measured in R** (R-multiples /
expectancy), the account- and instrument-agnostic yardstick — the research
loop is **pure feed-forward**: flat-1R, gross R → net R via the composable
cost-model graph (C10), no Sizer, no equity feedback, no `z⁻¹` register.
**Money is a live/deploy-edge concern**: currency P&L, fixed-fractional
sizing, and compounding are post-hoc money-management transforms of the net-R
sequence at the deploy/account layer, and the live broker is an **I/O
adapter** at the recording/deploy edge (C11/C13), never part of the strategy.
The broker-independent **position-event table** (`event_ts,
action[buy/sell/close], position_id, instrument_id, volume`) survives as the
**decoupled, derived** deploy/reconciliation audit artifact — the first
difference of the *book* (`deal = target book in_flight`), a computed
table (not a per-`eval` output, since one decision instant may yield >1
event).
8. **Deploy artifacts are frozen.** Hot-reload (cdylib) is an authoring-loop
tool only. The live bot is a statically-linked, versioned, frozen artifact —
never hot-swapped (audit trail: this bot = this commit).
+6 -6
View File
@@ -71,7 +71,7 @@ pub struct RunMetrics {
/// `runs.jsonl` lines still deserialise (C14/C18 back-compat).
#[serde(alias = "exposure_sign_flips")]
pub bias_sign_flips: u64,
/// Optional Stage-1 R metrics block. `None` for a pip-only run (and for legacy
/// Optional R metrics block. `None` for a pip-only run (and for legacy
/// `runs.jsonl` written before this field existed — `serde(default)`); omitted from
/// the JSON entirely when absent (`skip_serializing_if`), so the pip-only on-disk
/// shape stays byte-unchanged (C14/C18 back-compat).
@@ -79,7 +79,7 @@ pub struct RunMetrics {
pub r: Option<RMetrics>,
}
/// R-based signal-quality metrics (Stage-1), reduced from a `PositionManagement` dense
/// R-based signal-quality metrics, reduced from a `PositionManagement` dense
/// record stream by [`summarize_r`]. Account- and instrument-agnostic (pure R). Carries
/// the enriched dispersion/churn fields (SQN, conviction terciles, net-of-cost).
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -129,7 +129,7 @@ impl PartialEq for RMetrics {
// Dense `PositionManagement` record column indices — the lockstep contract with
// aura-std's FIELD_NAMES/RECORD_KINDS. The record crosses the crate boundary as a
// type-erased `Scalar` vector (C4 SoA), so it is read positionally, not by importing
// the producer's types; the layout is shared by convention and `stage1_r_e2e.rs`
// the producer's types; the layout is shared by convention and `r_sma_e2e.rs`
// guards that it matches.
mod r_col {
pub const CLOSED: usize = 0;
@@ -324,7 +324,7 @@ pub fn summarize_r(
/// `summarize_r_includes_open_trade_and_matches_r_metrics_from_rs` — touch one copy
/// without the other and that test is the sole tripwire. The
/// fields a flat R series cannot carry are set honestly: `n_open_at_end = 0`,
/// `net_expectancy_r = expectancy_r` (exact under the Stage-1 cost = 0 invariant),
/// `net_expectancy_r = expectancy_r` (exact under the cost = 0 invariant),
/// `conviction_terciles_r = [0,0,0]` (per-trade conviction is not pooled). Empty
/// input -> a well-defined all-zero `RMetrics`.
pub fn r_metrics_from_rs(rs: &[f64]) -> RMetrics {
@@ -375,7 +375,7 @@ pub fn r_metrics_from_rs(rs: &[f64]) -> RMetrics {
n_open_at_end: 0,
sqn,
sqn_normalized,
net_expectancy_r: mean, // cost = 0 -> net == gross (Stage-1 frictionless)
net_expectancy_r: mean, // cost = 0 -> net == gross (frictionless)
conviction_terciles_r: [0.0; 3],
trade_rs: Vec::new(),
}
@@ -1108,7 +1108,7 @@ mod tests {
#[test]
fn r_metrics_from_rs_folds_a_flat_series() {
// pooled across-window R series [2.0, -1.0, 1.0]: expectancy = 2/3, 2 wins of 3,
// profit_factor = (2+1)/1 = 3. At cost 0 (frictionless Stage-1) net == gross.
// profit_factor = (2+1)/1 = 3. At cost 0 (frictionless) net == gross.
// conviction terciles are not pooled -> [0,0,0]; n_open_at_end is not a pooled
// concept -> 0.
let m = r_metrics_from_rs(&[2.0, -1.0, 1.0]);
+2 -2
View File
@@ -12,8 +12,8 @@ path = "src/main.rs"
[dependencies]
aura-core = { path = "../aura-core" }
aura-engine = { path = "../aura-engine" }
# aura-composites: the Stage-1 RiskExecutor / vol_stop composite-builders the
# stage1-r harness wires (kept out of aura-engine so the engine stays domain-free).
# aura-composites: the RiskExecutor / vol_stop composite-builders the
# r-sma harness wires (kept out of aura-engine so the engine stays domain-free).
aura-composites = { path = "../aura-composites" }
aura-registry = { path = "../aura-registry" }
aura-std = { path = "../aura-std" }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
+1
View File
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
-1
View File
@@ -1 +0,0 @@
{"format_version":1,"blueprint":{"name":"stage1_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
@@ -1 +0,0 @@
{"format_version":1,"blueprint":{"name":"stage1_signal","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"stage1_signal","nodes":[{"primitive":{"type":"Nope","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"Nope","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
+1 -1
View File
@@ -234,7 +234,7 @@ fn graph_build_renders_role_kind_mismatch_at_finalize_by_identifier() {
/// runs and distinct for a structurally different topology. It is the same hash `aura run`
/// stamps as `topology_hash` (both via the one `content_id` primitive over the canonical
/// `blueprint_to_json` bytes), so a data-authored topology has a stable, comparable content
/// id. (Note: an op-script and the Rust `stage1_signal` builder produce *different*
/// id. (Note: an op-script and the Rust `sma_signal` builder produce *different*
/// canonical forms — composite debug-name, an unbound `Bias.scale` — so they are different
/// topologies by the byte definition; content-addressing keys on the canonical form.)
#[test]
+2 -2
View File
@@ -1,4 +1,4 @@
//! `aura-composites` — reusable Stage-1 composite-builders: the volatility stop and
//! `aura-composites` — reusable composite-builders for the feed-forward R loop: the volatility stop and
//! the per-symbol RiskExecutor. Both are `GraphBuilder` compositions (from
//! `aura-engine`) of `aura-std` primitives, so this crate sits *above* both and is
//! the single place where the engine's builder and the standard nodes are wired
@@ -35,7 +35,7 @@ pub fn vol_stop(length: i64, k: f64) -> Composite {
/// are `named` `stop_length` / `stop_k` so their slots surface in `param_space` under
/// the `.stop_length.length` / `.stop_k.weights[0]` suffixes). One body, so the
/// topology — every node, edge, and exposed role — cannot drift between the two arms;
/// only the two knobs differ (the `Option<i64>`-knob idiom `stage1_r_graph` uses).
/// only the two knobs differ (the `Option<i64>`-knob idiom `r_sma_graph` uses).
fn vol_stop_inner(knobs: Option<(i64, f64)>) -> Composite {
let mut g = GraphBuilder::new("vol_stop");
let price = g.input_role("price");
@@ -1,8 +1,8 @@
//! The `RiskExecutor` composite (Stage-1, #128): a per-symbol risk-based executor over a
//! The `RiskExecutor` composite (#128): a per-symbol risk-based executor over a
//! bias stream — `stop-rule -> Sizer -> position-management`, exposing the dense R-record.
//! Proves the composite bootstraps, runs end-to-end, and folds to the SAME R-outcomes as
//! the hand-wired iter-1 chain, and that R is invariant under the Sizer's `risk_budget`
//! (Stage-1 feed-forward). The Veto is a DOCUMENTED SEAM, not a runtime node (a
//! (feed-forward). The Veto is a DOCUMENTED SEAM, not a runtime node (a
//! pass-through identity is exactly what C19/C23 DCE deletes), so it appears nowhere here.
use aura_core::{
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar,
@@ -14,7 +14,7 @@ use aura_std::{Recorder, PM_FIELD_NAMES, PM_RECORD_KINDS};
use std::sync::mpsc::channel;
// The dense-record columns this fixture reads, named in lockstep with the sibling
// `stage1_r_e2e.rs` (which names `REALIZED_R = 1` the same way) so the cross-crate
// `r_sma_e2e.rs` (which names `REALIZED_R = 1` the same way) so the cross-crate
// `PM_FIELD_NAMES` layout is never referenced by a bare literal.
const REALIZED_R: usize = 1;
const SIZE: usize = 10;
@@ -100,7 +100,7 @@ fn risk_executor_bootstraps_and_folds_to_expected_rmetric() {
assert!((m.expectancy_r - 0.5).abs() < 1e-9, "window-end R = (105-100)/10; got {}", m.expectancy_r);
}
/// Property: R is invariant under the Sizer's `risk_budget` (Stage-1 feed-forward). The
/// Property: R is invariant under the Sizer's `risk_budget` (feed-forward). The
/// same price path at two budgets yields a bit-identical realised-R ledger, while the
/// `size` column scales with the budget — proving size flows through the Sizer into PM yet
/// never touches R.
@@ -127,7 +127,7 @@ fn risk_executor_r_invariant_under_risk_budget() {
}
/// Property: **a real folded `RMetrics` survives the `RunMetrics.r` serde round-trip
/// byte-for-byte (the Stage-1 on-disk back-compat contract), driven from an actual run —
/// byte-for-byte (the on-disk back-compat contract), driven from an actual run —
/// not a hand-built literal.** A bootstrapped RiskExecutor run is folded by `summarize_r`
/// and the result is attached as `RunMetrics.r = Some(..)`; serializing then
/// deserializing must reproduce an equal value, and the `r` key must be present. The
@@ -148,7 +148,7 @@ fn folded_rmetrics_survives_runmetrics_serde_round_trip() {
}
/// Property: the RiskExecutor embeds the `vol_stop` composite (the new `StopRule::Vol`
/// arm) and folds to a finite RMetric — the volatility-defined default the `stage1-r`
/// arm) and folds to a finite RMetric — the volatility-defined default the `r-sma`
/// harness uses. A short k·σ stop over a rising-then-falling path opens a trade and
/// (stop or window-end) closes at least one, so the fold is non-empty and sane.
#[test]
@@ -246,7 +246,7 @@ fn risk_executor_vol_open_exposes_the_two_stop_knobs_as_free_axes() {
/// Property (#137): the OPEN vol-stop arm, bootstrapped at a given `(length, k)`, folds to
/// the SAME R-outcome as the BOUND arm at the same values — the byte-equivalence the
/// no-flags stage1-r sweep relies on to stay golden (the gridding variant only frees the
/// no-flags r-sma sweep relies on to stay golden (the gridding variant only frees the
/// two knobs; topology and every other constant are unchanged). The open arm's positional
/// point is `[length, k]` in `param_space` slot order (length first, then k).
#[test]
+1 -1
View File
@@ -22,7 +22,7 @@ serde_json = { workspace = true }
[dev-dependencies]
# aura-std is a TEST-ONLY dependency: the engine's own integration tests
# (stage1_r_e2e, random_sweep_e2e, ger40_breakout) drive real standard nodes through
# (r_sma_e2e, random_sweep_e2e, ger40_breakout) drive real standard nodes through
# a bootstrapped Harness. The engine's library code never names a concrete node — it
# routes type-erased Scalar records (summarize_r reads the PositionManagement record
# positionally, by index), so aura-std stays out of [dependencies] and the engine is
@@ -3,7 +3,7 @@
//! {Gt, Gt} -> {Latch, Latch} -> Sub = bias in {-1,0,+1}. Tests the FADE
//! direction (price above mean+k*sigma -> short -1; below mean-k*sigma -> long
//! +1), the latched hold, and that no fade fires on a flat series. Built
//! straight from aura-std nodes (no CLI dependency); mirrors stage1_breakout_e2e.
//! straight from aura-std nodes (no CLI dependency); mirrors r_breakout_e2e.
use aura_core::{Scalar, Timestamp};
use aura_engine::{GraphBuilder, Harness, Source, VecSource};
@@ -1,4 +1,4 @@
//! Stage-1 R E2E: a synthetic bias+price chain through stop-rule + position-management,
//! R E2E: a synthetic bias+price chain through stop-rule + position-management,
//! recorded and folded by `summarize_r`. Also guards the dense-record layout contract
//! (aura-std's `PM_FIELD_NAMES`/`PM_RECORD_KINDS` vs the column indices `summarize_r`
//! reads).
@@ -94,7 +94,7 @@ fn synthetic_long_then_stop_produces_a_sane_rmetric() {
/// Drive the real cross-crate chain `stop -> PositionManagement` over a `(bias, price)`
/// path, collecting the producer's dense records (decoded by the producer's own
/// `PM_RECORD_KINDS`, the wire contract) into a ledger — the recorded stream a Stage-1
/// `PM_RECORD_KINDS`, the wire contract) into a ledger — the recorded stream an R-yardstick
/// run would persist. Per-cycle bias (not constant) so a fixture can drop bias to 0 to
/// forbid the immediate re-entry after a stop. The fold is left to the caller so the
/// same recorded ledger can be summarized at several `round_trip_cost` values.
@@ -124,7 +124,7 @@ fn run_chain_ledger(stop: &mut dyn Node, path: &[(f64, f64)]) -> Vec<(Timestamp,
/// Drive the real cross-crate chain `stop -> PositionManagement -> summarize_r`
/// over a `(bias, price)` path. This is the actual producer->consumer seam,
/// node-by-node; the resulting `RMetrics` is what a recorded Stage-1 run would report.
/// node-by-node; the resulting `RMetrics` is what a recorded R-yardstick run would report.
fn run_chain(stop: &mut dyn Node, path: &[(f64, f64)]) -> RMetrics {
summarize_r(&run_chain_ledger(stop, path), &[])
}
@@ -1,5 +1,5 @@
//! Folded sink reductions are byte-for-byte equal to the raw-recorder path.
//! Drives the folding sinks node-by-node (the `stage1_r_e2e.rs` direct-node
//! Drives the folding sinks node-by-node (the `r_sma_e2e.rs` direct-node
//! style) over a synthetic dense R-record + equity/exposure series, and asserts
//! `summarize_r` over `GatedRecorder`'s emitted rows equals `summarize_r` over
//! the full record, and a `SeriesReducer` summary equals `summarize`'s fields.
+4 -4
View File
@@ -1,4 +1,4 @@
//! `PositionManagement` — the stateful heart of Stage-1 risk-based execution. Turns a
//! `PositionManagement` — the stateful heart of feed-forward risk-based execution. Turns a
//! bias + a protective-stop distance into a stream of realised per-trade R-outcomes.
//! Emits ONE dense record per cycle (C8, like `SimBroker`; multi-field output on the
//! `Resample` precedent): the trade ledger is the rows where `closed_this_cycle`, the
@@ -146,7 +146,7 @@ impl Node for PositionManagement {
} else {
price.max(p.stop_level)
};
// size-invariant: R is a pure stop-distance ratio (Stage-1 feed-forward).
// size-invariant: R is a pure stop-distance ratio (feed-forward).
realized = p.dir as f64 * (fill - p.entry) / p.latched_dist;
was_stopped = true;
reason = ExitReason::Stop as i64;
@@ -157,7 +157,7 @@ impl Node for PositionManagement {
closed = true;
self.pos = None;
} else if bias_exit {
// size-invariant: R is a pure stop-distance ratio (Stage-1 feed-forward).
// size-invariant: R is a pure stop-distance ratio (feed-forward).
realized = p.dir as f64 * (price - p.entry) / p.latched_dist;
reason = if sign0(bias) != 0.0 {
ExitReason::ReversalLeg as i64
@@ -275,7 +275,7 @@ mod tests {
}
// (3) R-invariance under size: the Sizer's `size` flows into col 10 but never into R.
// Scaling size leaves every realized_r identical (the property that keeps Stage 1
// Scaling size leaves every realized_r identical (the property that keeps the research loop
// feed-forward); col 10 reflects the size so we know it actually flowed end-to-end.
#[test]
fn realized_r_is_invariant_under_size() {
+5 -5
View File
@@ -1,11 +1,11 @@
//! `Sizer` — the flat-1R sizing seam (C10 Stage-1). Turns a protective-stop distance
//! `Sizer` — the flat-1R sizing seam (C10). Turns a protective-stop distance
//! into a position `size = risk_budget / stop_distance`: with `risk_budget = 1.0` this is
//! true flat-1R (one risk unit per trade) and `size` is inversely proportional to the
//! stop — NOT a constant. The `bias` input gates firing (a size is only meaningful when a
//! strategy is taking a position) and is the Stage-2 seam: fixed-fractional sizing swaps
//! `risk_budget` for `risk_fraction · equity` (an added equity input), same node shape.
//! R is computed SIZE-INVARIANTLY downstream, so the Sizer never contaminates signal
//! quality — it only scales the (Stage-2) currency exposure.
//! strategy is taking a position) and is the live/deploy-edge seam: fixed-fractional
//! sizing swaps `risk_budget` for `risk_fraction · equity` (an added equity input), same
//! node shape. R is computed SIZE-INVARIANTLY downstream, so the Sizer never contaminates
//! signal quality — it only scales the (deploy-edge) currency exposure.
use aura_core::{
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
ScalarKind,
+17 -17
View File
@@ -614,7 +614,7 @@ the ground truth**. The cost model *approximates*; it **never claims realism**.
`SimBroker` (the pip-equity, unsized-exposure node) is the **pre-reframe pip
ancestor** — with the net-R cost model it is **redundant as a quality measure** (R
supersedes pips). It is retained as a **legacy / simple optional pip yardstick**
(still wired in the `stage1-r` harness for an honest dual readout), **not part of
(still wired in the `r-sma` harness for an honest dual readout), **not part of
the new model and not to be expanded**.
**Forbids.** Putting **sizing or the stop in the strategy** (bias is unsized; the
@@ -737,7 +737,7 @@ co-temporal cost stream (positional 1:1 join — `cost[i]` is `record[i]`'s cycl
cost = 0 baseline on an empty stream. The headline sink is **`net_r_equity`** =
`LinComb(4)[cum_realized_r, unrealized_r, cum_cost_in_r, open_cost_in_r]` → Recorder
(C8/C18), a sibling of `r_equity`, emitted only when a cost is authored. Wired on the
**run path** via `--cost-per-trade` (`stage1_r_graph`); sweep / walk-forward / mc pass
**run path** via `--cost-per-trade` (`r_sma_graph`); sweep / walk-forward / mc pass
`None` (cost on the reduce-mode sweep path and cost in OOS pooling deferred — the
positional join holds only while one cost node fires in lockstep with PM). Deferred to
later milestone cycles: the general `CostNode` trait + multi-node cost-graph
@@ -928,8 +928,8 @@ composite-builder (`risk_executor(StopRule, risk_budget)`) with a
`StopRule{Fixed,Vol}` **structural axis** (C20); per the 2026-06-28 reframe its
Sizer interior and `risk_budget` arg are dropped / vestigial, the **Veto** stays a
documented seam, not a runtime node. The layer is operable from the CLI: `aura run
--harness <sma|macd|stage1-r>` — a compile-time selector over Rust-authored
harnesses (C9/C17) — folds `summarize_r` into `RunMetrics.r`, the `stage1-r`
--harness <sma|macd|r-sma>` — a compile-time selector over Rust-authored
harnesses (C9/C17) — folds `summarize_r` into `RunMetrics.r`, the `r-sma`
harness fanning one bias into both `SimBroker` (legacy pip) and the RiskExecutor
(R); an `r_equity` tap charts the by-trade R-equity. Composites live in the
dedicated `aura-composites` crate, so `aura-engine`'s runtime dependency stays
@@ -937,10 +937,10 @@ dedicated `aura-composites` crate, so `aura-engine`'s runtime dependency stays
stays acyclic).
**Realization (cycle 0066).** SQN is the operational single-number objective for
ranking a Stage-1 sweep family by signal quality — C12 **axis-2 (argmax-metric)**
ranking an r-sma sweep family by signal quality — C12 **axis-2 (argmax-metric)**
over the C18 family store. `metric_cmp` (`aura-registry`) learns the
higher-is-better R metrics `sqn`, `expectancy_r`, `net_expectancy_r`; a member
with no `r` block sorts last (`NEG_INFINITY`). `aura sweep --strategy stage1-r`
with no `r` block sorts last (`NEG_INFINITY`). `aura sweep --strategy r-sma`
produces the rankable R family, each member folding `summarize_r` into
`RunMetrics.r`. The default grid varies **only the signal** (`fast` / `slow` SMA
lengths), holding the stop and sizing fixed: `risk_budget` is R-invariant and
@@ -954,8 +954,8 @@ beside the floated knobs (reproducible from its own manifest, C18).
(`SQN_CAP = 100`), turnover-robust where raw `sqn` rewards trade count. It is an
**opt-in** rank key (`Metric::SqnNormalized`); raw `sqn` and the default ranker
stay byte-unchanged, and the field carries `#[serde(default)]` (C18). Below the cap
(`n ≤ 100`) it equals raw `sqn` exactly. **#135 (stage1-r `--trace`):**
`stage1_r_sweep_family` persists each member's equity / exposure / r_equity under
(`n ≤ 100`) it equals raw `sqn` exactly. **#135 (r-sma `--trace`):**
`r_sma_sweep_family` persists each member's equity / exposure / r_equity under
`runs/traces/<n>/<member_key>/` via the same `persist_traces_r` the single run uses;
per-member `--trace` is symmetric across all three sweep strategies.
@@ -1327,7 +1327,7 @@ one `content_id` primitive `topology_hash` also uses (acc 1); a Tier-1 optional
blueprint does not use leaves the id byte-stable (acc 3, composing #156/#164).
`serde_json/float_roundtrip` is enabled so stored f64 metrics round-trip exactly through
`families.jsonl` — the precondition for a bit-identical compare (C1). **Signal-only this
cycle**: the id covers the signal blueprint; the fixed Stage-1-R scaffolding stays
cycle**: the id covers the signal blueprint; the fixed r-sma scaffolding stays
commit-identified (it is not yet blueprint-data, C24). Whole-harness / structural-axis
content-addressing, and whether the id should exclude the composite **debug-name** (a
non-load-bearing symbol, invariant 11 — an op-script and the Rust builder produce
@@ -1887,7 +1887,7 @@ Tier-2 section to validate it; recorded on #156).
**Realization (2026-06-30, cycle 0092 — runs are built FROM blueprint-data, #165).**
`aura run <blueprint.json>` now loads a serialized **signal** blueprint
(`blueprint_from_json` → the closed `std_vocabulary`; an unknown type fails clean as
`UnknownNodeType`, the data-plane face of invariant 9), wraps it in the Stage-1-R run
`UnknownNodeType`, the data-plane face of invariant 9), wraps it in the r-sma run
scaffolding (sinks / broker / data supplied **at run**, not serialized — C24's deferred
set), runs it, and emits a `RunReport` **bit-identical** (C1) to its Rust-built twin —
proven by `loaded_signal_runs_bit_identical_to_rust_built`. The `RunManifest` now carries
@@ -1895,8 +1895,8 @@ a **`topology_hash`**: SHA256 of the canonical (#164) `blueprint_to_json`, the #
reproducibility anchor, a Tier-1 optional field (#156). The hash + helper live
research-side (`aura-cli` + `sha2`), off the frozen engine (invariant 8). This is the
**keystone of the World/C21 milestone**: topology-as-data is now *runnable*, not only
serializable. The harness wrap was made shareable — `stage1_r_graph()` =
`wrap_stage1r(stage1_signal(...))`, so the Rust path and the data path are the same seam
serializable. The harness wrap was made shareable — `r_sma_graph()` =
`wrap_r(sma_signal(...))`, so the Rust path and the data path are the same seam
over the same signal (a behaviour-preserving C19/C23 restructure). Scope note: the hash
covers the **signal** only (the fixed scaffolding is identified by `commit`); a content-id
distinguishing harness-structural variants is a #158/#166 concern once scaffolding varies.
@@ -1904,7 +1904,7 @@ distinguishing harness-structural variants is a #158/#166 concern once scaffoldi
**Realization (2026-07-01, cycle 0093 — the World orchestrates FAMILIES from blueprint-data,
#166).** `aura sweep <blueprint.json> --axis <name>=<csv>` loads an **open** signal blueprint,
grids the by-name axes against its `param_space()`, builds each member through cycle-1's
`wrap_stage1r` seam, and aggregates to a `FamilyKind::Sweep` family — byte-identical in shape
`wrap_r` seam, and aggregates to a `FamilyKind::Sweep` family — byte-identical in shape
to the hard-wired sweep (`append_family` / `sweep_member_reports` reused verbatim). This is the
C21 step beyond a single run: the World now constructs and orchestrates **families** of
harnesses from topology-data, not just one. Each member manifest carries the **shared**
@@ -1928,7 +1928,7 @@ the loaded blueprint's params over the user `--axis` grid (the #169 prefixed nam
`select_winner` reused verbatim), runs it out-of-sample, and aggregates to a
`FamilyKind::WalkForward` family — persisted + content-addressed + `aura reproduce` bit-identical
(the read-side WalkForward branch rebuilds each OOS window from `manifest.window`). One
substitution deep from the hard-wired stage1-r WF arm (the loaded blueprint via
substitution deep from the hard-wired r-sma WF arm (the loaded blueprint via
`blueprint_axis_probe` replaces the built-in strategy); a bad `--axis` is a clean in-closure
exit 2, never a panic. This is Arm A (the settled direction): the loaded IS-optimizing form
honestly carries the `walkforward` name. Reduce-mode members are R-measured (`oos_r` the
@@ -1942,11 +1942,11 @@ prints are exactly what `--axis` binds. Every listed name is **mandatory** on a
`walkforward`: the blueprint must be fully bound before it runs, so a subset grid is refused with
the missing knob named (`BindError::MissingKnob`) and there is no default — pin a knob you do not
want to vary with a single-value axis (`--axis <name>=<one-value>`). A single `blueprint_axis_probe` helper now single-sources
the wrapped probe (`wrap_stage1r(loaded_signal).param_space()`) for the sweep terminal, the MC
the wrapped probe (`wrap_r(loaded_signal).param_space()`) for the sweep terminal, the MC
closed-check, AND the listing (three former inline copies → one), so **listed == swept by
construction** (and stays so across #159's harness retirement — the listing tracks whatever the
sweep actually resolves, never a second source of truth). The names are prefixed by the current
stage1-r wrapping (`stage1_signal.fast.length`, the nested-composite prefix), not the raw
r-sma wrapping (`sma_signal.fast.length`, the nested-composite prefix), not the raw
`param_space` — which is why the discovery lives on the sweep verb (it owns the wrapping), not
`graph introspect`. Per-member trace-writing on `--trace` (#168) remains tracked debt.
@@ -1956,7 +1956,7 @@ content-addressed blueprint store (`aura reproduce`) shipped cycle 0094 (see C18
Realization). What **remains** is a content-id that covers **structural-axis / whole-harness
variants** (the scaffolding is not yet blueprint-data) and the debug-name-in-id question
(#171/#170). Still deferred: retiring the pre-C24
hard-wired `aura-cli` harnesses (`HarnessKind`, `run_stage1_r`, `*_sweep_family`) once
hard-wired `aura-cli` harnesses (`HarnessKind`, `run_r_sma`, `*_sweep_family`) once
the project-as-crate layer lands (#159, paired with #157's data-authoring surface). **Out of the first cut's round-trippable set**
(deliberate; fails clean as `UnknownNodeType`, never a silent wrong graph): recording
sinks (capture an `mpsc::Sender` — runtime identity, not param-generic data, C19) and
+2 -2
View File
@@ -93,7 +93,7 @@ The set of harness instances produced by varying the structural axes (strategy
### exposure stream
**Avoid:** — (superseded)
**Superseded by `bias` (2026-06 R-reframe).** The pre-reframe primary output: a signed, bounded `f64 ∈ [-1,+1]` per cycle read as the desired fractional *position* (conflating direction + conviction + size). Reframed to `bias` (unsized direction + conviction), sizing moved downstream to the `Sizer`/`RiskExecutor`; the term and the `Exposure` node persist in pre-reframe code until the Stage-1 rename.
**Superseded by `bias` (2026-06 R-reframe).** The pre-reframe primary output: a signed, bounded `f64 ∈ [-1,+1]` per cycle read as the desired fractional *position* (conflating direction + conviction + size). Reframed to `bias` (unsized direction + conviction), sizing moved downstream to the `Sizer`/`RiskExecutor`; the term and the `Exposure` node persisted in pre-reframe code until the r-family rename (#174).
### firing policy
**Avoid:** —
@@ -249,7 +249,7 @@ System Quality Number — `√n · mean_R / stdev_R`: the dispersion- and trade-
### Stage 1 / Stage 2
**Avoid:** — (superseded)
**Superseded (2026-06-28 C10 reframe): there is no Stage-2 currency/compounding gate.** Research is one pure feed-forward R loop — gross R → net R via the `cost model` — with money, compounding, and a real broker living only at a separate later **live / deploy edge** (compounding is a post-hoc money-management transform of the net-R sequence, not an in-loop axis). The pre-reframe cleave gated a Stage-2 currency / fixed-fractional / realistic-broker layer behind `E[R] > 0`; **"Stage-1"** now survives only as a historical cycle / identifier name for the shipped feed-forward R chain, not one half of a two-stage gate.
**Superseded (2026-06-28 C10 reframe): there is no Stage-2 currency/compounding gate.** Research is one pure feed-forward R loop — gross R → net R via the `cost model` — with money, compounding, and a real broker living only at a separate later **live / deploy edge** (compounding is a post-hoc money-management transform of the net-R sequence, not an in-loop axis). The pre-reframe cleave gated a Stage-2 currency / fixed-fractional / realistic-broker layer behind `E[R] > 0`; **"Stage-1"** now survives only as a historical cycle name the shipped feed-forward R chain's identifier family was renamed to the r-family (`r-sma` / `r-breakout` / `r-meanrev`, #174) — not one half of a two-stage gate.
### strategy
**Avoid:** —