//! Shared `#[cfg(test)]` fixtures for the SMA-cross signal-quality harness, //! consumed by the `blueprint` and `sweep` unit-test modules. Kept in one place //! so the harness topology cannot silently diverge between them (see #53). //! Gated test-only at the declaration site in `lib.rs` //! (`#[cfg(test)] mod test_fixtures;`), matching the `reexport_tests` precedent. use crate::{BlueprintNode, Composite, Edge, OutField, Role, Target}; use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; use aura_std::{Bias, Recorder, SimBroker, Sma, Sub}; use std::sync::mpsc; /// Seven synthetic F64 ticks driving the harness; deterministic input fixture. pub(crate) 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 SMA-cross signal as a reusable value-empty composite: one input role /// (price), one output (fast-minus-slow spread). Lengths injected at compile. pub(crate) fn sma_cross() -> Composite { 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() }], ) } /// The signal-quality harness as a value-empty composite blueprint with two /// recording sinks (equity, exposure). #[allow(clippy::type_complexity)] pub(crate) fn composite_sma_cross_harness() -> ( Composite, mpsc::Receiver<(Timestamp, Vec)>, mpsc::Receiver<(Timestamp, Vec)>, ) { let (tx_eq, rx_eq) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel(); let bp = Composite::new( "root", vec![ BlueprintNode::Composite(sma_cross()), Bias::builder().named("bias").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 }, // composite out -> Bias Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker slot 0 Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity -> sink Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure -> sink ], vec![Role { name: "src".into(), targets: vec![ Target { node: 0, slot: 0 }, // price -> sma_cross role 0 Target { node: 2, slot: 1 }, // price -> SimBroker price slot ], source: Some(ScalarKind::F64), }], vec![], // output: the root ends in sinks ); (bp, rx_eq, rx_ex) } /// The SMA-cross **signal** graph as a sink-free, param-generic composite: fast /// SMA (length bound to 2) and slow SMA over one `price` role, their spread, a /// `Bias`. Output is the `bias` field. No recorder/broker — observation is /// attached by the caller. This is the canonical round-trip fixture (cycle 0087). pub(crate) fn sink_free_sma_cross_signal() -> Composite { Composite::new( "sma_cross", vec![ Sma::builder().named("fast").bind("length", Scalar::i64(2)).into(), Sma::builder().named("slow").into(), Sub::builder().into(), Bias::builder().into(), ], vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // fast -> Sub slot 0 Edge { from: 1, to: 2, slot: 1, from_field: 0 }, // slow -> Sub slot 1 Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // spread -> Bias ], vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }], vec![OutField { node: 3, field: 0, name: "bias".into() }], ) }