Files
Aura/crates/aura-engine/tests/ohlc_from_blueprint_data.rs
T
Brummel 1d14ebc1cd feat(cli,engine): multi-column opening on every real-data path (#231 task 4)
The six Close-only open sites route through the resolved binding's
column set via open_columns (probe_window, open_real_source,
run_sources/windowed_sources, the campaign member open, the trace
re-run open) — a strategy declaring high/low/close roles gets exactly
those columns, merged in the canonical C4 order. Synthetic data refuses
any binding beyond {close} with the honest --real remedy (the walk
generates a close series only); the sweep/mc family builders ride their
established Err contract, run/walkforward/reproduce refuse exit-1.

Proof at two layers: an engine e2e composes an OHLC blueprint from data
and runs it over four inline VecSources to hand-computed rows (the
merge-order pin), and an archive-gated CLI e2e sweeps a high/low
blueprint over GER40 opening the High and Low columns.

Verified: engine OHLC e2e green, full workspace suite green, clippy -D
warnings clean.

refs #231
2026-07-11 03:37:14 +02:00

138 lines
5.2 KiB
Rust

//! 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<Scalar>)> {
let (tx, rx) = mpsc::channel();
let roles: Vec<Role> = ["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<Box<dyn aura_engine::Source>> = columns
.into_iter()
.map(|c| Box::new(VecSource::new(c)) as Box<dyn aura_engine::Source>)
.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<Scalar>)> = [
(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",
);
}