feat(cli): generalize no-window fallback resolves the intersection of all symbols' windows
The no---from/--to fallback in dispatch_generalize probes EACH listed symbol's full archive window (same partial bounds threaded through) and resolves the one shared campaign window as their intersection — latest start, earliest end — instead of symbols[0]'s window alone, so the cross-instrument floor is measured over a genuinely common period. Disjoint archives refuse at runtime (exit 1) naming each symbol's span. The intersect-or-refuse arithmetic lives in the pure intersect_shared_window helper (unit-tested: single symbol, overlapping, disjoint); the explicit --from+--to path stays byte-identical. The superseded shape test is renamed to generalize_without_explicit_window_resolves_a_shared_window_and_completes with its doc comment reworded to the intersection contract. closes #213
This commit is contained in:
@@ -2959,8 +2959,10 @@ fn exit_axis_register_error(e: AxisRegisterError) -> ! {
|
||||
/// seam-crossing (never reimplement the division inline) so the executor's
|
||||
/// `unix_ms_to_epoch_ns` re-normalizes exactly once downstream, not twice.
|
||||
/// Shared by the campaign dispatchers — sweep/walkforward/mc always clip;
|
||||
/// generalize reaches here only through its no-explicit-window fallback (an
|
||||
/// explicit `--from`+`--to` pair passes through unclipped there BY DESIGN).
|
||||
/// generalize reaches here once per listed symbol, only through its
|
||||
/// no-explicit-window fallback, and intersects the per-symbol results into the
|
||||
/// one shared window (#213); an explicit `--from`+`--to` pair passes through
|
||||
/// unclipped there BY DESIGN.
|
||||
fn campaign_window_ms(choice: DataChoice, env: &project::Env) -> (i64, i64) {
|
||||
let source = DataSource::from_choice(choice, env);
|
||||
let (from_ts, to_ts) = source.full_window(env);
|
||||
@@ -2970,6 +2972,30 @@ fn campaign_window_ms(choice: DataChoice, env: &project::Env) -> (i64, i64) {
|
||||
)
|
||||
}
|
||||
|
||||
type SymbolSpans<'a> = Vec<(&'a str, (i64, i64))>;
|
||||
|
||||
/// The pure decision `generalize`'s no-explicit-window fallback reduces to once
|
||||
/// every listed symbol's full window is resolved (#213): the shared window is
|
||||
/// the intersection (latest start, earliest end); `Err` carries each symbol
|
||||
/// paired with its own resolved window when the intersection is empty (the
|
||||
/// archives never overlap), for the caller's eprintln-then-exit(1) refusal.
|
||||
/// Kept separate from `dispatch_generalize` so the intersect-or-refuse
|
||||
/// arithmetic is unit-testable on synthetic windows — real archives on this
|
||||
/// host never disjoint (every symbol's tail reaches the live present), so the
|
||||
/// refusal branch has no reachable e2e fixture.
|
||||
fn intersect_shared_window<'a>(
|
||||
symbols: &'a [String],
|
||||
windows: &[(i64, i64)],
|
||||
) -> Result<(i64, i64), SymbolSpans<'a>> {
|
||||
let shared_from = windows.iter().map(|w| w.0).max().expect("caller passes at least one window");
|
||||
let shared_to = windows.iter().map(|w| w.1).min().expect("caller passes at least one window");
|
||||
if shared_from > shared_to {
|
||||
Err(symbols.iter().map(String::as_str).zip(windows.iter().copied()).collect())
|
||||
} else {
|
||||
Ok((shared_from, shared_to))
|
||||
}
|
||||
}
|
||||
|
||||
/// `aura run`: the loaded-blueprint branch (an existing `.json` first-positional) or
|
||||
/// the built-in harness-kind dispatch.
|
||||
fn dispatch_run(a: RunCmd, env: &project::Env) {
|
||||
@@ -3124,13 +3150,35 @@ fn dispatch_generalize(a: GeneralizeCmd, env: &project::Env) {
|
||||
.unwrap_or_else(|e| exit_axis_register_error(e));
|
||||
// Window: the explicit --from/--to when both present (byte-identical to the
|
||||
// retired welded path, verified by the exact-grade anchor); otherwise the
|
||||
// first symbol's full archive window as the single shared campaign window.
|
||||
// INTERSECTION of every listed symbol's full archive window — the only span
|
||||
// over which every instrument actually has data (#213). A single listed
|
||||
// symbol degenerates to its own full window unchanged.
|
||||
let (from_ms, to_ms) = match (from, to) {
|
||||
(Some(f), Some(t)) => (f, t),
|
||||
_ => campaign_window_ms(
|
||||
DataChoice::Real { symbol: symbols[0].clone(), from_ms: from, to_ms: to },
|
||||
env,
|
||||
),
|
||||
_ => {
|
||||
let windows: Vec<(i64, i64)> = symbols
|
||||
.iter()
|
||||
.map(|s| {
|
||||
campaign_window_ms(
|
||||
DataChoice::Real { symbol: s.clone(), from_ms: from, to_ms: to },
|
||||
env,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
match intersect_shared_window(&symbols, &windows) {
|
||||
Ok(w) => w,
|
||||
Err(spans) => {
|
||||
for (s, w) in spans {
|
||||
eprintln!("aura: generalize: {s} spans [{}, {}]", w.0, w.1);
|
||||
}
|
||||
eprintln!(
|
||||
"aura: generalize: no window is shared across all listed symbols \
|
||||
(their archives do not overlap)"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let inv = verb_sugar::SugarInvocation {
|
||||
axes: &raw_axes,
|
||||
@@ -3601,6 +3649,41 @@ fn main() {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A single listed symbol degenerates to its own full window unchanged
|
||||
/// (#213) — the intersection of one window with itself is that window.
|
||||
#[test]
|
||||
fn intersect_shared_window_of_a_single_symbol_is_its_own_window() {
|
||||
let symbols = vec!["GER40".to_string()];
|
||||
let windows = vec![(100, 200)];
|
||||
assert_eq!(intersect_shared_window(&symbols, &windows), Ok((100, 200)));
|
||||
}
|
||||
|
||||
/// Overlapping multi-symbol windows resolve to the intersection — the
|
||||
/// latest start, earliest end (#213) — never `symbols[0]`'s own window.
|
||||
#[test]
|
||||
fn intersect_shared_window_of_overlapping_windows_is_latest_start_earliest_end() {
|
||||
let symbols = vec!["AAPL.US".to_string(), "GER40".to_string()];
|
||||
let windows = vec![(0, 300), (100, 200)];
|
||||
assert_eq!(intersect_shared_window(&symbols, &windows), Ok((100, 200)));
|
||||
}
|
||||
|
||||
/// Disjoint archives (no shared instant across all listed symbols) refuse
|
||||
/// rather than silently pooling a floor over different periods per
|
||||
/// instrument (#213); the error names each symbol next to its own span,
|
||||
/// which the caller eprintln's before exiting 1 — the boundary this
|
||||
/// covers is otherwise unreachable in an e2e fixture on this host, since
|
||||
/// every archived symbol's tail reaches the live present (no two windows
|
||||
/// are ever genuinely disjoint here).
|
||||
#[test]
|
||||
fn intersect_shared_window_of_disjoint_windows_refuses_with_every_span() {
|
||||
let symbols = vec!["A".to_string(), "B".to_string()];
|
||||
let windows = vec![(0, 100), (200, 300)];
|
||||
assert_eq!(
|
||||
intersect_shared_window(&symbols, &windows),
|
||||
Err(vec![("A", (0, 100)), ("B", (200, 300))]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Regenerates the shipped r_breakout examples from the carved signal. Run by hand:
|
||||
/// `cargo test --bin aura emit_r_breakout_examples -- --ignored`. The examples are
|
||||
/// green-by-construction (serialised from the builder, never hand-authored).
|
||||
|
||||
@@ -3000,18 +3000,19 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
|
||||
assert_eq!(count("campaigns"), 2, "two distinct candidates persist two distinct campaign documents");
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// Property (#210 T3, dissolution; window semantics superseded by #213):
|
||||
/// omitting `--from`/`--to` still completes — the dispatch rewrite's
|
||||
/// window-resolution fallback (`dispatch_generalize` in main.rs) resolves ONE
|
||||
/// shared campaign window from the INTERSECTION of the listed symbols' 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.
|
||||
/// independent full window). A wrong intersection (or a panic on the
|
||||
/// per-symbol probe) in that 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() {
|
||||
fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() {
|
||||
let (cwd, _g) = fresh_project();
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
|
||||
Reference in New Issue
Block a user