//! Acceptance pin for the harness-input-binding spec (#231): a MULTI-COLUMN //! blueprint (open/high/low/close input roles) composes from blueprint data //! and runs over four inline `VecSource` columns to hand-computed values — //! pinning the role-declaration/merge-order contract (role i is fed by source //! i; same-timestamp ties break by source index, i.e. canonical column order — //! C4) without any archive. Mirrors `rsi_from_blueprint_data.rs` (the same //! public seam: a JSON document + `blueprint_from_json` + an injected //! vocabulary, C24). Pure engine machinery — no CLI, no binding module. use std::sync::mpsc; use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; use aura_engine::{blueprint_from_json, BlueprintNode, Composite, Edge, Role, Target, VecSource}; use aura_std::{std_vocabulary, Recorder}; // value = (high - low) + (close - open): every column is load-bearing, so any // mis-pairing of roles to sources (or a wrong merge tie-break) changes the // recorded rows. Node indices: 0 Sub(range = high - low) · // 1 Sub(body = close - open) · 2 Add(range + body). const OHLC_BLUEPRINT_JSON: &str = r#"{ "format_version": 1, "blueprint": { "name": "ohlc_probe", "nodes": [ {"primitive":{"type":"Sub"}}, {"primitive":{"type":"Sub"}}, {"primitive":{"type":"Add"}} ], "edges": [ {"from":0,"to":2,"slot":0,"from_field":0}, {"from":1,"to":2,"slot":1,"from_field":0} ], "input_roles": [ {"name":"open","targets":[{"node":1,"slot":1}]}, {"name":"high","targets":[{"node":0,"slot":0}]}, {"name":"low","targets":[{"node":0,"slot":1}]}, {"name":"close","targets":[{"node":1,"slot":0}]} ], "output": [{"node":2,"field":0,"name":"value"}] } }"#; // Nest the loaded signal under a Rust root that declares the four source roles // in canonical column order (open, high, low, close) and records the single // output (the rsi_from_blueprint_data idiom — sinks are outside the std // vocabulary, so the recorder is added in Rust). Root role slot i targets the // nested composite's role i (input_roles order). fn run_recording( signal: Composite, columns: [Vec<(Timestamp, Scalar)>; 4], ) -> Vec<(Timestamp, Vec)> { let (tx, rx) = mpsc::channel(); let roles: Vec = ["open", "high", "low", "close"] .iter() .enumerate() .map(|(slot, name)| Role { name: (*name).to_string(), targets: vec![Target { node: 0, slot }], source: Some(ScalarKind::F64), }) .collect(); let root = Composite::new( "h", vec![ BlueprintNode::Composite(signal), Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(), ], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], roles, vec![], ); let mut h = root.bootstrap_with_params(vec![]).expect("bootstraps (no open params)"); let sources: Vec> = columns .into_iter() .map(|c| Box::new(VecSource::new(c)) as Box) .collect(); h.run(sources); rx.try_iter().collect() } fn col(values: &[f64]) -> Vec<(Timestamp, Scalar)> { values .iter() .enumerate() .map(|(i, &v)| (Timestamp(i as i64 + 1), Scalar::f64(v))) .collect() } /// Hand-computed contract. The four sources at one bar timestamp arrive in /// FOUR consecutive engine cycles (the k-way merge breaks the timestamp tie /// by source index = canonical column order: open, high, low, close); every /// `Firing::Any` node re-evaluates per fresh input over the newest value of /// each leg (sample-and-hold on the stale leg). Bars (o,h,l,c): /// (1,10,5,3), (2,20,8,6), (4,40,16,12). /// /// Bar 1: open=1 -> body fires, close leg cold -> None; high=10 -> range /// fires, low leg cold -> None; low=5 -> range = 10-5 = 5, add fires, body /// leg cold -> None; close=3 -> body = 3-1 = 2, add = 5+2 = 7 -> (t1, 7). /// Bar 2: open=2 -> body = 3-2 = 1 (close held at 3), add = 5+1 = 6; /// high=20 -> range = 20-5 = 15 (low held), add = 15+1 = 16; /// low=8 -> range = 20-8 = 12, add = 12+1 = 13; /// close=6 -> body = 6-2 = 4, add = 12+4 = 16. /// Bar 3: open=4 -> body = 6-4 = 2, add = 12+2 = 14; high=40 -> range = /// 40-8 = 32, add = 32+2 = 34; low=16 -> range = 40-16 = 24, add = 26; /// close=12 -> body = 12-4 = 8, add = 24+8 = 32. #[test] fn ohlc_blueprint_runs_over_four_columns_in_canonical_order() { let signal = blueprint_from_json(OHLC_BLUEPRINT_JSON, &|t| std_vocabulary(t)) .expect("loads through the std vocabulary"); let trace = run_recording( signal, [ col(&[1.0, 2.0, 4.0]), // open col(&[10.0, 20.0, 40.0]), // high col(&[5.0, 8.0, 16.0]), // low col(&[3.0, 6.0, 12.0]), // close ], ); let want: Vec<(Timestamp, Vec)> = [ (1, 7.0), (2, 6.0), (2, 16.0), (2, 13.0), (2, 16.0), (3, 14.0), (3, 34.0), (3, 26.0), (3, 32.0), ] .iter() .map(|&(t, v)| (Timestamp(t), vec![Scalar::f64(v)])) .collect(); assert_eq!( trace, want, "role i must be fed by source i in canonical column order", ); }