refactor(stage1-r): rename the strategy-output param namespace exposure -> bias

Complete the deferred rename tail from cycle 0065's #126 (which renamed only the
typed metric, keeping the param namespace uniformly "exposure" so the single-run
rename would not smuggle in a ~30-site sweep-pinned change). Now renamed together
as one consistent unit, all driven by the Bias node's instance name:

- Bias::builder().named("exposure") -> .named("bias") (the harness builders +
  the engine fixtures in test_fixtures.rs / blueprint.rs);
- the derived knob path exposure.scale -> bias.scale (Composite::param_space
  derives the knob from the node name) -- the sweep .axis/.range bindings and
  .with() points (blueprint.rs, main.rs), the walkforward ParamSpecs
  (walkforward.rs), the member_key dir names, and the JSON byte-pins in
  cli_run.rs / report.rs / main.rs tests;
- the exposure_scale manifest param label -> bias_scale (the single-run manifest
  builders + the streaming_seam / real_bars fixtures).

The rename is surgical: the three targets (named("exposure"), exposure.scale,
exposure_scale) are syntactically distinct from the deliberately-kept "exposure"
forms, which are untouched -- SimBroker's pre-reframe exposure input slot +
prev_exposure + the exposure-integral; the LongOnly / gate / latch ports named
"exposure"; and the on-disk "exposure" tap label (the recorded bias series that
feeds `chart --tap exposure`, decoupled from the node name via
ColumnarTrace::from_rows("exposure", ...)).

No on-disk back-compat break: the knob path is a runtime param address, and
recorded params in old runs.jsonl / families.jsonl are inert data strings (a
pre-existing recorded "exposure.scale" still loads, it is just data).

INDEX.md cycle-0065 realization note amended: the param namespace is recorded as
the now-completed tail; the SimBroker slot + the on-disk tap label remain the
deliberate permanent keeps (#117), and exposure_sign_flips stays a serde alias.

Verified: cargo build --workspace --all-targets clean; clippy --all-targets
-D warnings clean; full suite 514 passed / 0 failed (source + byte-pins renamed
together); live `aura run` emits "bias_scale", `aura sweep` emits "bias.scale".

closes #134
refs #126 #117
This commit is contained in:
2026-06-24 14:40:51 +02:00
parent 5c6de45b6e
commit 68605b7d88
10 changed files with 65 additions and 62 deletions
+34 -34
View File
@@ -556,7 +556,7 @@ fn run_sample(trace: Option<&str>) -> RunReport {
vec![
("sma_fast".to_string(), Scalar::i64(2)),
("sma_slow".to_string(), Scalar::i64(4)),
("exposure_scale".to_string(), Scalar::f64(0.5)),
("bias_scale".to_string(), Scalar::f64(0.5)),
],
window,
0,
@@ -593,7 +593,7 @@ fn run_sample_real(symbol: &str, from_ms: Option<i64>, to_ms: Option<i64>, trace
vec![
("sma_fast".to_string(), Scalar::i64(2)),
("sma_slow".to_string(), Scalar::i64(4)),
("exposure_scale".to_string(), Scalar::f64(0.5)),
("bias_scale".to_string(), Scalar::f64(0.5)),
],
window,
0,
@@ -949,7 +949,7 @@ fn sample_blueprint_with_sinks(pip_size: f64) -> (
let (tx_ex, rx_ex) = mpsc::channel();
let mut g = GraphBuilder::new("sample");
let sig = g.add(signals("signals"));
let exposure = g.add(Bias::builder().named("exposure"));
let exposure = g.add(Bias::builder().named("bias"));
let broker = g.add(SimBroker::builder(pip_size));
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));
@@ -994,7 +994,7 @@ fn sweep_family(trace: Option<&str>, data: &DataSource) -> SweepFamily {
.axis("signals.momentum.signal.length", [msig])
.axis("signals.blend.weights[0]", [1.0])
.axis("signals.blend.weights[1]", [1.0])
.axis("exposure.scale", [0.5]);
.axis("bias.scale", [0.5]);
let varying: HashSet<String> = binder.varying_axes().into_iter().collect();
binder
.sweep(|point| {
@@ -1023,7 +1023,7 @@ fn sweep_family(trace: Option<&str>, data: &DataSource) -> SweepFamily {
/// The signal is how far price sits above/below its own EMA (`price - ema`), bounded
/// into a directional (unsized) bias by `Bias`, then passed through the long-only
/// `LongOnly` gate. Three swept
/// knobs of three kinds — `ema.length` (i64), `exposure.scale` (f64),
/// knobs of three kinds — `ema.length` (i64), `bias.scale` (f64),
/// `longonly.enabled` (bool) — with nothing in common with the SMA-cross demo: the
/// generic-sweep proof. The exposure sink taps the FINAL (gated) exposure so the
/// bool's effect is visible in the trace.
@@ -1037,11 +1037,11 @@ fn momentum_blueprint_with_sinks(pip_size: f64) -> (
let (tx_ex, rx_ex) = mpsc::channel();
let mut g = GraphBuilder::new("momentum");
// Name the param-bearing nodes explicitly so the swept paths are guaranteed
// ema.length / exposure.scale / longonly.enabled regardless of default-name
// ema.length / bias.scale / longonly.enabled regardless of default-name
// derivation (the member-key examples + the param-space test depend on these).
let ema = g.add(Ema::builder().named("ema")); // ema.length
let dist = g.add(Sub::builder()); // momentum = price - ema
let expo = g.add(Bias::builder().named("exposure")); // exposure.scale
let expo = g.add(Bias::builder().named("bias")); // bias.scale
let gate = g.add(LongOnly::builder().named("longonly")); // longonly.enabled (the bool param)
let broker = g.add(SimBroker::builder(pip_size));
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
@@ -1059,7 +1059,7 @@ fn momentum_blueprint_with_sinks(pip_size: f64) -> (
}
/// Run the momentum strategy over its built-in grid — `ema.length ∈ {5,10}` ×
/// `exposure.scale ∈ {0.5,1.0}` × `longonly.enabled ∈ {true,false}` = 8 points,
/// `bias.scale ∈ {0.5,1.0}` × `longonly.enabled ∈ {true,false}` = 8 points,
/// all three axes varying. Mirrors `sweep_family`: capture the binder's varying
/// axes, key each member via the generic portable `member_key`. With `--trace`,
/// persist each member under `runs/traces/<name>/<member_key>/`.
@@ -1070,7 +1070,7 @@ fn momentum_sweep_family(trace: Option<&str>, data: &DataSource) -> SweepFamily
let space = bp.param_space();
let binder = bp
.axis("ema.length", [5, 10])
.axis("exposure.scale", [0.5, 1.0])
.axis("bias.scale", [0.5, 1.0])
.axis("longonly.enabled", [true, false]);
let varying: HashSet<String> = binder.varying_axes().into_iter().collect();
binder
@@ -1315,7 +1315,7 @@ fn sweep_over(from: Timestamp, to: Timestamp, data: &DataSource) -> SweepFamily
.axis("signals.momentum.signal.length", [msig])
.axis("signals.blend.weights[0]", [1.0])
.axis("signals.blend.weights[1]", [1.0])
.axis("exposure.scale", [0.5])
.axis("bias.scale", [0.5])
.sweep(|point| {
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks(pip);
let mut h = bp
@@ -1438,7 +1438,7 @@ fn mc_family(trace: Option<&str>) -> McFamily {
vec![
("sma_fast".to_string(), Scalar::i64(2)),
("sma_slow".to_string(), Scalar::i64(4)),
("exposure_scale".to_string(), Scalar::f64(0.5)),
("bias_scale".to_string(), Scalar::f64(0.5)),
],
window,
seed,
@@ -1600,7 +1600,7 @@ fn macd_strategy_blueprint(
) -> Composite {
let mut g = GraphBuilder::new("macd_strategy");
let macd_node = g.add(macd("macd"));
let exposure = g.add(Bias::builder().named("exposure"));
let exposure = g.add(Bias::builder().named("bias"));
let broker = g.add(SimBroker::builder(SYNTHETIC_PIP_SIZE));
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));
@@ -1674,7 +1674,7 @@ fn run_macd(trace: Option<&str>) -> RunReport {
("ema_fast".to_string(), Scalar::i64(2)),
("ema_slow".to_string(), Scalar::i64(4)),
("ema_signal".to_string(), Scalar::i64(3)),
("exposure_scale".to_string(), Scalar::f64(0.5)),
("bias_scale".to_string(), Scalar::f64(0.5)),
],
window,
0,
@@ -1737,7 +1737,7 @@ fn stage1_r_blueprint(
let fast = g.add(Sma::builder().named("fast").bind("length", Scalar::i64(2)));
let slow = g.add(Sma::builder().named("slow").bind("length", Scalar::i64(4)));
let spread = g.add(Sub::builder());
let exposure = g.add(Bias::builder().named("exposure").bind("scale", Scalar::f64(0.5)));
let exposure = g.add(Bias::builder().named("bias").bind("scale", Scalar::f64(0.5)));
// pip branch (verbatim from sample_blueprint_with_sinks).
let broker = g.add(SimBroker::builder(SYNTHETIC_PIP_SIZE));
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
@@ -1824,7 +1824,7 @@ fn run_stage1_r(data: RunData, trace: Option<&str>) -> RunReport {
vec![
("sma_fast".to_string(), Scalar::i64(2)),
("sma_slow".to_string(), Scalar::i64(4)),
("exposure_scale".to_string(), Scalar::f64(0.5)),
("bias_scale".to_string(), Scalar::f64(0.5)),
("stop_length".to_string(), Scalar::i64(STAGE1_R_STOP_LENGTH)), // vol_stop EWMA length
("stop_k".to_string(), Scalar::f64(STAGE1_R_STOP_K)), // vol_stop multiplier (1R = k·σ)
],
@@ -2394,7 +2394,7 @@ mod tests {
vec![
("sma_fast".to_string(), Scalar::i64(2)),
("sma_slow".to_string(), Scalar::i64(4)),
("exposure_scale".to_string(), Scalar::f64(0.5)),
("bias_scale".to_string(), Scalar::f64(0.5)),
],
window,
seed,
@@ -2441,7 +2441,7 @@ mod tests {
.with("signals.momentum.signal.length", 3)
.with("signals.blend.weights[0]", 1.0)
.with("signals.blend.weights[1]", 1.0)
.with("exposure.scale", 0.5)
.with("bias.scale", 0.5)
.bootstrap()
.expect("sample blueprint compiles under a valid point");
h.run(vec![Box::new(VecSource::new(showcase_prices()))]);
@@ -2460,10 +2460,10 @@ mod tests {
for line in &lines {
assert!(line.starts_with(r#"{"manifest":{"commit":""#), "not a RunReport: {line}");
}
assert!(lines[0].contains(r#""params":[["signals.trend.fast.length",{"I64":2}],["signals.trend.slow.length",{"I64":4}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["exposure.scale",{"F64":0.5}]]"#), "line0: {}", lines[0]);
assert!(lines[1].contains(r#""params":[["signals.trend.fast.length",{"I64":2}],["signals.trend.slow.length",{"I64":5}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["exposure.scale",{"F64":0.5}]]"#), "line1: {}", lines[1]);
assert!(lines[2].contains(r#""params":[["signals.trend.fast.length",{"I64":3}],["signals.trend.slow.length",{"I64":4}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["exposure.scale",{"F64":0.5}]]"#), "line2: {}", lines[2]);
assert!(lines[3].contains(r#""params":[["signals.trend.fast.length",{"I64":3}],["signals.trend.slow.length",{"I64":5}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["exposure.scale",{"F64":0.5}]]"#), "line3: {}", lines[3]);
assert!(lines[0].contains(r#""params":[["signals.trend.fast.length",{"I64":2}],["signals.trend.slow.length",{"I64":4}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["bias.scale",{"F64":0.5}]]"#), "line0: {}", lines[0]);
assert!(lines[1].contains(r#""params":[["signals.trend.fast.length",{"I64":2}],["signals.trend.slow.length",{"I64":5}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["bias.scale",{"F64":0.5}]]"#), "line1: {}", lines[1]);
assert!(lines[2].contains(r#""params":[["signals.trend.fast.length",{"I64":3}],["signals.trend.slow.length",{"I64":4}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["bias.scale",{"F64":0.5}]]"#), "line2: {}", lines[2]);
assert!(lines[3].contains(r#""params":[["signals.trend.fast.length",{"I64":3}],["signals.trend.slow.length",{"I64":5}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["bias.scale",{"F64":0.5}]]"#), "line3: {}", lines[3]);
for line in &lines {
assert!(line.contains(r#""total_pips":"#), "missing total_pips: {line}");
assert!(line.contains(r#""max_drawdown":"#), "missing max_drawdown: {line}");
@@ -2565,7 +2565,7 @@ mod tests {
/// EMA `length` slots by node name to `macd.fast.length` / `macd.slow.length` /
/// `macd.signal.length` — the named composite boundary visible end-to-end through
/// `param_space()`, with the slot count and order unchanged (C23 — node names are
/// non-load-bearing: every interior slot stays sweepable, the `exposure.scale`
/// non-load-bearing: every interior slot stays sweepable, the `bias.scale`
/// knob is unaffected).
#[test]
fn macd_param_space_surfaces_the_three_named_legs() {
@@ -2579,7 +2579,7 @@ mod tests {
"macd.fast.length".to_string(),
"macd.slow.length".to_string(),
"macd.signal.length".to_string(),
"exposure.scale".to_string(),
"bias.scale".to_string(),
],
"MACD param surface must expose the three named EMA lengths + scale",
);
@@ -2757,13 +2757,13 @@ mod tests {
fn member_key_renders_varying_axes_portably() {
let named = vec![
pair("ema.length", Scalar::i64(5)),
pair("exposure.scale", Scalar::f64(0.5)),
pair("bias.scale", Scalar::f64(0.5)),
pair("longonly.enabled", Scalar::bool(true)),
];
let varying: std::collections::HashSet<String> =
named.iter().map(|(n, _)| n.clone()).collect();
let key = member_key(&named, &varying);
assert_eq!(key, "ema.length-5_exposure.scale-0.5_longonly.enabled-true");
assert_eq!(key, "ema.length-5_bias.scale-0.5_longonly.enabled-true");
assert!(key.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')));
}
@@ -2771,7 +2771,7 @@ mod tests {
fn member_key_omits_pinned_axes() {
let named = vec![
pair("ema.length", Scalar::i64(5)),
pair("exposure.scale", Scalar::f64(0.5)),
pair("bias.scale", Scalar::f64(0.5)),
pair("longonly.enabled", Scalar::bool(false)),
];
let mut varying = std::collections::HashSet::new();
@@ -2781,10 +2781,10 @@ mod tests {
#[test]
fn member_key_handles_negative_float_and_sanitises_names() {
let named = vec![pair("exposure.scale", Scalar::f64(-0.5))];
let named = vec![pair("bias.scale", Scalar::f64(-0.5))];
let varying: std::collections::HashSet<String> =
["exposure.scale".to_string()].into_iter().collect();
assert_eq!(member_key(&named, &varying), "exposure.scale--0.5");
["bias.scale".to_string()].into_iter().collect();
assert_eq!(member_key(&named, &varying), "bias.scale--0.5");
let named2 = vec![pair("weird key!", Scalar::f64(1.0))];
let varying2: std::collections::HashSet<String> =
@@ -2794,7 +2794,7 @@ mod tests {
#[test]
fn member_key_is_m_when_no_axis_varies() {
let named = vec![pair("exposure.scale", Scalar::f64(0.5))];
let named = vec![pair("bias.scale", Scalar::f64(0.5))];
let varying = std::collections::HashSet::new();
assert_eq!(member_key(&named, &varying), "m");
}
@@ -2816,15 +2816,15 @@ mod tests {
#[test]
fn member_key_collision_free_over_a_non_trend_axis_set() {
// #105 regression: vary exposure.scale + a bool (NOT the old hardcoded
// #105 regression: vary bias.scale + a bool (NOT the old hardcoded
// trend.fast/slow). Distinct points -> distinct keys; the old f…s… key
// would have collapsed them all to one dir.
let varying: std::collections::HashSet<String> =
["exposure.scale".to_string(), "longonly.enabled".to_string()].into_iter().collect();
["bias.scale".to_string(), "longonly.enabled".to_string()].into_iter().collect();
let p = |s: f64, b: bool| {
vec![
pair("ema.length", Scalar::i64(5)), // pinned -> omitted from key
pair("exposure.scale", Scalar::f64(s)),
pair("bias.scale", Scalar::f64(s)),
pair("longonly.enabled", Scalar::bool(b)),
]
};
@@ -2846,7 +2846,7 @@ mod tests {
names,
vec![
"ema.length".to_string(),
"exposure.scale".to_string(),
"bias.scale".to_string(),
"longonly.enabled".to_string(),
],
);