1d14ebc1cd
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
138 lines
5.2 KiB
Rust
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",
|
|
);
|
|
}
|