feat(cli): dissolve aura generalize into the campaign path (#210 T3)
`dispatch_generalize` synthesizes the bare `sma_signal` blueprint, resolves the
window (explicit --from/--to, else the first symbol's full window), and calls
the new `run_generalize_sugar`; the inline `run_generalize` is deleted. The
sugar builds the selection-bearing campaign doc, runs it through
`run_campaign_returning`, reprints the byte-identical `{"generalize":…}` +
`{"family_id":…}` lines, and persists the CrossInstrument family.
The exact-grade anchor stays green through the path shift — proving the two stop
mechanisms (former grid axis, now the risk-regime seam -> StopRule::Vol) yield
identical R. The family pin relaxes to "exactly one CrossInstrument family"
since the campaign executor now also persists a per-instrument Sweep family per
cell (the durable audit trail; #210 Q4). Two new e2e pin the window fallback and
the family member attribution order.
Deviations from the plan (necessary, verified): cross_instrument_members returns
Vec<aura_engine::RunReport> (aura_registry::RunReport is not pub); a shared
validate_before_register helper is factored out of run_sweep_sugar (DRY); the
run_generalize doc-comment and the now-unused `generalization` import removed.
refs #210
This commit is contained in:
@@ -3637,6 +3637,45 @@ fn generalize_real_e2e_pins_the_exact_current_grade() {
|
||||
assert_eq!(per[1][1].as_f64(), Some(0.005795903617609842), "USDJPY expectancy R: {grade_line}");
|
||||
}
|
||||
|
||||
/// Property (#210 T3, dissolution): omitting `--from`/`--to` still completes —
|
||||
/// the dispatch rewrite's window-resolution fallback (`dispatch_generalize` in
|
||||
/// main.rs) resolves ONE shared campaign window from the FIRST listed symbol's
|
||||
/// full archive geometry and applies it to every instrument in the campaign
|
||||
/// document (a `CampaignDoc` carries a single shared window, unlike the old
|
||||
/// inline `run_generalize`, which let each instrument resolve its OWN
|
||||
/// independent full window). A wrong index or an empty-`symbols` panic in that
|
||||
/// new fallback would only surface when `--from`/`--to` are absent — every
|
||||
/// other generalize e2e pins the explicit-window path, leaving this one
|
||||
/// unexercised. Gated on local GER40/USDJPY data.
|
||||
#[test]
|
||||
fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_window() {
|
||||
let cwd = temp_cwd("generalize-default-window");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40/USDJPY data");
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
|
||||
assert!(stdout.contains("\"n_instruments\":2"), "two instruments graded: {stdout}");
|
||||
assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}");
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property: a single-instrument generalize is refused at parse time (exit 2),
|
||||
/// before any data access — so it asserts the arity refusal on any machine.
|
||||
#[test]
|
||||
@@ -3785,10 +3824,91 @@ fn generalize_persists_a_discoverable_cross_instrument_family() {
|
||||
.expect("spawn families");
|
||||
assert!(fams.status.success(), "families exit: {:?}", fams.status);
|
||||
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
|
||||
assert_eq!(fams_out.lines().count(), 1, "families: {fams_out:?}");
|
||||
assert!(fams_out.contains("\"family_id\":\"generalize-0\""), "families: {fams_out:?}");
|
||||
assert!(fams_out.contains("\"kind\":\"CrossInstrument\""), "families: {fams_out:?}");
|
||||
assert!(fams_out.contains("\"members\":2"), "one member per instrument: {fams_out:?}");
|
||||
// The dissolved path routes through the campaign executor, which persists a
|
||||
// per-instrument Sweep family per cell (each instrument's candidate run, now
|
||||
// a durable audit artifact) alongside the one CrossInstrument grade family
|
||||
// the sugar appends. Pin exactly one CrossInstrument family (the generalize
|
||||
// result) — not the total family count, which now includes those per-cell
|
||||
// Sweep families.
|
||||
let cross: Vec<&str> = fams_out
|
||||
.lines()
|
||||
.filter(|l| l.contains("\"kind\":\"CrossInstrument\""))
|
||||
.collect();
|
||||
assert_eq!(cross.len(), 1, "exactly one CrossInstrument family: {fams_out:?}");
|
||||
assert!(cross[0].contains("\"family_id\":\"generalize-0\""), "families: {fams_out:?}");
|
||||
assert!(cross[0].contains("\"members\":2"), "one member per instrument: {fams_out:?}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property (#210 T3, dissolution): the persisted `CrossInstrument` family's
|
||||
/// members are attributed to the RIGHT instrument in the RIGHT cell order —
|
||||
/// not merely correct in count. The dissolved path builds the family via
|
||||
/// `cross_instrument_members`, which walks `outcome.cells` and trusts their
|
||||
/// order/labelling, replacing the old `run_generalize`'s explicit
|
||||
/// `zip(symbols, members)`. A cell-ordering or index bug in the new
|
||||
/// extraction would still print the right aggregate (the campaign's own
|
||||
/// `Generalize` stage computes that independently) while silently persisting
|
||||
/// swapped or mislabelled members — a regression this test, not the exact-grade
|
||||
/// anchor, would catch. Asserts `aura runs family generalize-0` lists GER40
|
||||
/// first with the exact-grade anchor's GER40 `expectancy_r`, then USDJPY with
|
||||
/// its own. Gated on the local GER40/USDJPY data (the shared Sept-2024 window).
|
||||
#[test]
|
||||
fn generalize_family_members_are_attributed_to_the_right_instrument() {
|
||||
const FROM_MS: &str = "1725148800000";
|
||||
const TO_MS: &str = "1727740799999";
|
||||
let cwd = temp_cwd("generalize-member-attribution");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40/USDJPY data");
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
|
||||
let fam = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["runs", "family", "generalize-0"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn runs family");
|
||||
assert!(fam.status.success(), "runs family exit: {:?}", fam.status);
|
||||
let fam_out = String::from_utf8(fam.stdout).expect("utf-8");
|
||||
let members: Vec<serde_json::Value> = fam_out
|
||||
.lines()
|
||||
.filter(|l| l.starts_with('{'))
|
||||
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
|
||||
.collect();
|
||||
assert_eq!(members.len(), 2, "one member per instrument: {fam_out:?}");
|
||||
|
||||
assert_eq!(
|
||||
members[0]["manifest"]["instrument"].as_str(), Some("GER40"),
|
||||
"first member is GER40: {fam_out:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
members[0]["metrics"]["r"]["expectancy_r"].as_f64(), Some(0.01056371324510624),
|
||||
"GER40's own expectancy_r, matching the exact-grade anchor: {fam_out:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
members[1]["manifest"]["instrument"].as_str(), Some("USDJPY"),
|
||||
"second member is USDJPY: {fam_out:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
members[1]["metrics"]["r"]["expectancy_r"].as_f64(), Some(0.005795903617609842),
|
||||
"USDJPY's own expectancy_r, matching the exact-grade anchor: {fam_out:?}"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user