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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1671,6 +1671,63 @@ fn stage1_r_both_costs_compose_net_below_each_alone() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Golden characterization (cycle 0083, the CostNode-trait migration): pins the
|
||||
/// EXACT `net_expectancy_r` of `aura run --harness stage1-r --cost-per-trade 2`, so
|
||||
/// the ConstantCost-through-CostRunner migration (Task 2) is VERIFIED byte-preserving
|
||||
/// at the observable boundary, not assumed. The sibling
|
||||
/// `stage1_r_cost_run_persists_net_r_equity_and_charges_cost` only asserts the
|
||||
/// RELATION `net < gross`; a regression that drifted the cost numerator or the
|
||||
/// `cost_per_trade / |entry - stop|` R-normalization (the token form the cycle
|
||||
/// promises to keep verbatim) would keep `net < gross` true and pass that test
|
||||
/// silently while shifting this value. Pins only the cost-affected float — gross
|
||||
/// `expectancy_r` is already pinned by `stage1_r_single_run_output_golden` and is
|
||||
/// unchanged by cost. Deterministic over the fixed synthetic stream (C1).
|
||||
#[test]
|
||||
fn stage1_r_flat_cost_net_expectancy_r_golden() {
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", "--harness", "stage1-r", "--cost-per-trade", "2"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "exit: {:?}; stderr: {}", out.status,
|
||||
String::from_utf8_lossy(&out.stderr));
|
||||
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
|
||||
let net = v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap();
|
||||
assert_eq!(
|
||||
net, -614.3134020253314,
|
||||
"flat-cost net_expectancy_r drifted from the golden — the ConstantCost \
|
||||
factor migration is not byte-preserving: {s}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Golden characterization (cycle 0083, the composed cost path): pins the EXACT
|
||||
/// `net_expectancy_r` of `aura run --harness stage1-r --cost-per-trade 2
|
||||
/// --slip-vol-mult 0.5`, so the VolSlippageCost-through-CostRunner migration (Task 3)
|
||||
/// AND the CostSum per-field aggregation reading the shared `COST_FIELD_NAMES`
|
||||
/// contract (Task 4) are VERIFIED byte-preserving end to end. The sibling
|
||||
/// `stage1_r_both_costs_compose_net_below_each_alone` only asserts the RELATION
|
||||
/// `net_both < net_flat`; a drift in the vol-scaled numerator, the CostSum field
|
||||
/// order, or the geometry-prefix slot offsets (`slot * COST_WIDTH + f`) would keep
|
||||
/// that relation true and pass silently while shifting this value. Deterministic
|
||||
/// over the fixed synthetic stream (C1).
|
||||
#[test]
|
||||
fn stage1_r_composed_cost_net_expectancy_r_golden() {
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", "--harness", "stage1-r", "--cost-per-trade", "2", "--slip-vol-mult", "0.5"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "exit: {:?}; stderr: {}", out.status,
|
||||
String::from_utf8_lossy(&out.stderr));
|
||||
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
|
||||
let net = v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap();
|
||||
assert_eq!(
|
||||
net, -615.0304388396047,
|
||||
"composed-cost net_expectancy_r drifted from the golden — the VolSlippageCost \
|
||||
factor + CostSum aggregation is not byte-preserving: {s}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (the spec's dual-yardstick headline): `aura run --harness stage1-r` scores
|
||||
/// ONE signal in BOTH yardsticks at once — the single emitted `RunReport` carries the pip
|
||||
/// `metrics.total_pips` (off the SimBroker branch) AND the nested `r` block (off the
|
||||
|
||||
Reference in New Issue
Block a user