d5c44dd1ea
Cycle 4 of milestone #148 (Cost-model graph in R), decision E. New `cost_graph(Vec<PrimitiveBuilder>) -> Composite` in aura-composites: it fans the 4 PM-geometry inputs to N cost nodes, surfaces each node's extra inputs (discovered via schema introspection past GEOMETRY_WIDTH) as `cost[k].<port>` roles, sums them through CostSum, and exposes the 3-field aggregate. Replaces the CLI's manual slot-indexed cost-wiring + the hardcoded MAX_RUN_COST_NODES=2 cap with one principled composite of arbitrary arity. GEOMETRY_WIDTH re-exported from aura-std (first cross-crate consumer). Runtime port names .leak()'d (the risk_executor precedent), a bounded one-time cost at blueprint construction. Behaviour-preserving at the value level: the composite inlines at bootstrap (C11) to the same flat fan-in, so the two cycle-3 net_expectancy_r goldens (-614.3134020253314 flat, -615.0304388396047 composed), the C18 no-cost golden, and the full suite stay green verbatim. Honours C9 (a composite of cost nodes is ordinary downstream nodes), C16 (wiring lives in aura-composites, not aura-engine), C23 (label drift under cost_graph nesting is permitted; no with-cost graph label/shape golden exists). Four aura-composites unit tests pin the exposed contract: the two planned (n=2 heterogeneous role set; n=1 no-extra shape) plus two the implementer added that strengthen the headline — arbitrary-arity per-node namespacing (n=3, two VolSlippageCost -> distinct cost[1]/cost[2].volatility, the property that retires the 2-node cap) and geometry fan-once (n=2 extra-free -> only the 4 geometry roles). Accepted nit (held, non-gating): the CLI reconstructs the `cost[k].volatility` role name to feed the vol node's extra input — a string coupling to the composite's public role-naming convention. Kept: role-based composite wiring addresses roles by name (as the CLI already does for "closed"/"bias"/"price"); the convention is tested + build-validated; a decoupling accessor would widen this cycle's scope. Verified: cargo test --workspace (0 failures), clippy --workspace --all-targets -D warnings clean, cargo doc -p aura-composites clean. refs #148
96 lines
4.8 KiB
Rust
96 lines
4.8 KiB
Rust
//! `cost_graph` composite-builder: it fans the 4 PM-geometry inputs to N cost
|
|
//! nodes, surfaces each node's extra inputs as `cost[k].<port>` roles, and exposes
|
|
//! `CostSum`'s 3-field aggregate. These tests pin the exposed wiring contract (the
|
|
//! input-role set + the output triple); value byte-identity is the CLI goldens' job.
|
|
|
|
use aura_composites::cost_graph;
|
|
use aura_core::Scalar;
|
|
use aura_std::{ConstantCost, VolSlippageCost};
|
|
|
|
/// Two heterogeneous cost nodes: ConstantCost (no extras, index 0) and
|
|
/// VolSlippageCost (one extra `volatility`, index 1). The composite exposes the 4
|
|
/// geometry roles + the namespaced `cost[1].volatility`, and the 3-field aggregate.
|
|
#[test]
|
|
fn cost_graph_exposes_geometry_and_namespaced_extras() {
|
|
let cg = cost_graph(vec![
|
|
ConstantCost::builder().bind("cost_per_trade", Scalar::f64(2.0)),
|
|
VolSlippageCost::builder().bind("slip_vol_mult", Scalar::f64(0.5)),
|
|
]);
|
|
let roles: Vec<&str> = cg.input_roles().iter().map(|r| r.name.as_str()).collect();
|
|
assert_eq!(
|
|
roles,
|
|
vec!["closed", "open", "entry_price", "stop_price", "cost[1].volatility"]
|
|
);
|
|
let outs: Vec<&str> = cg.output().iter().map(|o| o.name.as_str()).collect();
|
|
assert_eq!(outs, vec!["cost_in_r", "cum_cost_in_r", "open_cost_in_r"]);
|
|
}
|
|
|
|
/// A lone cost node with no extras exposes only the 4 geometry roles (the n=1
|
|
/// shape), plus the 3-field aggregate.
|
|
#[test]
|
|
fn cost_graph_single_node_has_no_extra_roles() {
|
|
let cg = cost_graph(vec![
|
|
ConstantCost::builder().bind("cost_per_trade", Scalar::f64(2.0)),
|
|
]);
|
|
let roles: Vec<&str> = cg.input_roles().iter().map(|r| r.name.as_str()).collect();
|
|
assert_eq!(roles, vec!["closed", "open", "entry_price", "stop_price"]);
|
|
let outs: Vec<&str> = cg.output().iter().map(|o| o.name.as_str()).collect();
|
|
assert_eq!(outs, vec!["cost_in_r", "cum_cost_in_r", "open_cost_in_r"]);
|
|
}
|
|
|
|
/// Property: `cost_graph` composes an ARBITRARY number of cost nodes — the
|
|
/// cycle-0084 headline that retired the hardcoded `MAX_RUN_COST_NODES = 2` cap —
|
|
/// and namespaces each extra-bearing node's inputs by the node index, so two
|
|
/// same-typed factors never collide. Three nodes (ConstantCost at idx 0 with no
|
|
/// extras, then two VolSlippageCost at idx 1 and 2, each carrying a `volatility`
|
|
/// extra) must expose the 4 SHARED geometry roles plus the two DISTINCT
|
|
/// `cost[1].volatility` / `cost[2].volatility` roles, and still collapse to the
|
|
/// single 3-field aggregate output regardless of arity. The shipped n=2 fixture
|
|
/// has only one extra-bearing node, so it cannot witness a cross-node collision:
|
|
/// a mis-indexed `k`, or extras flattened to a bare `volatility`, would alias the
|
|
/// two nodes' inputs into one role and silently mis-feed the cost graph — this
|
|
/// n=3 two-extra fixture fails loudly on that class while the n=1/n=2 fixtures
|
|
/// stay green.
|
|
#[test]
|
|
fn cost_graph_arbitrary_arity_namespaces_each_extra_by_node_index() {
|
|
let cg = cost_graph(vec![
|
|
ConstantCost::builder().bind("cost_per_trade", Scalar::f64(2.0)),
|
|
VolSlippageCost::builder().bind("slip_vol_mult", Scalar::f64(0.5)),
|
|
VolSlippageCost::builder().bind("slip_vol_mult", Scalar::f64(0.25)),
|
|
]);
|
|
let roles: Vec<&str> = cg.input_roles().iter().map(|r| r.name.as_str()).collect();
|
|
assert_eq!(
|
|
roles,
|
|
vec![
|
|
"closed",
|
|
"open",
|
|
"entry_price",
|
|
"stop_price",
|
|
"cost[1].volatility",
|
|
"cost[2].volatility",
|
|
]
|
|
);
|
|
let outs: Vec<&str> = cg.output().iter().map(|o| o.name.as_str()).collect();
|
|
assert_eq!(outs, vec!["cost_in_r", "cum_cost_in_r", "open_cost_in_r"]);
|
|
}
|
|
|
|
/// Property: the 4 PM-geometry inputs are FANNED (shared once) across every cost
|
|
/// node, never duplicated per node — so N extra-free cost nodes expose exactly the
|
|
/// 4 geometry roles and ZERO `cost[k]` roles, independent of N. Two extra-free
|
|
/// ConstantCost nodes must expose only `[closed, open, entry_price, stop_price]`
|
|
/// (and the 3-field aggregate). The shipped single-node fixture has N=1, so it
|
|
/// cannot catch a regression that per-node-duplicated the geometry into
|
|
/// `cost[k].closed`-style roles, or that leaked a spurious `cost[k]` role for an
|
|
/// extra-free node; this multi-node extra-free fixture pins the fan-once contract.
|
|
#[test]
|
|
fn cost_graph_fans_shared_geometry_across_multiple_extra_free_nodes() {
|
|
let cg = cost_graph(vec![
|
|
ConstantCost::builder().bind("cost_per_trade", Scalar::f64(2.0)),
|
|
ConstantCost::builder().bind("cost_per_trade", Scalar::f64(1.0)),
|
|
]);
|
|
let roles: Vec<&str> = cg.input_roles().iter().map(|r| r.name.as_str()).collect();
|
|
assert_eq!(roles, vec!["closed", "open", "entry_price", "stop_price"]);
|
|
let outs: Vec<&str> = cg.output().iter().map(|o| o.name.as_str()).collect();
|
|
assert_eq!(outs, vec!["cost_in_r", "cum_cost_in_r", "open_cost_in_r"]);
|
|
}
|