Files
Aura/crates/aura-engine/tests/vol_stop_composite.rs
T
Brummel a6fc48daa7 feat(stage1-r): operate the R layer from the CLI (iter 3)
`aura run --harness stage1-r [--real <SYM>] [--trace <n>]` now runs a Stage-1 R
backtest and prints its signal-quality metrics (the R block) in the RunReport
JSON - one shell call, the research loop's primary verb, ergonomic for Claude
to drive.

What shipped:
- vol_stop + risk_executor promoted from test fixtures to public aura-engine
  composite-builders, with a StopRule{Fixed,Vol} axis (C11). aura-std becomes a
  normal dependency of aura-engine (the composites are the engine's convenience
  layer over the standard nodes - the sibling tier of summarize_r; the graph
  stays acyclic: aura-engine -> aura-std -> aura-core). Both fixtures call the
  public fns; a Vol-backed RED test pins the new match arm.
- stage1_r_blueprint fans one bias into BOTH a SimBroker (pip) and the
  RiskExecutor (R), so one report carries both yardsticks honestly. run_stage1_r
  folds summarize (pip) + summarize_r (R, round_trip_cost 0.0 - Stage-1 is
  frictionless signal quality) and sets RunMetrics.r = Some(..). An r_equity tap
  (cum_realized_r + unrealized_r via LinComb) persists through a separate 3-tap
  persist_traces_r and renders via the existing `aura chart --tap r_equity`.
- `aura run --harness <sma|macd|stage1-r>`: a parse_run_args tokenizer replaces
  the five `run` literal-slice arms so --harness/--real/--from/--to/--trace
  compose in any order; --macd kept as a back-compat alias; --harness sma the
  default. A compile-time match over Rust-authored built-ins - not a runtime node
  registry, not a DSL (C9/C17): the CLI runs an authored harness, it does not
  wire one.

Back-compat: --harness sma/macd leave RunMetrics.r = None, so skip_serializing_if
keeps pip-only and legacy runs.jsonl JSON byte-unchanged; the plain-run tap list
stays ["equity","exposure"].

Refactor beyond the plan (verified behavior-equivalent): the old standalone
parse_real_args was orphaned by the new tokenizer, so it and its now-redundant
unit test were removed and run's --real parsing inlined into parse_run_args. The
--real strictness (empty-symbol, non-i64 ms, repeated-flag, window-requires-real
-> exit 2) is preserved and now pinned by
parse_run_args_rejects_malformed_real_and_repeated_flags (added to restore the
deleted coverage). All five run_real integration tests stay green.

Verification: cargo build/test --workspace green (every crate, 0 failed); clippy
--workspace --all-targets -D warnings clean. The full diff was adversarially
reviewed (run-arg refactor equivalence against the removed function, harness
wiring, C1/C2/C9/C17, the serde back-compat pin) - verdict SOUND; the one flagged
coverage gap is closed here.

Follow-on (noted, not done): the stage1-r manifest reuses the
"sim-optimal(pip_size=...)" broker label; a dedicated "risk-executor" label would
read more honestly for a dual-tap harness that runs a RiskExecutor branch.

closes #129
refs #117 #128
2026-06-24 12:09:58 +02:00

44 lines
1.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The volatility stop as a COMPOSITION of primitives (the post-correction design):
//! `stop_distance = k · Sqrt(Ema(Mul(Δ,Δ), length))`, `Δ = Sub(price, Delay(price,1))`
//! — a rolling EWMA standard deviation. This proves the decomposition wires through
//! `GraphBuilder`, bootstraps, and computes the right number end-to-end — the principle
//! that a node expressible as a DAG of primitives is a composition, not a fused node.
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{vol_stop, GraphBuilder, VecSource};
use aura_std::Recorder;
use std::sync::mpsc::channel;
/// An alternating ±1 price path makes `Δ² ≡ 1`, so the EWMA variance is `1`, `σ = 1`,
/// and `stop_distance = k·σ = k`. With `k = 2.0` the warmed-up output is exactly `2.0`.
#[test]
fn vol_stop_emits_k_times_rolling_stddev() {
let (tx, rx) = channel();
let mut g = GraphBuilder::new("vol_stop_harness");
let price = g.source_role("price", ScalarKind::F64);
let vs = g.add(vol_stop(/*length*/ 3, /*k*/ 2.0));
let rec = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx));
g.feed(price, [vs.input("price")]);
g.connect(vs.output("stop_distance"), rec.input("col[0]"));
let mut h = g
.build()
.expect("harness wires")
.bootstrap_with_params(vec![])
.expect("bootstraps");
let prices = [100.0, 101.0, 100.0, 101.0, 100.0, 101.0, 100.0, 101.0, 100.0, 101.0];
let stream: Vec<(Timestamp, Scalar)> = prices
.iter()
.enumerate()
.map(|(i, &p)| (Timestamp(i as i64), Scalar::f64(p)))
.collect();
h.run(vec![Box::new(VecSource::new(stream))]);
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.try_iter().collect();
let last = rows.last().expect("vol_stop produced output after warm-up");
assert!(
(last.1[0].as_f64() - 2.0).abs() < 1e-9,
"expected stop_distance = k·σ = 2.0, got {}",
last.1[0].as_f64()
);
}