fieldtest: milestone construction-layer — 4 examples, 7 findings
End-to-end milestone fieldtest (closing gate) for the "Construction layer" milestone (#12 composites + #13 aura graph render). Four real downstream tasks authored against the PUBLIC interface only (ledger + doc-comments + re-exports + the aura CLI), under fieldtests/milestone-construction-layer/: mc_1 — author a named sma_cross composite, build a Blueprint, compile + bootstrap + run (12 populated equity rows); mc_2 — correct vs fast/slow-swapped cross: labels + graph-as-data differ observably (the headline mis-wire-visible property); mc_3 — composite-nested-in-composite inlines and runs; mc_4 — walk the built graph via the read-only accessors, no engine internals. Roll-up: friction_found, NO bugs. The milestone delivers its core promise — fractal authoring -> compile -> bootstrap -> run, introspection as graph-as-data, and param-carrying labels that make a mis-wire readable. None of the findings block the gate. Findings filed to the forward queue (not fixed here): - #28 (feature) — the headline friction: `aura graph` renders only the built-in sample and the render adapter is CLI-private, so a consumer can't render their OWN graph. Subsumes the reachability of #26 (the nested-render panic is structurally unreachable until the render is parameterizable). Likely next (consumer-project / World) milestone. - #29 (idea) — aura-engine doesn't re-export the scalar vocabulary (ScalarKind etc.) a graph-builder needs; one-crate ergonomics tidy. - #16 (comment) — `aura graph` arg policy (help / unknown-arg) should unify with the still-open `aura run` strictness question. The fieldtester read no implementation source; all artefacts are the consumer crate + the two live render captures (render_clustered.txt / render_compiled.txt, byte-identical across runs).
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aura-core"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "aura-engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aura-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aura-std"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aura-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ms-construction-layer-fieldtest"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aura-core",
|
||||
"aura-engine",
|
||||
"aura-std",
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
# Milestone fieldtest consumer crate — Construction layer.
|
||||
#
|
||||
# A standalone downstream project (the shape a C16 research project takes):
|
||||
# path-deps on the engine crates only. It drives the construction layer
|
||||
# (Blueprint / Composite / compile / bootstrap / run) and the graph-as-data
|
||||
# accessors from the PUBLIC interface only — no crates/*/src was read; the
|
||||
# API was discovered from the design ledger (docs/design/INDEX.md) + rustdoc
|
||||
# doc-comments + the public re-exports.
|
||||
#
|
||||
# Empty [workspace] table: this fixture crate is its OWN workspace root, so it
|
||||
# is not pulled into the aura engine workspace.
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "ms-construction-layer-fieldtest"
|
||||
edition = "2024"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "composite_build_run"
|
||||
path = "mc_1_composite_build_run.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "miswire_render"
|
||||
path = "mc_2_miswire_render.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "nested_composite"
|
||||
path = "mc_3_nested_composite.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "introspect_graph"
|
||||
path = "mc_4_introspect_graph.rs"
|
||||
|
||||
[dependencies]
|
||||
aura-core = { path = "../../crates/aura-core" }
|
||||
aura-std = { path = "../../crates/aura-std" }
|
||||
aura-engine = { path = "../../crates/aura-engine" }
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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 /
|
||||
// OutPort / 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, OutPort, 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.
|
||||
let sma_cross = Composite::new(
|
||||
"sma_cross",
|
||||
vec![
|
||||
BlueprintNode::from(Sma::new(2)),
|
||||
BlueprintNode::from(Sma::new(4)),
|
||||
BlueprintNode::from(Sub::new()),
|
||||
],
|
||||
// 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![vec![
|
||||
Target { node: 0, slot: 0 },
|
||||
Target { node: 1, slot: 0 },
|
||||
]],
|
||||
// single exposed output: Sub's output field 0
|
||||
OutPort { node: 2, field: 0 },
|
||||
);
|
||||
|
||||
// --- 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),
|
||||
BlueprintNode::from(Exposure::new(0.5)),
|
||||
BlueprintNode::from(SimBroker::new(1e-4)),
|
||||
BlueprintNode::from(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
// 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() -------------------------------------
|
||||
let mut harness = blueprint
|
||||
.bootstrap()
|
||||
.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.");
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Milestone fieldtest — axis (b): deliberately mis-wire a graph (swap the
|
||||
// fast/slow SMA legs of the cross) and confirm the wiring reads back
|
||||
// differently — the milestone's headline promise that a render visibly
|
||||
// surfaces a mis-wiring.
|
||||
//
|
||||
// The headline render is the `aura graph` CLI (exercised separately, in the
|
||||
// accompanying .sh-free flow: see the spec). HERE we test the property the
|
||||
// render RELIES ON from the consumer side: that the labels + graph-as-data of a
|
||||
// correctly-wired composite differ observably from a swapped one. If a swap
|
||||
// produced an identical graph-as-data, no renderer could ever surface it.
|
||||
//
|
||||
// Public interface only: Composite / Blueprint accessors + Node::label() via
|
||||
// the boxed node; ledger C8-refinement (label is the disambiguating symbol).
|
||||
|
||||
use aura_engine::{BlueprintNode, Composite, Edge, OutPort, Target};
|
||||
// NB: `node.label()` on a `&Box<dyn Node>` leaf dispatches via the trait object
|
||||
// without an explicit `use aura_core::Node` (the method is in scope through the
|
||||
// boxed trait object). A consumer does not need to import the Node trait to read
|
||||
// labels off a built graph.
|
||||
use aura_std::{Sma, Sub};
|
||||
|
||||
/// Build an SMA-cross composite. `swap` flips which SMA feeds Sub's slot 0.
|
||||
/// Correct: fast(SMA2) -> minuend(slot0), slow(SMA4) -> subtrahend(slot1).
|
||||
/// Swapped: slow -> slot0, fast -> slot1 (the classic fast/slow mis-wire).
|
||||
fn cross(swap: bool) -> Composite {
|
||||
let (e_fast, e_slow) = if swap {
|
||||
// slow(node 1) into slot0, fast(node 0) into slot1
|
||||
(
|
||||
Edge { from: 1, to: 2, slot: 0, from_field: 0 },
|
||||
Edge { from: 0, to: 2, slot: 1, from_field: 0 },
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||
)
|
||||
};
|
||||
Composite::new(
|
||||
"sma_cross",
|
||||
vec![
|
||||
BlueprintNode::from(Sma::new(2)),
|
||||
BlueprintNode::from(Sma::new(4)),
|
||||
BlueprintNode::from(Sub::new()),
|
||||
],
|
||||
vec![e_fast, e_slow],
|
||||
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
|
||||
OutPort { node: 2, field: 0 },
|
||||
)
|
||||
}
|
||||
|
||||
fn node_labels(c: &Composite) -> Vec<String> {
|
||||
c.nodes()
|
||||
.iter()
|
||||
.map(|n| match n {
|
||||
BlueprintNode::Leaf(node) => node.label(),
|
||||
BlueprintNode::Composite(inner) => format!("composite:{}", inner.name()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let correct = cross(false);
|
||||
let swapped = cross(true);
|
||||
|
||||
// The per-node labels carry identifying params (SMA(2) vs SMA(4)) so two
|
||||
// SMAs are distinguishable in a render.
|
||||
println!("correct labels: {:?}", node_labels(&correct));
|
||||
println!("swapped labels: {:?}", node_labels(&swapped));
|
||||
|
||||
// The mis-wire lives in the EDGE table; the graph-as-data must reflect it.
|
||||
let ce: Vec<Edge> = correct.edges().to_vec();
|
||||
let se: Vec<Edge> = swapped.edges().to_vec();
|
||||
println!("correct interior edges: {ce:?}");
|
||||
println!("swapped interior edges: {se:?}");
|
||||
|
||||
// Headline property: the two graphs-as-data differ. A renderer reading the
|
||||
// edge table + labels can therefore make the swap visible.
|
||||
assert_ne!(
|
||||
ce, se,
|
||||
"a fast/slow swap MUST change the graph-as-data, else no render could surface it"
|
||||
);
|
||||
|
||||
// And the disambiguating labels are present (SMA(2)/SMA(4) distinguishable).
|
||||
let labels = node_labels(&correct);
|
||||
assert!(
|
||||
labels.iter().any(|l| l.contains("SMA(2)")) && labels.iter().any(|l| l.contains("SMA(4)")),
|
||||
"labels must carry SMA params so a swapped leg reads differently: {labels:?}"
|
||||
);
|
||||
println!("OK: a fast/slow swap is observable in graph-as-data + labels.");
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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, OutPort, 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 }]],
|
||||
OutPort { node: 2, field: 0 },
|
||||
)
|
||||
}
|
||||
|
||||
/// 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
|
||||
OutPort { node: 1, field: 0 },
|
||||
)
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Milestone fieldtest — axis (d): introspect a built Blueprint via the
|
||||
// read-only graph-as-data accessors — can a consumer walk the graph as data
|
||||
// WITHOUT reading engine internals?
|
||||
//
|
||||
// Accessors under test (all public, from rustdoc):
|
||||
// Blueprint::nodes() / sources() / edges()
|
||||
// Composite::name() / nodes() / edges() / input_roles() / output()
|
||||
// BlueprintNode::{Leaf, Composite}; Node::label() on a boxed leaf
|
||||
//
|
||||
// Public interface only.
|
||||
|
||||
use aura_core::ScalarKind;
|
||||
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutPort, SourceSpec, Target};
|
||||
// NB (fieldtest finding): `aura_engine::ScalarKind` does NOT resolve — the
|
||||
// construction surface (aura-engine) does not re-export the core scalar
|
||||
// vocabulary (ScalarKind / Scalar / Firing) that a graph-builder needs. A
|
||||
// downstream author building a Blueprint must reach into aura_core directly.
|
||||
// Recorded as friction in the spec. `node.label()` below dispatches on the
|
||||
// boxed leaf via the Node trait object without an explicit `use` of Node.
|
||||
|
||||
fn sma_cross() -> Composite {
|
||||
Composite::new(
|
||||
"sma_cross",
|
||||
vec![
|
||||
BlueprintNode::from(aura_std::Sma::new(2)),
|
||||
BlueprintNode::from(aura_std::Sma::new(4)),
|
||||
BlueprintNode::from(aura_std::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 }]],
|
||||
OutPort { node: 2, field: 0 },
|
||||
)
|
||||
}
|
||||
|
||||
fn describe_node(prefix: &str, n: &BlueprintNode) {
|
||||
match n {
|
||||
BlueprintNode::Leaf(node) => {
|
||||
let sch = node.schema();
|
||||
println!(
|
||||
"{prefix}LEAF {:<14} inputs={} output_fields={}",
|
||||
node.label(),
|
||||
sch.inputs.len(),
|
||||
sch.output.len()
|
||||
);
|
||||
}
|
||||
BlueprintNode::Composite(c) => {
|
||||
println!(
|
||||
"{prefix}COMPOSITE name={:?} interior_items={} roles={} out=({},{})",
|
||||
c.name(),
|
||||
c.nodes().len(),
|
||||
c.input_roles().len(),
|
||||
c.output().node,
|
||||
c.output().field,
|
||||
);
|
||||
for inner in c.nodes() {
|
||||
describe_node(&format!("{prefix} "), inner);
|
||||
}
|
||||
for (r, targets) in c.input_roles().iter().enumerate() {
|
||||
println!("{prefix} role[{r}] fans into {targets:?}");
|
||||
}
|
||||
for e in c.edges() {
|
||||
println!("{prefix} interior edge {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let bp = Blueprint::new(
|
||||
vec![
|
||||
BlueprintNode::Composite(sma_cross()),
|
||||
BlueprintNode::from(aura_std::Exposure::new(0.5)),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![Target { node: 0, slot: 0 }],
|
||||
}],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
||||
);
|
||||
|
||||
println!("== Blueprint graph-as-data walk ==");
|
||||
println!("sources: {}", bp.sources().len());
|
||||
for (i, s) in bp.sources().iter().enumerate() {
|
||||
println!(" source[{i}] kind={:?} -> {:?}", s.kind, s.targets);
|
||||
}
|
||||
println!("top-level items: {}", bp.nodes().len());
|
||||
for n in bp.nodes() {
|
||||
describe_node(" ", n);
|
||||
}
|
||||
println!("top-level edges: {}", bp.edges().len());
|
||||
for e in bp.edges() {
|
||||
println!(" edge {e:?}");
|
||||
}
|
||||
|
||||
// The walk must reach the interior of the composite purely through the
|
||||
// public accessors — no engine internals.
|
||||
let first = &bp.nodes()[0];
|
||||
if let BlueprintNode::Composite(c) = first {
|
||||
assert_eq!(c.name(), "sma_cross");
|
||||
assert_eq!(c.nodes().len(), 3, "composite interior should have 3 items");
|
||||
assert_eq!(c.input_roles().len(), 1, "one price role");
|
||||
assert_eq!(c.input_roles()[0].len(), 2, "price fans into both SMAs");
|
||||
let out = c.output();
|
||||
assert_eq!((out.node, out.field), (2, 0), "Sub output is the port");
|
||||
println!("OK: walked the composite interior via public accessors only.");
|
||||
} else {
|
||||
panic!("expected composite as first item");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[source:F64]
|
||||
┌─────────└┐────────┐
|
||||
│ │ │
|
||||
╔═════╪══════════╪════╗ │
|
||||
║ sma_cross │ ║ │
|
||||
║ ↓ ↓ ║ │
|
||||
║ [SMA(2)] [SMA(4)] ║ │
|
||||
║ └──┌───────┘ ║ │
|
||||
║ ↓ ┌─╫───┘
|
||||
║ [Sub] │ ║
|
||||
║ │ │ ║
|
||||
╚════════╪══════════╪═╝
|
||||
│ │
|
||||
↓ │
|
||||
[Exposure(0.5)] │
|
||||
┌───────┘────────┐ │
|
||||
↓ ↓─┘
|
||||
[Recorder] [SimBroker(0.0001)]
|
||||
│
|
||||
↓
|
||||
[Recorder]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[source:F64]
|
||||
┌────────└─┐──────┐
|
||||
↓ ↓ │
|
||||
[SMA(2)] [SMA(4)] │
|
||||
└────┌─────┘ │
|
||||
↓ ┌──────┘
|
||||
[Sub] │
|
||||
│ │
|
||||
↓ └────┐
|
||||
[Exposure(0.5)] │
|
||||
┌──────┘─────────┐│
|
||||
↓ ↓┘
|
||||
[Recorder] [SimBroker(0.0001)]
|
||||
┌─────┘
|
||||
↓
|
||||
[Recorder]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user