feat(0083): CostNode trait + shared cost-record contract

Lift the duplicated cost-node skeleton — shared verbatim by ConstantCost and
VolSlippageCost and locked by convention against CostSum — into one abstraction:

- A new aura-std/src/cost.rs owns the cost-record contract (COST_WIDTH,
  COST_FIELD_NAMES, GEOMETRY_WIDTH), the CostNode factor trait (one hook:
  cost_numerator, in price units), the generic CostRunner<F> adapter that holds
  the shared state (cum, out) and the co-temporality skeleton (geometry gating,
  numerator/latched R-normalization, closed/open charge, 3-field emit), and a
  cost_node_builder schema assembler.
- ConstantCost and VolSlippageCost become thin CostNode factors; their new()
  returns CostRunner<Self>, so every existing call site and unit test binds
  transparently and stays green verbatim.
- CostSum and main.rs drop their local triple consts for the shared
  COST_FIELD_NAMES — the cycle-2 audit-flagged by-convention 3-field lockstep is
  now a structural single-source contract (the four copies of the triple collapse
  to one). main.rs also reads COST_WIDTH in place of the literal slot stride.

Behaviour-preserving: the builders emit unchanged schemas (same port names), so
the wiring, the net_r_equity seam, and summarize_r are untouched; the
numerator/latched token form is preserved verbatim (IEEE-754 byte-identity). The
existing suite is the regression net — all green: the per-node unit tests pass
verbatim (0.5/1.5, 0.375/1.375), the composition E2E exact, the C18 no-cost
golden byte-identical. Two new CLI characterization goldens pin the exact
net_expectancy_r of the flat and composed cost paths (the prior tests only
asserted net < gross, which a numerator drift would pass silently). New cost.rs
runner/contract tests + the HalfSpreadCost author doctest cover the new surface.

Deviation from the plan: CostNode::name() was dropped (the plan over-specified
it) — it is dead surface, nothing consumes it (CostRunner forwards label();
cost_node_builder takes the name as an explicit arg). Removed from the trait,
both impls, the doctest, and the test stubs; build + suite + clippy green.

Verified: cargo build --workspace --all-targets clean; cargo test --workspace
0 failures; cargo clippy --workspace --all-targets -- -D warnings clean; doctest
green.

refs #148
This commit is contained in:
2026-06-28 19:26:34 +02:00
parent c787488e64
commit 6b53c239dd
7 changed files with 517 additions and 168 deletions
+7 -11
View File
@@ -31,7 +31,7 @@ use aura_registry::{
use aura_std::{
Add, Bias, ConstantCost, CostSum, Delay, Ema, GatedRecorder, Gt, Latch, LinComb, LongOnly, Mul,
Recorder, RollingMax, RollingMin, SeriesReducer, SimBroker, Sma, Sqrt, Sub, VolSlippageCost,
PM_FIELD_NAMES, PM_RECORD_KINDS,
COST_FIELD_NAMES, COST_WIDTH, PM_FIELD_NAMES, PM_RECORD_KINDS,
};
use std::sync::mpsc::{self, Receiver};
use std::sync::LazyLock;
@@ -2575,18 +2575,14 @@ const SLIP_VOL_LENGTH: i64 = 5;
/// vol slippage). Sizes the interned `CostSum` input-port names below.
const MAX_RUN_COST_NODES: usize = 2;
/// The 3-field cost-in-R record order — a lockstep contract with each cost node's
/// output schema and `CostSum`'s inputs (and `summarize_r`'s `cost_col`).
const COST_FIELDS: [&str; 3] = ["cost_in_r", "cum_cost_in_r", "open_cost_in_r"];
/// Interned `cost[k].<field>` `CostSum` input-port names, built once. Same
/// `&'static str`-from-a-static rationale as `COL_PORTS`: `GraphBuilder::input`
/// wants `&'static str`, so the names live in a `static` rather than being
/// `format!(...).leak()`ed per build.
static COST_SUM_PORTS: LazyLock<Vec<String>> = LazyLock::new(|| {
let mut v = Vec::with_capacity(MAX_RUN_COST_NODES * 3);
let mut v = Vec::with_capacity(MAX_RUN_COST_NODES * COST_WIDTH);
for k in 0..MAX_RUN_COST_NODES {
for field in COST_FIELDS {
for field in COST_FIELD_NAMES {
v.push(format!("cost[{k}].{field}"));
}
}
@@ -2762,8 +2758,8 @@ fn stage1_r_graph(
g.connect(exec.output("open"), cc.input("open"));
g.connect(exec.output("entry_price"), cc.input("entry_price"));
g.connect(exec.output("stop_price"), cc.input("stop_price"));
for (f, field) in COST_FIELDS.iter().copied().enumerate() {
g.connect(cc.output(field), agg.input(COST_SUM_PORTS[slot * 3 + f].as_str()));
for (f, field) in COST_FIELD_NAMES.iter().copied().enumerate() {
g.connect(cc.output(field), agg.input(COST_SUM_PORTS[slot * COST_WIDTH + f].as_str()));
}
slot += 1;
}
@@ -2776,8 +2772,8 @@ fn stage1_r_graph(
g.connect(exec.output("entry_price"), vs.input("entry_price"));
g.connect(exec.output("stop_price"), vs.input("stop_price"));
g.connect(vrange.output("value"), vs.input("volatility"));
for (f, field) in COST_FIELDS.iter().copied().enumerate() {
g.connect(vs.output(field), agg.input(COST_SUM_PORTS[slot * 3 + f].as_str()));
for (f, field) in COST_FIELD_NAMES.iter().copied().enumerate() {
g.connect(vs.output(field), agg.input(COST_SUM_PORTS[slot * COST_WIDTH + f].as_str()));
}
slot += 1;
}