b39fd63396
Phase 4 of the Stratification milestone. aura-std held four C28 ladder layers in one roster; this cuts them into layer-aligned, aura-core-only node crates so the import direction is enforced by the crate graph: - aura-std — engine nodes only (arithmetic/logic/rolling + sinks) - aura-market — session, resample - aura-strategy — bias, stops, sizer, cost-model machinery - aura-backtest — sim_broker, position_management - aura-vocabulary — the relocated closed std_vocabulary roster Node modules move verbatim (byte-identical renames); consumers are rewired by import path only. A new structural test (aura-vocabulary/tests/c28_layering.rs) asserts each node crate's [dependencies] stay within its C28-permitted inner set, catching the acyclic-but-outward violation the compiler misses. Behaviour byte-identical: full workspace suite green (1448 tests), no golden edited, clippy -D warnings clean. C28 Status block updated. closes #288
175 lines
6.9 KiB
Rust
175 lines
6.9 KiB
Rust
//! End-to-end coverage for `ListSpace` (cycle 0107 task 2, C12.1): an explicit,
|
|
//! non-lattice point set — modelled on `random_sweep_e2e.rs`'s worked-author
|
|
//! pattern — driven through the PUBLIC `sweep` surface with a REAL bootstrapped
|
|
//! harness (not a fake closure), so the property under test is that `ListSpace`
|
|
//! integrates with the actual bootstrap -> run -> summarize pipeline the same
|
|
//! way `GridSpace`/`RandomSpace` do, exercising the exact use case `ListSpace`
|
|
//! exists for: re-running an arbitrary member subset (e.g. a gate's survivors)
|
|
//! that no cartesian `GridSpace` can represent.
|
|
|
|
use std::sync::mpsc;
|
|
|
|
use aura_core::{Cell, Firing, ScalarKind, Timestamp};
|
|
use aura_engine::{
|
|
f64_field, summarize, sweep, BlueprintNode, Composite, Edge, ListSpace, OutField, ParamSpec,
|
|
Role, RunManifest, RunReport, Scalar, SweepError, Target, VecSource,
|
|
};
|
|
use aura_backtest::SimBroker;
|
|
use aura_std::{Recorder, Sma, Sub};
|
|
use aura_strategy::Bias;
|
|
|
|
/// Seven synthetic F64 ticks — the same fixture shape as `random_sweep_e2e.rs`,
|
|
/// short enough that small SMA windows warm up deterministically.
|
|
fn synthetic_prices() -> Vec<(Timestamp, Scalar)> {
|
|
[
|
|
(1_i64, 1.0000_f64),
|
|
(2, 1.0010),
|
|
(3, 1.0030),
|
|
(4, 1.0060),
|
|
(5, 1.0040),
|
|
(6, 1.0010),
|
|
(7, 0.9990),
|
|
]
|
|
.iter()
|
|
.map(|&(t, p)| (Timestamp(t), Scalar::f64(p)))
|
|
.collect()
|
|
}
|
|
|
|
/// The same SMA-cross `[fast: I64, slow: I64, scale: F64]` harness as
|
|
/// `random_sweep_e2e.rs`, built through the public builder API.
|
|
#[allow(clippy::type_complexity)]
|
|
fn sma_cross_harness() -> (
|
|
Composite,
|
|
mpsc::Receiver<(Timestamp, Vec<Scalar>)>,
|
|
mpsc::Receiver<(Timestamp, Vec<Scalar>)>,
|
|
) {
|
|
let (tx_eq, rx_eq) = mpsc::channel();
|
|
let (tx_ex, rx_ex) = mpsc::channel();
|
|
let sma_cross = Composite::new(
|
|
"sma_cross",
|
|
vec![
|
|
Sma::builder().named("fast").into(),
|
|
Sma::builder().named("slow").into(),
|
|
Sub::builder().into(),
|
|
],
|
|
vec![
|
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
|
],
|
|
vec![Role {
|
|
name: "price".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
|
source: None,
|
|
}],
|
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
);
|
|
let bp = Composite::new(
|
|
"root",
|
|
vec![
|
|
BlueprintNode::Composite(sma_cross),
|
|
Bias::builder().into(),
|
|
SimBroker::builder(0.0001).into(),
|
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
|
|
],
|
|
vec![
|
|
Edge { from: 0, to: 1, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 2, to: 3, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 4, slot: 0, from_field: 0 },
|
|
],
|
|
vec![Role {
|
|
name: "src".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 2, slot: 1 }],
|
|
source: Some(ScalarKind::F64),
|
|
}],
|
|
vec![],
|
|
);
|
|
(bp, rx_eq, rx_ex)
|
|
}
|
|
|
|
/// Build + bootstrap + run + summarize one point into a real `RunReport` — the
|
|
/// full pipeline `ListSpace` must survive end-to-end, mirroring
|
|
/// `random_sweep_e2e.rs::run_point`.
|
|
fn run_point(point: &[Cell]) -> RunReport {
|
|
let (bp, rx_eq, rx_ex) = sma_cross_harness();
|
|
let mut h = bp.bootstrap_with_cells(point).expect("ListSpace validates points before sweep");
|
|
h.run(vec![Box::new(VecSource::new(synthetic_prices()))]);
|
|
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
|
|
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
|
|
RunReport {
|
|
manifest: RunManifest {
|
|
commit: "list-sweep-e2e".to_string(),
|
|
params: Vec::new(),
|
|
defaults: Vec::new(),
|
|
window: (Timestamp(0), Timestamp(0)),
|
|
seed: 0,
|
|
broker: "test".to_string(),
|
|
selection: None,
|
|
instrument: None,
|
|
topology_hash: None,
|
|
project: None,
|
|
},
|
|
metrics: summarize(&equity, &exposure),
|
|
}
|
|
}
|
|
|
|
/// Property: `ListSpace` reproduces the exact use case it exists for — an
|
|
/// arbitrary, non-lattice member subset (e.g. what a gate stage's survivor
|
|
/// ordinals would pick back out of a prior sweep family) driven through a REAL
|
|
/// bootstrapped harness produces one finite-metric `RunReport` per given point,
|
|
/// in the given order — never the cartesian product a `GridSpace` over the same
|
|
/// values would enumerate.
|
|
#[test]
|
|
fn list_space_reruns_an_arbitrary_survivor_subset_through_a_real_harness() {
|
|
let space = sma_cross_harness().0.param_space();
|
|
// a hand-picked, non-lattice subset: point 1 and point 2 share no axis value
|
|
// pairwise with what a full [fast]x[slow]x[scale] grid over these values
|
|
// would also produce as *other* combinations — this is a strict subset, not
|
|
// a grid, and `ListSpace` must run exactly these three, nothing else.
|
|
let survivors = vec![
|
|
vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(1.0)],
|
|
vec![Scalar::i64(3), Scalar::i64(5), Scalar::f64(0.5)],
|
|
vec![Scalar::i64(2), Scalar::i64(5), Scalar::f64(0.75)],
|
|
];
|
|
let ls = ListSpace::new(&space, survivors.clone()).expect("valid survivor subset");
|
|
let family = sweep(&ls, run_point);
|
|
|
|
assert_eq!(family.points.len(), 3, "exactly the given survivor points ran, no more");
|
|
for (i, expected) in survivors.iter().enumerate() {
|
|
let named = family.named_params(i);
|
|
let got: Vec<Scalar> = named.into_iter().map(|(_, v)| v).collect();
|
|
assert_eq!(&got, expected, "point {i} carried through in input order, untouched");
|
|
assert!(
|
|
family.points[i].report.metrics.total_pips.is_finite(),
|
|
"point {i} ran through the real bootstrap -> run -> summarize pipeline"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Property: the typed validation gate on `ListSpace::new` rejects a
|
|
/// wrong-kind value BEFORE any run — a `F64` value in the harness's `I64`
|
|
/// `fast` slot returns the public `SweepError::KindMismatch`, never a panic
|
|
/// and never a run of the malformed point.
|
|
#[test]
|
|
fn list_space_rejects_wrong_kind_value_before_any_run() {
|
|
let space = sma_cross_harness().0.param_space();
|
|
let err = ListSpace::new(
|
|
&space,
|
|
vec![vec![Scalar::f64(2.0), Scalar::i64(4), Scalar::f64(1.0)]],
|
|
)
|
|
.expect_err("fast slot is I64, not F64");
|
|
assert_eq!(
|
|
err,
|
|
SweepError::KindMismatch {
|
|
slot: 0,
|
|
value_index: 0,
|
|
expected: ScalarKind::I64,
|
|
got: ScalarKind::F64,
|
|
}
|
|
);
|
|
// and the schema type stays reachable even on the rejection path (a real
|
|
// author inspects it to correct the point)
|
|
let _: &[ParamSpec] = &space;
|
|
}
|