//! Fieldtest c0009 #2 — comparison + determinism over the report surface. //! //! Axis (carrier): run two harnesses differing in a tuning param (Exposure //! scale), confirm each run is deterministic (bit-identical metrics across two //! runs) and the two metrics differ as expected. This is the smallest slice of //! the World's reason to exist (C21): compare runs by their RunMetrics. //! //! Same SMA-cross harness as #1, but the Exposure node's `scale` is the swept //! tuning param. A smaller scale => larger |exposure| (clamp(signal/scale,..)), //! so the same price move earns proportionally more pips — until the exposure //! saturates at +1. So total_pips(scale=2) > total_pips(scale=4) here. use std::sync::mpsc::{self, Sender}; use aura_core::{Ctx, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind, Timestamp}; use aura_engine::{ Edge, Harness, RunManifest, RunMetrics, RunReport, SourceSpec, Target, f64_field, summarize, }; use aura_std::{Exposure, SimBroker, Sma, Sub}; struct RowRecorder { tx: Sender<(Timestamp, Vec)>, } impl Node for RowRecorder { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }], output: vec![], } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { let w = ctx.f64_in(0); if w.is_empty() { return None; } let _ = self.tx.send((ctx.now(), vec![Scalar::F64(w[0])])); None } } fn f64_stream(pairs: &[(i64, f64)]) -> Vec<(Timestamp, Scalar)> { pairs.iter().map(|&(t, v)| (Timestamp(t), Scalar::F64(v))).collect() } const PRICES: &[(i64, f64)] = &[ (1, 100.0), (2, 102.0), (3, 104.0), (4, 106.0), (5, 108.0), (6, 110.0), (7, 112.0), ]; /// One full run: bootstrap a harness with the given exposure scale, run, drain /// the two sinks, reduce to a RunReport. This is exactly the World's per-instance /// step in a tuning sweep (C12/C19/C20), authored in plain Rust. fn run_one(exposure_scale: f64) -> RunReport { let (eq_tx, eq_rx) = mpsc::channel(); let (exp_tx, exp_rx) = mpsc::channel(); let mut h = Harness::bootstrap( vec![ Box::new(Sma::new(2)), Box::new(Sma::new(4)), Box::new(Sub::new()), Box::new(Exposure::new(exposure_scale)), Box::new(SimBroker::new(1.0)), Box::new(RowRecorder { tx: eq_tx }), Box::new(RowRecorder { tx: exp_tx }), ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![ Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }, Target { node: 4, slot: 1 }, ], }], vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 1, from_field: 0 }, Edge { from: 2, to: 3, slot: 0, from_field: 0 }, Edge { from: 3, to: 4, slot: 0, from_field: 0 }, Edge { from: 3, to: 6, slot: 0, from_field: 0 }, Edge { from: 4, to: 5, slot: 0, from_field: 0 }, ], ) .expect("valid harness"); h.run(vec![f64_stream(PRICES)]); let equity = f64_field(&eq_rx.try_iter().collect::>(), 0); let exposure = f64_field(&exp_rx.try_iter().collect::>(), 0); let metrics = summarize(&equity, &exposure); RunReport { manifest: RunManifest { commit: "fieldtest-c0009-synthetic".to_string(), params: vec![("exposure_scale".to_string(), exposure_scale)], window: (Timestamp(1), Timestamp(7)), seed: 0, broker: "sim-optimal(pip_size=1)".to_string(), }, metrics, } } fn main() { // Determinism (C1): the same instance run twice yields bit-identical metrics. let a1 = run_one(4.0); let a2 = run_one(4.0); assert_eq!(a1.metrics, a2.metrics, "scale=4 deterministic across runs"); assert_eq!(a1.to_json(), a2.to_json(), "scale=4 JSON bit-identical"); println!("determinism OK: scale=4 metrics = {:?}", a1.metrics); // Comparison: a second instance with a different tuning param. let b = run_one(2.0); println!("scale=4 report json = {}", a1.to_json()); println!("scale=2 report json = {}", b.to_json()); // Expectation (public model): scale=2 doubles |exposure| (0.5 -> 1.0 saturated) // versus scale=4 (0.5), so the same +2/cycle price move earns ~2x the pips. let RunMetrics { total_pips: p4, .. } = a1.metrics; let RunMetrics { total_pips: p2, .. } = b.metrics; println!("total_pips scale=4 = {p4}, scale=2 = {p2}"); assert!(p2 > p4, "smaller scale => larger exposure => more pips ({p2} > {p4})"); // Both should be drawdown-free monotonic-up curves on a steadily-rising price, // and stay long throughout (no sign flips) — a sanity check the comparison is // apples-to-apples, differing only on the swept axis. assert_eq!(a1.metrics.max_drawdown, 0.0); assert_eq!(b.metrics.max_drawdown, 0.0); assert_eq!(a1.metrics.exposure_sign_flips, 0); assert_eq!(b.metrics.exposure_sign_flips, 0); println!("c0009_2 OK: deterministic + comparable two-run metrics over the report surface"); }