41cbb5506f
Brings the other two composite-boundary edge-kinds to the named-projection shape #40 gave outputs. input_roles changes from a bare Vec<Vec<Target>> to Vec<Role { name, targets }> (rendered [in:<name>]); a composite gains params: Vec<ParamAlias { name, node, slot }> that relabels an interior leaf param slot's surface name in param_space() (rendered [param:<name>]). Param aliasing is a PURE NAMING OVERLAY, not curation (the load-bearing decision, per spec 0019): every interior param slot stays in param_space() and sweepable; the alias only relabels in place, never reorders or hides. Proven empirically — the MACD run is byte-identical (total_pips 0.1637945563898923, 3 sign flips), only the param labels improved: param_space() now surfaces [macd.fast, macd.slow, macd.signal] instead of three indistinguishable macd.length, and the manifest reads ema_fast/ ema_slow/ema_signal. C23 honoured: role/param/output names are non-load-bearing debug symbols dropped at lowering; identity is positional (role index, param slot, output field). compiled_view_golden is byte-identical (verified: the golden region is untouched in the diff). An out-of-range alias (missing/ non-leaf node or slot past the leaf's param count) is rejected at compile_with_params as BadInteriorIndex, mirroring the output range-check (no new variant). Orthogonal to #36 — purely additive at the composite level. Aliasing is demonstrated on the CLI MACD site only (the spec's worked example + a new E2E test macd_param_space_surfaces_the_three_named_aliases); sma_cross, the engine test fixtures, and the construction-layer fieldtests get the forced role-name + empty params, so the param_space C23 anchor goldens (param_space_mirrors_compiled_flat_node_param_order + siblings) stay byte-identical. Verification (orchestrator-run, not trusted from the agent report): cargo build/test/clippy --workspace -D warnings all green (engine 66, cli 12); the separate-workspace construction-layer fieldtest crate builds (guards the #42 latent-drift recurrence); compiled_view_golden + MACD determinism unchanged. Two faithful repairs to the plan's literal test/code bodies, no semantic change: the out-of-range test uses .err()/Some(BadInteriorIndex) (the Ok arm Vec<Box<dyn Node>> is not Debug, so .unwrap_err() would not compile), and the inline_composite destructure binds params as `param_aliases` to avoid shadowing the injected `params: &[Scalar]` arg. closes #41
121 lines
5.2 KiB
Rust
121 lines
5.2 KiB
Rust
// Milestone fieldtest — axis (a): author a small signal as a reusable NAMED
|
|
// Composite, build a Blueprint that uses it, compile() + bootstrap() + run(),
|
|
// and confirm the recorded trace is non-empty.
|
|
//
|
|
// Public interface only: Blueprint / Composite / BlueprintNode / Edge / Target /
|
|
// OutField / SourceSpec from aura-engine; Sma / Sub / Exposure / SimBroker /
|
|
// Recorder from aura-std; ScalarKind / Scalar / Firing / Timestamp from
|
|
// aura-core. No crates/*/src was read.
|
|
//
|
|
// The downstream task: package an SMA(2)/SMA(4)-cross into a reusable
|
|
// `sma_cross` composite exposing one output (the cross spread), then wire that
|
|
// composite into a harness blueprint (source -> composite -> Exposure ->
|
|
// SimBroker -> Recorder), bootstrap it, and run it over a tiny synthetic price
|
|
// ramp. This is the milestone's headline authoring story: build a graph
|
|
// fractally from a Rust builder via a named composite.
|
|
|
|
use std::sync::mpsc;
|
|
|
|
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
|
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, Role, SourceSpec, Target};
|
|
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
|
|
|
fn main() {
|
|
// --- The reusable composite: a 2/4 SMA cross. ----------------------------
|
|
// Interior layout (local indices):
|
|
// 0: Sma(2) (fast) 1: Sma(4) (slow) 2: Sub (fast - slow)
|
|
// One input role (the price) fans into BOTH SMAs' slot 0.
|
|
// The exposed output port is Sub's single output field.
|
|
// Value-empty recipes (factories); the two SMA lengths are injected at
|
|
// bootstrap (param vector below), not baked here.
|
|
let sma_cross = Composite::new(
|
|
"sma_cross",
|
|
vec![
|
|
Sma::factory().into(),
|
|
Sma::factory().into(),
|
|
Sub::factory().into(),
|
|
],
|
|
// interior edges (local indices): fast -> Sub.slot0, slow -> Sub.slot1
|
|
vec![
|
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
|
],
|
|
// input role 0 (price) fans into Sma(2).slot0 AND Sma(4).slot0
|
|
vec![Role {
|
|
name: "price".into(),
|
|
targets: vec![
|
|
Target { node: 0, slot: 0 },
|
|
Target { node: 1, slot: 0 },
|
|
],
|
|
}],
|
|
// no param aliases
|
|
vec![],
|
|
// single exposed output: Sub's output field 0
|
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
);
|
|
|
|
// --- The harness blueprint that USES the composite. ----------------------
|
|
// Top-level items:
|
|
// 0: sma_cross (Composite) 1: Exposure(0.5) 2: SimBroker(1e-4)
|
|
// 3: Recorder over [F64] (taps the broker equity)
|
|
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
|
|
let blueprint = Blueprint::new(
|
|
vec![
|
|
BlueprintNode::Composite(sma_cross),
|
|
Exposure::factory().into(),
|
|
SimBroker::factory(1e-4).into(),
|
|
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx).into(),
|
|
],
|
|
// one f64 price source: feeds the composite (item 0, role 0) AND the
|
|
// broker's price leg (item 2, slot 1).
|
|
vec![SourceSpec {
|
|
kind: ScalarKind::F64,
|
|
targets: vec![
|
|
Target { node: 0, slot: 0 }, // composite price role
|
|
Target { node: 2, slot: 1 }, // broker price leg
|
|
],
|
|
}],
|
|
// top-level edges: composite-out -> Exposure -> broker.exposure(slot0)
|
|
// -> Recorder
|
|
vec![
|
|
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // sma_cross -> Exposure
|
|
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // Exposure -> SimBroker.exposure
|
|
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // SimBroker -> Recorder
|
|
],
|
|
);
|
|
|
|
// --- compile() + bootstrap() + run() -------------------------------------
|
|
// Param vector (depth-first item order, C12): sma_cross's two SMA lengths
|
|
// (I64), then Exposure's scale (F64); Sub/SimBroker/Recorder declare none.
|
|
let mut harness = blueprint
|
|
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
|
|
.expect("composite-authored blueprint should bootstrap");
|
|
|
|
// a synthetic upward price ramp then a dip, enough to warm SMA(4) and flip
|
|
let prices: Vec<f64> = vec![
|
|
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
|
|
];
|
|
let stream: Vec<(Timestamp, Scalar)> = prices
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
|
|
.collect();
|
|
harness.run(vec![stream]);
|
|
drop(harness); // drop the recorder's tx clone held inside
|
|
|
|
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.iter().collect();
|
|
println!("recorded equity rows: {}", rows.len());
|
|
for (ts, row) in &rows {
|
|
let v = match row[0] {
|
|
Scalar::F64(x) => x,
|
|
other => panic!("expected f64 equity, got {other:?}"),
|
|
};
|
|
println!(" ts={:>14} equity_pips={:.4}", ts.0, v);
|
|
}
|
|
assert!(
|
|
!rows.is_empty(),
|
|
"the composite-authored harness must record a populated equity trace"
|
|
);
|
|
println!("OK: composite authored, compiled, bootstrapped, ran, recorded.");
|
|
}
|