refactor(aura-cli): author the sample blueprints via GraphBuilder
Adopt the name-based GraphBuilder (cycle 0039) in the CLI sample blueprints, which previously hand-wired raw positional Edge/Role/OutField via Composite::new. sma_cross, macd, signals, sample_blueprint_with_sinks, and macd_strategy_blueprint now read as typed-handle authoring: nodes are added for NodeHandles, ports/fields are wired by name (series/lhs/rhs/value, exposure/price, term[i], col[0], the macd histogram/signal/macd outputs), and build() lowers to the same index-wired Composite. This is what #64 was for — the builder was previously exercised only in its own tests; the showcase sample now dogfoods it. Behaviour-preserving: node order, instance names (.named), bound params (blend.weights[2]), and wiring are byte-identical, so param_space, the graph render, the single run, and the sweep are unchanged — guarded green by the existing aura-cli sample/sweep/param_space/render tests and the engine suite (full workspace 0 failed; clippy --all-targets -D warnings clean). sample_harness is deliberately left as a direct FlatGraph construction (baked Sma::new(2) nodes, the single-run path) — it builds the compilat directly, not a value-empty Composite, so it is not a GraphBuilder target.
This commit is contained in:
+74
-129
@@ -10,8 +10,8 @@ mod render;
|
||||
|
||||
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
||||
use aura_engine::{
|
||||
f64_field, summarize, BlueprintNode, Composite, Edge, FlatGraph, Harness,
|
||||
OutField, Role, RunManifest, RunReport, SourceSpec, SweepFamily, Target,
|
||||
f64_field, summarize, Composite, Edge, FlatGraph, GraphBuilder, Harness,
|
||||
RunManifest, RunReport, SourceSpec, SweepFamily, Target,
|
||||
};
|
||||
use aura_registry::{rank_by, Registry};
|
||||
use aura_std::{Ema, Exposure, LinComb, Recorder, SimBroker, Sma, Sub};
|
||||
@@ -151,24 +151,16 @@ fn run_sample() -> RunReport {
|
||||
/// `blueprint.rs`'s test helper is the dedup tracked in #14). Value-empty: the SMA
|
||||
/// lengths are injected at compile, not baked here.
|
||||
fn sma_cross(name: &str) -> Composite {
|
||||
Composite::new(
|
||||
name,
|
||||
vec![
|
||||
Sma::builder().named("fast").into(), // fast SMA leg
|
||||
Sma::builder().named("slow").into(), // slow SMA leg
|
||||
Sub::builder().into(),
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||
],
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||
source: None,
|
||||
}],
|
||||
vec![OutField { node: 2, field: 0, name: "cross".into() }],
|
||||
)
|
||||
let mut g = GraphBuilder::new(name);
|
||||
let fast = g.add(Sma::builder().named("fast")); // fast SMA leg
|
||||
let slow = g.add(Sma::builder().named("slow")); // slow SMA leg
|
||||
let sub = g.add(Sub::builder());
|
||||
let price = g.input_role("price");
|
||||
g.feed(price, [fast.in_("series"), slow.in_("series")]);
|
||||
g.connect(fast.out("value"), sub.in_("lhs"));
|
||||
g.connect(slow.out("value"), sub.in_("rhs"));
|
||||
g.expose(sub.out("value"), "cross");
|
||||
g.build().expect("sample sma_cross wiring resolves")
|
||||
}
|
||||
|
||||
/// The blended signal: a trend leg (SMA-cross) and a momentum leg (MACD), combined
|
||||
@@ -176,35 +168,25 @@ fn sma_cross(name: &str) -> Composite {
|
||||
/// momentum}); the blend is a multi-param node living inside it, with one weight
|
||||
/// bound as a structural constant (so it drops out of the sweepable surface).
|
||||
fn signals(name: &str) -> Composite {
|
||||
Composite::new(
|
||||
name,
|
||||
vec![
|
||||
BlueprintNode::Composite(sma_cross("trend")), // 0 trend leg (one f64 "cross")
|
||||
BlueprintNode::Composite(macd("momentum")), // 1 momentum leg (3 outputs)
|
||||
// 2 blend: Σ wᵢ·termᵢ over [trend.cross, momentum.histogram, momentum.signal].
|
||||
// weights[2] is bound (a fixed signal-line weight) → removed from param_space;
|
||||
// weights[0]/[1] stay tunable. `.named("blend")` makes the path signals.blend.*
|
||||
// (and renders the `blend:` prefix).
|
||||
LinComb::builder(3)
|
||||
.named("blend")
|
||||
.bind("weights[2]", Scalar::F64(0.5))
|
||||
.into(),
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // trend.cross → blend.term[0]
|
||||
Edge { from: 1, to: 2, slot: 1, from_field: 2 }, // momentum.histogram → blend.term[1]
|
||||
Edge { from: 1, to: 2, slot: 2, from_field: 1 }, // momentum.signal → blend.term[2]
|
||||
],
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price → trend role 0
|
||||
Target { node: 1, slot: 0 }, // price → momentum role 0
|
||||
],
|
||||
source: None,
|
||||
}],
|
||||
vec![OutField { node: 2, field: 0, name: "signal".into() }],
|
||||
)
|
||||
let mut g = GraphBuilder::new(name);
|
||||
let trend = g.add(sma_cross("trend")); // trend leg (one f64 "cross")
|
||||
let momentum = g.add(macd("momentum")); // momentum leg (3 outputs)
|
||||
// blend: Σ wᵢ·termᵢ over [trend.cross, momentum.histogram, momentum.signal].
|
||||
// weights[2] is bound (a fixed signal-line weight) → removed from param_space;
|
||||
// weights[0]/[1] stay tunable. `.named("blend")` makes the path signals.blend.*
|
||||
// (and renders the `blend:` prefix).
|
||||
let blend = g.add(
|
||||
LinComb::builder(3)
|
||||
.named("blend")
|
||||
.bind("weights[2]", Scalar::F64(0.5)),
|
||||
);
|
||||
let price = g.input_role("price");
|
||||
g.feed(price, [trend.in_("price"), momentum.in_("price")]);
|
||||
g.connect(trend.out("cross"), blend.in_("term[0]")); // trend.cross → blend.term[0]
|
||||
g.connect(momentum.out("histogram"), blend.in_("term[1]")); // momentum.histogram → blend.term[1]
|
||||
g.connect(momentum.out("signal"), blend.in_("term[2]")); // momentum.signal → blend.term[2]
|
||||
g.expose(blend.out("value"), "signal");
|
||||
g.build().expect("sample signals wiring resolves")
|
||||
}
|
||||
|
||||
/// The sample signal-quality blueprint (value-empty) **with its two recording
|
||||
@@ -222,31 +204,19 @@ fn sample_blueprint_with_sinks() -> (
|
||||
) {
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let bp = Composite::new(
|
||||
"sample",
|
||||
vec![
|
||||
BlueprintNode::Composite(signals("signals")),
|
||||
Exposure::builder().into(),
|
||||
SimBroker::builder(0.0001).into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // spread -> Exposure
|
||||
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker slot 0
|
||||
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity -> sink
|
||||
Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure -> sink
|
||||
],
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price -> sma_cross role 0
|
||||
Target { node: 2, slot: 1 }, // price -> SimBroker price slot
|
||||
],
|
||||
source: Some(ScalarKind::F64),
|
||||
}],
|
||||
vec![], // output: the root ends in sinks, no re-export
|
||||
);
|
||||
let mut g = GraphBuilder::new("sample");
|
||||
let sig = g.add(signals("signals"));
|
||||
let exposure = g.add(Exposure::builder());
|
||||
let broker = g.add(SimBroker::builder(0.0001));
|
||||
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
|
||||
let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex));
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, [sig.in_("price"), broker.in_("price")]);
|
||||
g.connect(sig.out("signal"), exposure.in_("signal")); // blended signal -> Exposure
|
||||
g.connect(exposure.out("exposure"), broker.in_("exposure")); // exposure -> broker slot 0
|
||||
g.connect(broker.out("equity"), eq.in_("col[0]")); // equity -> sink
|
||||
g.connect(exposure.out("exposure"), ex.in_("col[0]")); // exposure -> sink
|
||||
let bp = g.build().expect("sample blueprint wiring resolves");
|
||||
(bp, rx_eq, rx_ex)
|
||||
}
|
||||
|
||||
@@ -394,36 +364,23 @@ fn load_runs_or_exit() -> Vec<RunReport> {
|
||||
/// histogram). Three `length` knobs (fast, slow, signal) are injected at compile
|
||||
/// in node order; value-empty here.
|
||||
fn macd(name: &str) -> Composite {
|
||||
Composite::new(
|
||||
name,
|
||||
vec![
|
||||
Ema::builder().named("fast").into(), // 0 fast EMA
|
||||
Ema::builder().named("slow").into(), // 1 slow EMA
|
||||
Sub::builder().into(), // 2 MACD line = fast − slow
|
||||
Ema::builder().named("signal").into(), // 3 signal EMA of the MACD line
|
||||
Sub::builder().into(), // 4 histogram = MACD line − signal
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // fast → line[0]
|
||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 }, // slow → line[1]
|
||||
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // line → signal EMA
|
||||
Edge { from: 2, to: 4, slot: 0, from_field: 0 }, // line → histogram[0]
|
||||
Edge { from: 3, to: 4, slot: 1, from_field: 0 }, // signal → histogram[1]
|
||||
],
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price → fast EMA
|
||||
Target { node: 1, slot: 0 }, // price → slow EMA
|
||||
],
|
||||
source: None,
|
||||
}],
|
||||
vec![
|
||||
OutField { node: 2, field: 0, name: "macd".into() }, // the MACD line
|
||||
OutField { node: 3, field: 0, name: "signal".into() }, // the signal line
|
||||
OutField { node: 4, field: 0, name: "histogram".into() }, // the histogram
|
||||
],
|
||||
)
|
||||
let mut g = GraphBuilder::new(name);
|
||||
let fast = g.add(Ema::builder().named("fast")); // fast EMA
|
||||
let slow = g.add(Ema::builder().named("slow")); // slow EMA
|
||||
let line = g.add(Sub::builder()); // MACD line = fast − slow
|
||||
let signal = g.add(Ema::builder().named("signal")); // signal EMA of the MACD line
|
||||
let hist = g.add(Sub::builder()); // histogram = MACD line − signal
|
||||
let price = g.input_role("price");
|
||||
g.feed(price, [fast.in_("series"), slow.in_("series")]);
|
||||
g.connect(fast.out("value"), line.in_("lhs")); // fast → line
|
||||
g.connect(slow.out("value"), line.in_("rhs")); // slow → line
|
||||
g.connect(line.out("value"), signal.in_("series")); // line → signal EMA
|
||||
g.connect(line.out("value"), hist.in_("lhs")); // line → histogram
|
||||
g.connect(signal.out("value"), hist.in_("rhs")); // signal → histogram
|
||||
g.expose(line.out("value"), "macd"); // the MACD line
|
||||
g.expose(signal.out("value"), "signal"); // the signal line
|
||||
g.expose(hist.out("value"), "histogram"); // the histogram
|
||||
g.build().expect("sample macd wiring resolves")
|
||||
}
|
||||
|
||||
/// The MACD strategy blueprint (value-empty): the `macd` histogram → `Exposure` →
|
||||
@@ -433,31 +390,19 @@ fn macd_strategy_blueprint(
|
||||
tx_eq: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
tx_ex: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
) -> Composite {
|
||||
Composite::new(
|
||||
"macd_strategy",
|
||||
vec![
|
||||
BlueprintNode::Composite(macd("macd")),
|
||||
Exposure::builder().into(),
|
||||
SimBroker::builder(0.0001).into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 2 }, // histogram → Exposure
|
||||
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure → broker slot 0
|
||||
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity → sink
|
||||
Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure → sink
|
||||
],
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price → macd role 0
|
||||
Target { node: 2, slot: 1 }, // price → SimBroker price slot
|
||||
],
|
||||
source: Some(ScalarKind::F64),
|
||||
}],
|
||||
vec![], // output: the root ends in sinks, no re-export
|
||||
)
|
||||
let mut g = GraphBuilder::new("macd_strategy");
|
||||
let macd_node = g.add(macd("macd"));
|
||||
let exposure = g.add(Exposure::builder());
|
||||
let broker = g.add(SimBroker::builder(0.0001));
|
||||
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
|
||||
let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex));
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, [macd_node.in_("price"), broker.in_("price")]);
|
||||
g.connect(macd_node.out("histogram"), exposure.in_("signal")); // histogram → Exposure
|
||||
g.connect(exposure.out("exposure"), broker.in_("exposure")); // exposure → broker slot 0
|
||||
g.connect(broker.out("equity"), eq.in_("col[0]")); // equity → sink
|
||||
g.connect(exposure.out("exposure"), ex.in_("col[0]")); // exposure → sink
|
||||
g.build().expect("macd_strategy wiring resolves")
|
||||
}
|
||||
|
||||
/// The MACD strategy blueprint as a param-space fixture (receivers dropped, since
|
||||
|
||||
Reference in New Issue
Block a user