test(cli): RED — no-window generalize resolves symbols[0], not the intersection
The new archive-gated e2e pins the #213 contract: a multi-symbol generalize without --from/--to must resolve the shared campaign window as the intersection (latest start, earliest end) of the listed symbols' full archive windows. On the current tree it resolves symbols[0]'s full window (AAPL.US listed first keeps AAPL's wider span on both bounds), so the test fails RED. Expected windows are derived from the live archive via single-symbol sweep probes, never hard-coded. refs #213
This commit is contained in:
@@ -3039,6 +3039,114 @@ fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_windo
|
|||||||
assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}");
|
assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Property (#213, no-window semantics): invoked WITHOUT `--from`/`--to`, a
|
||||||
|
/// multi-symbol `generalize` resolves the ONE shared campaign window as the
|
||||||
|
/// INTERSECTION of the listed symbols' full archive windows — the latest start
|
||||||
|
/// and earliest end, i.e. the only span over which EVERY listed instrument
|
||||||
|
/// actually has data. `generalize` is the cross-instrument comparison (the
|
||||||
|
/// worst-case R floor, #146); a floor pooled over a different period per
|
||||||
|
/// instrument conflates the instrument axis with the period axis, so the common
|
||||||
|
/// window is the only one that isolates what the verb measures. Today's fallback
|
||||||
|
/// picks `symbols[0]`'s full window alone (the superseded shape test above):
|
||||||
|
/// listing the WIDER symbol first (`AAPL.US`, archive from 2006) keeps AAPL's
|
||||||
|
/// early start, whereas the intersection with the later-starting `GER40` (archive
|
||||||
|
/// from 2014) must start at GER40's first bar and end at whichever archive ends
|
||||||
|
/// first — so on the current tree the resolved window differs from the
|
||||||
|
/// intersection on both bounds. Each symbol's full window is resolved
|
||||||
|
/// independently here: a single-symbol `sweep` with no window records it into its
|
||||||
|
/// generated campaign document, in the same Unix-ms currency and via the same
|
||||||
|
/// `campaign_window_ms(full_window)` probe generalize itself resolves against —
|
||||||
|
/// so the expected intersection is derived from the live archive, never a
|
||||||
|
/// hard-coded span that shifts as the archive grows. Archive-gated; skips
|
||||||
|
/// cleanly on a data-less host.
|
||||||
|
#[test]
|
||||||
|
fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() {
|
||||||
|
if !local_data_present() {
|
||||||
|
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (cwd, _g) = fresh_project();
|
||||||
|
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|
||||||
|
// The resolved shared window persists verbatim in the generated campaign
|
||||||
|
// document; read the (from_ms, to_ms) of the one whose `name` field matches.
|
||||||
|
let window_of = |name: &str| -> (i64, i64) {
|
||||||
|
let dir = cwd.join("runs").join("campaigns");
|
||||||
|
for entry in std::fs::read_dir(&dir).expect("campaigns dir exists after a run") {
|
||||||
|
let path = entry.expect("readable dir entry").path();
|
||||||
|
let doc: serde_json::Value =
|
||||||
|
serde_json::from_str(&std::fs::read_to_string(&path).expect("read campaign doc"))
|
||||||
|
.expect("campaign doc parses as JSON");
|
||||||
|
if doc["name"].as_str() == Some(name) {
|
||||||
|
let w = &doc["data"]["windows"][0];
|
||||||
|
return (
|
||||||
|
w["from_ms"].as_i64().expect("from_ms is an integer"),
|
||||||
|
w["to_ms"].as_i64().expect("to_ms is an integer"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("no campaign document named {name:?} in {}", dir.display());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve one symbol's full archive window by running a single-symbol,
|
||||||
|
// single-cell sweep with NO --from/--to; the window it records is exactly the
|
||||||
|
// `campaign_window_ms(full_window)` value generalize resolves per symbol.
|
||||||
|
let probe = |symbol: &str, name: &str| -> (i64, i64) {
|
||||||
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
|
.args([
|
||||||
|
"sweep", &fixture, "--real", symbol,
|
||||||
|
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||||
|
"--name", name,
|
||||||
|
])
|
||||||
|
.current_dir(cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura sweep");
|
||||||
|
assert_eq!(
|
||||||
|
out.status.code(), Some(0),
|
||||||
|
"single-symbol window-probe sweep for {symbol} must run to exit 0: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
window_of(name)
|
||||||
|
};
|
||||||
|
|
||||||
|
let aapl = probe("AAPL.US", "probe-aapl");
|
||||||
|
let ger40 = probe("GER40", "probe-ger40");
|
||||||
|
// Precondition: the archives genuinely differ, so "the intersection" is a
|
||||||
|
// distinct claim from "symbols[0]'s full window" (else the test is vacuous).
|
||||||
|
assert_ne!(
|
||||||
|
aapl, ger40,
|
||||||
|
"AAPL.US {aapl:?} and GER40 {ger40:?} must span different windows for the test to bite"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The contract: the shared window is the intersection — latest start, earliest end.
|
||||||
|
let expected = (aapl.0.max(ger40.0), aapl.1.min(ger40.1));
|
||||||
|
|
||||||
|
// AAPL.US listed FIRST: today's `symbols[0]` fallback keeps AAPL's (wider)
|
||||||
|
// window, which differs from the intersection on both bounds — so a pass here
|
||||||
|
// can only mean the intersection semantics landed.
|
||||||
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
|
.args([
|
||||||
|
"generalize", &fixture, "--real", "AAPL.US,GER40",
|
||||||
|
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||||
|
"--name", "gen-both",
|
||||||
|
])
|
||||||
|
.current_dir(cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura generalize");
|
||||||
|
assert_eq!(
|
||||||
|
out.status.code(), Some(0),
|
||||||
|
"two-symbol no-window generalize must run to exit 0: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let resolved = window_of("gen-both");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolved, expected,
|
||||||
|
"no-window generalize must resolve the INTERSECTION {expected:?} of AAPL.US {aapl:?} \
|
||||||
|
and GER40 {ger40:?} — not symbols[0]'s ({aapl:?}) full window (resolved {resolved:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Property: a single-instrument generalize is refused at parse time (exit 2),
|
/// 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.
|
/// before any data access — so it asserts the arity refusal on any machine.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user