Files
Brummel 1a6eafa056 refactor(composites): extract Stage-1 composite-builders into their own crate
Dissolve the aura-engine -> aura-std dependency by moving the vol_stop /
risk_executor composite-builders out of aura-engine into a new aura-composites
crate. This restores the engine's domain-free layering, which the
composites-in-engine placement (cycle 0065) had inverted.

Why: the engine routes type-erased Scalar records and never names a concrete
node -- summarize_r reads the PositionManagement record positionally, by column
index, without linking aura-std (which is exactly why aura-std was a dev-only
dependency before 0065). Parking the node-naming composite-builders inside
aura-engine forced the engine's production code to reference the node library
for the first time (aura-engine -> aura-std), an upside-down layering: the
engine is the foundation the node library builds upon, not the reverse.

The clean home is a dedicated crate that depends on BOTH the engine's
GraphBuilder and the std nodes, leaving each lower crate untouched:

    aura-composites -> { aura-engine, aura-std } -> aura-core   (acyclic)
    aura-engine     -> aura-core only            (domain-free again)
    aura-std        -> aura-core only            (lean node library)

aura-std reverts to a [dev-dependency] of aura-engine (the engine's own E2E
tests -- stage1_r_e2e, random_sweep_e2e, ger40_breakout -- still drive real
std nodes through a bootstrapped Harness; that test-only edge creates no cycle).

Moved with history (git mv):
- crates/aura-engine/src/composites.rs           -> crates/aura-composites/src/lib.rs
- crates/aura-engine/tests/risk_executor.rs      -> crates/aura-composites/tests/
- crates/aura-engine/tests/vol_stop_composite.rs -> crates/aura-composites/tests/

Consumers rewired: aura-cli imports risk_executor / StopRule from aura_composites;
the moved tests import the builders from the crate under test. The intra-doc link
to RollMode and the module rationale doc are updated for the new home.

Behaviour-preserving: workspace build clean; clippy --all-targets -D warnings
clean; full suite 513 passed / 0 failed, identical to baseline (the two moved
test files run unchanged under aura-composites); cargo doc --no-deps clean (no
broken intra-doc links).
2026-06-24 13:44:44 +02:00

45 lines
2.0 KiB
Rust
Raw Permalink 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_composites::vol_stop;
use aura_engine::{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()
);
}