a6fc48daa7
`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
44 lines
1.9 KiB
Rust
44 lines
1.9 KiB
Rust
//! 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()
|
||
);
|
||
}
|