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(),
],
);
+4 -4
View File
@@ -635,7 +635,7 @@ fn sweep_prints_four_family_json_lines_and_exits_zero() {
// `broker` (the commit value is the volatile git HEAD, pinned by params below).
assert!(first.starts_with("{\"family_id\":\"sweep-0\",\"report\":{\"manifest\":{"), "got: {stdout}");
assert!(
first.contains("\"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}]]"),
first.contains("\"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}]]"),
"got: {stdout}"
);
let _ = std::fs::remove_dir_all(&cwd);
@@ -1035,7 +1035,7 @@ fn sweep_strategy_momentum_prints_eight_typed_bool_member_lines() {
/// Property (#105, the bool actually gates — observable on disk): the
/// `longonly.enabled` param is genuinely WIRED into the momentum harness, not a
/// no-op knob. For one grid point's two bool members (same `ema.length` +
/// `exposure.scale`, differing only in the bool), the `enabled=true` member
/// `bias.scale`, differing only in the bool), the `enabled=true` member
/// records a long-only exposure stream — every recorded value `>= 0` — while the
/// otherwise-identical `enabled=false` member records at least one NEGATIVE
/// exposure over the same deterministic showcase stream. This pins the gate's
@@ -1063,8 +1063,8 @@ fn momentum_longonly_true_clamps_exposure_nonnegative_false_keeps_negatives() {
// One grid point, its two bool members (identical but for the bool).
let base = cwd.join("runs/traces/gate");
let enabled = base.join("ema.length-5_exposure.scale-1_longonly.enabled-true");
let disabled = base.join("ema.length-5_exposure.scale-1_longonly.enabled-false");
let enabled = base.join("ema.length-5_bias.scale-1_longonly.enabled-true");
let disabled = base.join("ema.length-5_bias.scale-1_longonly.enabled-false");
let exposures = |dir: &std::path::Path| -> Vec<f64> {
let body = std::fs::read_to_string(dir.join("exposure.json")).expect("read exposure.json");
+9 -9
View File
@@ -304,7 +304,7 @@ impl Composite {
/// fluent alternative to a positional `bootstrap_with_params` vector). The
/// bound name is the exact `param_space()` name — `<node>.<param>` at every
/// level, e.g. `sma_cross.fast.length` for a composite-interior knob and
/// `exposure.scale` for a root-level knob.
/// `bias.scale` for a root-level knob.
pub fn with(self, name: &str, v: impl Into<Scalar>) -> Binder {
Binder { bp: self, bound: vec![(name.to_string(), v.into())] }
}
@@ -962,7 +962,7 @@ mod tests {
.0
.range("sma_cross.fast.length", fast)
.range("sma_cross.slow.length", slow)
.range("exposure.scale", scale)
.range("bias.scale", scale)
.sweep(count, seed, run_point)
.expect("named ranges resolve against param_space()");
@@ -972,7 +972,7 @@ mod tests {
.0
.range("sma_cross.slow.length", slow)
.range("sma_cross.fast.length", fast)
.range("exposure.scale", scale)
.range("bias.scale", scale)
.sweep(count, seed, run_point)
.expect("named ranges resolve regardless of call order");
@@ -1078,7 +1078,7 @@ mod tests {
let result = bp
.axis("sma_cross.fast.length", [2.0, 3.0]) // F64 values for the I64 slot
.axis("sma_cross.slow.length", [4])
.axis("exposure.scale", [0.5])
.axis("bias.scale", [0.5])
.sweep(|_: &[Cell]| -> RunReport { panic!("axis pre-validation must reject before running") });
assert_eq!(
result,
@@ -1100,7 +1100,7 @@ mod tests {
&[
("sma_cross.fast.length".to_string(), vec![Scalar::i64(2), Scalar::i64(3)]),
("sma_cross.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(5)]),
("exposure.scale".to_string(), vec![Scalar::f64(0.5)]),
("bias.scale".to_string(), vec![Scalar::f64(0.5)]),
],
)
.expect("named axes resolve");
@@ -1209,7 +1209,7 @@ mod tests {
let mut named = bp
.with("sma_cross.fast.length", 2)
.with("sma_cross.slow.length", 4)
.with("exposure.scale", 0.5)
.with("bias.scale", 0.5)
.bootstrap()
.expect("named binding resolves and bootstraps");
named.run(vec![Box::new(VecSource::new(synthetic_prices()))]);
@@ -2104,7 +2104,7 @@ mod tests {
// scale (F64); Sub/SimBroker/Recorder declare none
assert_eq!(
space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
["sma_cross.fast.length", "sma_cross.slow.length", "exposure.scale"],
["sma_cross.fast.length", "sma_cross.slow.length", "bias.scale"],
);
assert_eq!(
space.iter().map(|p| p.kind).collect::<Vec<_>>(),
@@ -2563,7 +2563,7 @@ mod tests {
"root",
vec![
BlueprintNode::Composite(cross),
Bias::builder().named("exposure").into(),
Bias::builder().named("bias").into(),
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
],
vec![
@@ -2583,7 +2583,7 @@ mod tests {
let mut h = bp
.with("sma_cross.fast.length", 2)
.with("sma_cross.slow.length", 4)
.with("exposure.scale", 0.5)
.with("bias.scale", 0.5)
.bootstrap()
.expect("named (injective) cross resolves by name and bootstraps");
h.run(vec![Box::new(VecSource::new(prices))]);
+1 -1
View File
@@ -236,7 +236,7 @@ mod tests {
use aura_core::Scalar;
// Shared param vector for the harness param-space (sma_cross.fast.length,
// sma_cross.slow.length, exposure.scale) — applied to both sides, so it
// sma_cross.slow.length, bias.scale) — applied to both sides, so it
// cancels in the comparison. Params size buffers, not topology (C11), so
// edges/sources are param-independent; a fixed vector just satisfies the
// arity gate compile() (no-param) trips on this value-empty harness.
+5 -5
View File
@@ -637,7 +637,7 @@ mod tests {
params: 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: (Timestamp(1), Timestamp(5)),
seed: 0,
@@ -879,7 +879,7 @@ mod tests {
params: vec![
("sma_fast".to_string(), Scalar::i64(2)),
("sma_slow".to_string(), Scalar::i64(4)),
("exposure_scale".to_string(), Scalar::f64(1.0)),
("bias_scale".to_string(), Scalar::f64(1.0)),
],
window: (Timestamp(1), Timestamp(6)),
seed: 0,
@@ -894,7 +894,7 @@ mod tests {
};
assert_eq!(
report.to_json(),
r#"{"manifest":{"commit":"abc123","params":[["sma_fast",{"I64":2}],["sma_slow",{"I64":4}],["exposure_scale",{"F64":1.0}]],"window":[1,6],"seed":0,"broker":"sim-optimal(pip_size=1.0)"},"metrics":{"total_pips":12.0,"max_drawdown":1.0,"bias_sign_flips":1}}"#,
r#"{"manifest":{"commit":"abc123","params":[["sma_fast",{"I64":2}],["sma_slow",{"I64":4}],["bias_scale",{"F64":1.0}]],"window":[1,6],"seed":0,"broker":"sim-optimal(pip_size=1.0)"},"metrics":{"total_pips":12.0,"max_drawdown":1.0,"bias_sign_flips":1}}"#,
);
}
@@ -907,7 +907,7 @@ mod tests {
params: vec![
("sma_fast".to_string(), Scalar::i64(2)),
("sma_slow".to_string(), Scalar::i64(4)),
("exposure_scale".to_string(), Scalar::f64(1.0)),
("bias_scale".to_string(), Scalar::f64(1.0)),
],
window: (Timestamp(1), Timestamp(6)),
seed: 0,
@@ -927,7 +927,7 @@ mod tests {
params: vec![
("sma_fast".to_string(), Scalar::i64(2)),
("sma_slow".to_string(), Scalar::i64(4)),
("exposure_scale".to_string(), Scalar::f64(1.0)),
("bias_scale".to_string(), Scalar::f64(1.0)),
],
window: (Timestamp(1), Timestamp(6)),
seed: 0,
+1 -1
View File
@@ -62,7 +62,7 @@ pub(crate) fn composite_sma_cross_harness() -> (
"root",
vec![
BlueprintNode::Composite(sma_cross()),
Bias::builder().named("exposure").into(),
Bias::builder().named("bias").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(),
+3 -3
View File
@@ -467,7 +467,7 @@ mod tests {
let result = WalkForwardResult {
space: vec![
ParamSpec { name: "sma.fast".to_string(), kind: ScalarKind::I64 },
ParamSpec { name: "exposure.scale".to_string(), kind: ScalarKind::F64 },
ParamSpec { name: "bias.scale".to_string(), kind: ScalarKind::F64 },
],
windows: vec![mk(8, 0.25), mk(13, 0.75)],
stitched_oos_equity: vec![],
@@ -476,14 +476,14 @@ mod tests {
result.named_params(0),
vec![
("sma.fast".to_string(), Scalar::i64(8)),
("exposure.scale".to_string(), Scalar::f64(0.25)),
("bias.scale".to_string(), Scalar::f64(0.25)),
],
);
assert_eq!(
result.named_params(1),
vec![
("sma.fast".to_string(), Scalar::i64(13)),
("exposure.scale".to_string(), Scalar::f64(0.75)),
("bias.scale".to_string(), Scalar::f64(0.75)),
],
);
}
+1 -1
View File
@@ -84,7 +84,7 @@ fn run_sample_over(prices: Vec<(Timestamp, Scalar)>) -> RunReport {
params: 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: 0,
+1 -1
View File
@@ -84,7 +84,7 @@ fn run_sample(window: (Timestamp, Timestamp), source: Box<dyn Source>) -> RunRep
params: 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: 0,
+6 -3
View File
@@ -606,9 +606,12 @@ runnable GER40 examples (already at the correct `1.0`) are routed through the sa
lookup separately (#98).
**Realization (cycle 0065 — Stage-1 R signal quality, #119/#126/#127/#128/#129).**
The Stage-1 chain above is now built. `Exposure → Bias` renames the unsized strategy
output (the node + its output field); the persisted `exposure_sign_flips` metric key,
the `SimBroker` `exposure` input slot, and the `exposure_scale` manifest param keep
the old name as **serde-stable residue** — a deferred rename tail. A **stop-rule**
output (the node + its output field); the persisted `exposure_sign_flips` metric key
(kept as a serde alias) and the `SimBroker` `exposure` input slot + the on-disk
`exposure` **tap** label (a deliberate permanent keep, #117) retain the old name. The
strategy-output **param namespace** — the `Bias` instance, its `bias.scale` knob, and
the `bias_scale` manifest param — was the deferred rename tail, **completed in #134**
(the knob path is a runtime address, so no on-disk back-compat break). A **stop-rule**
defines 1R: `FixedStop` (a triggered-constant primitive) and a `vol_stop(length, k)`
**composition** `k·√EMA(Δ²)` (the originally-fused node was corrected to a composition
of new `Mul`/`Sqrt` primitives — a node is a primitive only if not DAG-expressible