784e6c917a
Replace a composite's single output port (OutPort { node, field }) with
an ordered, named record (Vec<OutField { node, field, name }>). A
composite can now re-export K interior fields as one output; a consumer
selects one via the existing Edge::from_field — the same field-wise
selector that already works for multi-output leaf producers (OHLCV).
Why: multi-line indicators (MACD, Bollinger, Stochastic, Ichimoku) could
not be authored as a composition and re-exported as a unit — only one
line escaped the boundary. The MACD PoC was forced to expose only the
histogram. This lifts the three `field == 0` / `from_field == 0` caps at
the composite boundary (inline_composite nested arm, rewrite_edge
composite arm), range-checking the index instead.
Boundary completion, not an invariant change:
- C8/C7/C4 untouched — one port, one row per eval, K base columns (a
3-line MACD is identical in kind to OHLCV).
- C23 honoured — re-export names live at the blueprint boundary only and
are dropped in the compilat (raw index wiring). compiled_view_golden
stayed byte-identical (the regression guard): no name leaked into the
flat graph.
The MACD PoC now re-exports macd / signal / histogram as a record; the
strategy still trades the histogram, selected via from_field: 2. The
render shows K [out:<name>] markers per multi-output composite (input
roles stay [in:<index>] — naming them is the dependent #41).
Output naming ships with the capability (OutField is born name-ready) so
#41 — composite param aliasing + input-role naming — does not reopen the
type.
Verification: cargo build/test/clippy --workspace -- -D warnings all
green; aura-engine 62 tests (+3 capability, +1 through-run E2E), aura-cli
unchanged-count green; compiled_view_golden byte-identical.
Known debt (out of scope, pre-existing): the non-workspace construction-
layer fieldtests (fieldtests/milestone-construction-layer/mc_1..mc_4)
carry their OutField sweep but still do not compile standalone — they use
the Sma::new / BlueprintNode::from(Sma) API retired in the cycle-0016
value-empty migration, never propagated to these fixtures. Tracked as a
follow-up.
closes #40
101 lines
3.8 KiB
Rust
101 lines
3.8 KiB
Rust
// Milestone fieldtest — axis (c): nest a composite INSIDE another composite,
|
|
// build a Blueprint, compile() + bootstrap() + run(). The milestone promises
|
|
// composites "nest arbitrarily" and that the compiled/flat view handles
|
|
// nesting (#26 notes the CLUSTERED blueprint render has an `unimplemented!` on
|
|
// nested composites — but that path is only reachable through `aura graph` on
|
|
// the built-in sample, which a downstream consumer cannot point at their own
|
|
// graph; see the spec finding).
|
|
//
|
|
// Here we verify the run-path promise from the consumer side: a composite that
|
|
// CONTAINS a composite lowers (inlines, recursively) to a flat runnable
|
|
// instance that produces a populated trace.
|
|
//
|
|
// Public interface only.
|
|
|
|
use std::sync::mpsc;
|
|
|
|
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
|
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, SourceSpec, Target};
|
|
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
|
|
|
/// Inner composite: the SMA(2)/SMA(4) cross (one price role -> spread output).
|
|
fn inner_cross() -> Composite {
|
|
Composite::new(
|
|
"sma_cross",
|
|
vec![
|
|
BlueprintNode::from(Sma::new(2)),
|
|
BlueprintNode::from(Sma::new(4)),
|
|
BlueprintNode::from(Sub::new()),
|
|
],
|
|
vec![
|
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
|
],
|
|
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
|
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
)
|
|
}
|
|
|
|
/// Outer composite: wraps inner_cross then maps it through Exposure(0.5).
|
|
/// Interior: 0 = inner_cross (Composite), 1 = Exposure(0.5).
|
|
/// One price role fans into the inner composite's role 0.
|
|
/// Output: Exposure's output field.
|
|
fn strategy() -> Composite {
|
|
Composite::new(
|
|
"strategy",
|
|
vec![
|
|
BlueprintNode::Composite(inner_cross()),
|
|
BlueprintNode::from(Exposure::new(0.5)),
|
|
],
|
|
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // cross -> Exposure
|
|
vec![vec![Target { node: 0, slot: 0 }]], // price -> inner role 0
|
|
vec![OutField { node: 1, field: 0, name: "out".into() }],
|
|
)
|
|
}
|
|
|
|
fn main() {
|
|
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
|
|
// Top-level: 0 = strategy (nested Composite), 1 = SimBroker, 2 = Recorder.
|
|
let blueprint = Blueprint::new(
|
|
vec![
|
|
BlueprintNode::Composite(strategy()),
|
|
BlueprintNode::from(SimBroker::new(1e-4)),
|
|
BlueprintNode::from(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
|
],
|
|
vec![SourceSpec {
|
|
kind: ScalarKind::F64,
|
|
targets: vec![
|
|
Target { node: 0, slot: 0 }, // strategy price role
|
|
Target { node: 1, slot: 1 }, // broker price leg
|
|
],
|
|
}],
|
|
vec![
|
|
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // strategy -> broker.exposure
|
|
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // broker -> Recorder
|
|
],
|
|
);
|
|
|
|
let mut harness = blueprint
|
|
.bootstrap()
|
|
.expect("nested-composite blueprint should bootstrap");
|
|
|
|
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);
|
|
|
|
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.iter().collect();
|
|
println!("nested-composite recorded rows: {}", rows.len());
|
|
assert!(
|
|
!rows.is_empty(),
|
|
"a composite-in-composite must inline to a flat runnable instance"
|
|
);
|
|
println!("OK: composite-inside-composite inlined, bootstrapped, ran, recorded.");
|
|
}
|