feat(real-family): --real parser + dispatch wiring; complete the feature
Plan 0060 Tasks 4-5 (#106): wire `aura sweep|walkforward --real` end to end. - A shared RealWindowGrammar accumulator holds the `--real <SYMBOL> [--from <ms>] [--to <ms>]` grammar (flag-repeat strictness, empty-symbol + window-without-real rejection) for both parsers, mirroring parse_real_args. - parse_sweep_args grows the DataChoice 4th element; parse_walkforward_args is new (no --strategy axis). The sweep + walkforward dispatch arms become rest-matched, build the DataSource via from_choice (build-or-refuse on un-vetted symbol / absent data), and run. USAGE documents the --real tails. mc stays exact-matched, so `aura mc --real` falls through to usage + exit(2) — Fork A: MC excluded (its seed varies a synthetic price-walk, undefined over real data). - Gated integration tests (skip on no local data): sweep --real EURUSD persists 4 portable member dirs over real bars and charts; walkforward --real EURUSD persists one oos<ns> dir per rolling window; same real sweep twice is byte-identical (C1). Un-gated: un-vetted symbol, window-without-real, and mc --real each refuse with exit 2. - Remove the now-redundant `#[allow(dead_code)]` from the Task-1 provider: every const, both enums, and every method are live once the wiring lands. Verified end-to-end over real EURUSD data: sweep --real -> 4 members; walkforward --real over 2024 -> 9 rolling OOS windows; all refusals exit 2. Full workspace test + clippy -D warnings green. closes #106
This commit is contained in:
@@ -1007,3 +1007,195 @@ fn momentum_longonly_true_clamps_exposure_nonnegative_false_keeps_negatives() {
|
||||
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
// --- Real-data family path (#106, plan 0060 Task 5) -------------------------
|
||||
//
|
||||
// These drive `aura sweep|walkforward --real <SYMBOL>` end to end over the local
|
||||
// data-server archive. The gated tests need the Pepperstone M1 archive present;
|
||||
// they skip (print + pass) on a data-less machine, the project's
|
||||
// skip-on-no-data convention. The pip-refusal test is NOT gated: the un-specced
|
||||
// symbol is rejected before any data access.
|
||||
|
||||
// EURUSD 2024-06 (UTC inclusive ms): a bounded vetted real window.
|
||||
const EURUSD_JUN2024_FROM_MS: i64 = 1_717_200_000_000;
|
||||
const EURUSD_JUN2024_TO_MS: i64 = 1_719_791_999_999;
|
||||
|
||||
// EURUSD 2024 full year (UTC inclusive ms): 2024-01-01 00:00:00.000 ..
|
||||
// 2024-12-31 23:59:59.999 — a span wide enough for several 90/30/30-day
|
||||
// walk-forward windows.
|
||||
const EURUSD_2024_FROM_MS: i64 = 1_704_067_200_000;
|
||||
const EURUSD_2024_TO_MS: i64 = 1_735_689_599_999;
|
||||
|
||||
fn local_data_present() -> bool {
|
||||
std::path::Path::new(data_server::DEFAULT_DATA_PATH).is_dir()
|
||||
}
|
||||
|
||||
/// Property: `aura sweep --real EURUSD` runs the whole built-in grid over REAL M1
|
||||
/// close bars (not the synthetic VecSource), persisting one filesystem-portable
|
||||
/// member dir per grid point — the four-point SMA grid lands as four
|
||||
/// `[A-Za-z0-9._-]` dirs under `runs/traces/<name>/`, and one of them charts as a
|
||||
/// self-contained uPlot page. This pins the real source actually flowing through
|
||||
/// the family builders (Tasks 2-4) at the built-binary boundary, gated on local
|
||||
/// data so it never fails on a data-less machine.
|
||||
#[test]
|
||||
fn sweep_real_persists_portable_member_dirs_over_real_bars() {
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let dir = temp_cwd("sweep_real");
|
||||
let out = std::process::Command::new(BIN)
|
||||
.args(["sweep", "--real", "EURUSD",
|
||||
"--from", &EURUSD_JUN2024_FROM_MS.to_string(),
|
||||
"--to", &EURUSD_JUN2024_TO_MS.to_string(),
|
||||
"--trace", "swpr"])
|
||||
.current_dir(&dir).output().unwrap();
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let members: Vec<_> = std::fs::read_dir(dir.join("runs/traces/swpr")).unwrap()
|
||||
.map(|e| e.unwrap().file_name().into_string().unwrap()).collect();
|
||||
assert_eq!(members.len(), 4, "four sweep members: {members:?}");
|
||||
assert!(members.iter().all(|k| k.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))),
|
||||
"all keys filesystem-portable: {members:?}");
|
||||
// one member charts as a uPlot page
|
||||
let key = &members[0];
|
||||
let chart = std::process::Command::new(BIN)
|
||||
.args(["chart", &format!("swpr/{key}")]).current_dir(&dir).output().unwrap();
|
||||
assert!(chart.status.success(), "stderr: {}", String::from_utf8_lossy(&chart.stderr));
|
||||
assert!(String::from_utf8_lossy(&chart.stdout).contains("uPlot"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property: `aura walkforward --real EURUSD` over a full year of real bars rolls
|
||||
/// the real-data window roller (90/30/30-day, ns-stamped) into SEVERAL OOS
|
||||
/// windows, persisting one `oos<ns>`-keyed member dir per window. Pins that the
|
||||
/// windowed real path draws its span from the probed `--from..--to` (not the
|
||||
/// synthetic showcase span) and that the roller produces a multi-window family
|
||||
/// over real data, gated on the local archive.
|
||||
#[test]
|
||||
fn walkforward_real_persists_one_oos_member_per_window() {
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let dir = temp_cwd("wf_real");
|
||||
// a full year so 90/30/30-day rolling fits several windows
|
||||
let out = std::process::Command::new(BIN)
|
||||
.args(["walkforward", "--real", "EURUSD",
|
||||
"--from", &EURUSD_2024_FROM_MS.to_string(),
|
||||
"--to", &EURUSD_2024_TO_MS.to_string(),
|
||||
"--trace", "wfr"])
|
||||
.current_dir(&dir).output().unwrap();
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let members: Vec<_> = std::fs::read_dir(dir.join("runs/traces/wfr")).unwrap()
|
||||
.map(|e| e.unwrap().file_name().into_string().unwrap()).collect();
|
||||
assert!(members.iter().all(|k| k.starts_with("oos")), "oos<ns> member dirs: {members:?}");
|
||||
assert!(members.len() >= 2, "several OOS windows over a year: {members:?}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property: the per-instrument pip refusal fires for `aura sweep --real
|
||||
/// <unspecced>` exactly as it does for `aura run --real` — an un-vetted symbol
|
||||
/// (`AAPL.US`, absent from the instrument table) is rejected with exit 2 and the
|
||||
/// "no vetted pip" message BEFORE any data access. NOT gated: the refusal
|
||||
/// precedes the archive entirely, so it is CI-safe and runs on every machine.
|
||||
#[test]
|
||||
fn sweep_real_unspecced_symbol_refuses_with_exit_2() {
|
||||
// not gated: the pip refusal happens before any data access.
|
||||
let dir = temp_cwd("sweep_real_refuse");
|
||||
let out = std::process::Command::new(BIN)
|
||||
.args(["sweep", "--real", "AAPL.US"]).current_dir(&dir).output().unwrap();
|
||||
assert_eq!(out.status.code(), Some(2));
|
||||
assert!(String::from_utf8_lossy(&out.stderr).contains("no vetted pip"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property (C1): the same real sweep run twice is byte-identical — re-running
|
||||
/// `aura sweep --real EURUSD` over the same bounded window produces an identical
|
||||
/// report body. The whole determinism contract rests on a real backtest being
|
||||
/// reproducible; a thread-race in the real source ingestion or member fold would
|
||||
/// surface as a drift here. Gated on the local archive.
|
||||
#[test]
|
||||
fn sweep_real_is_byte_deterministic_across_runs() {
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let run = || {
|
||||
// a fresh wiped cwd per call -> an empty registry each run, so the
|
||||
// `family_id` ordinal is `sweep-0` for both; the raw stdout bodies are
|
||||
// directly comparable with no ordinal normalisation needed.
|
||||
let dir = temp_cwd("sweep_real_det");
|
||||
let o = std::process::Command::new(BIN)
|
||||
.args(["sweep", "--real", "EURUSD",
|
||||
"--from", &EURUSD_JUN2024_FROM_MS.to_string(),
|
||||
"--to", &EURUSD_JUN2024_TO_MS.to_string()])
|
||||
.current_dir(&dir).output().unwrap();
|
||||
let body = String::from_utf8_lossy(&o.stdout).into_owned();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
body
|
||||
};
|
||||
assert_eq!(run(), run(), "same real sweep twice must be byte-identical (C1)");
|
||||
}
|
||||
|
||||
/// Property (spec-0060 MC exclusion): `aura mc` admits NO `--real` flag. The
|
||||
/// spec carves MC out of the real-data path — its seed varies a *synthetic*
|
||||
/// price-walk realization, undefined over real bars (one realization -> identical
|
||||
/// members). The observable contract is that `aura mc --real EURUSD` is REJECTED
|
||||
/// (usage on stderr, exit 2) at the binary boundary, never silently accepted as a
|
||||
/// real run. This pins the exclusion: if an `["mc", rest @ ..]` arm ever wired
|
||||
/// `--real` into MC, this test fails loudly. NOT gated — the refusal precedes any
|
||||
/// data access, so it is CI-safe on every machine.
|
||||
#[test]
|
||||
fn mc_rejects_real_flag_with_usage_exit_2() {
|
||||
let dir = temp_cwd("mc_no_real");
|
||||
let out = Command::new(BIN)
|
||||
.args(["mc", "--real", "EURUSD"])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura mc --real");
|
||||
assert_eq!(out.status.code(), Some(2), "mc --real must exit 2; status: {:?}", out.status);
|
||||
assert!(out.stdout.is_empty(), "mc --real must not emit a run on stdout: {:?}", out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("usage"), "mc --real stderr must carry usage: {stderr:?}");
|
||||
// the exclusion is total: no `runs/` registry write either.
|
||||
assert!(!dir.join("runs").exists(), "mc --real must not start a real run");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property (window-flags-require-`--real`, at the binary boundary): a family
|
||||
/// command given a `--from`/`--to` window WITHOUT `--real` is a usage error —
|
||||
/// there is no synthetic window knob, so `aura sweep --from 100` and
|
||||
/// `aura walkforward --to 200` exit 2 (usage on stderr), never a silent synthetic
|
||||
/// run over the whole built-in stream. The pure grammar is unit-tested in
|
||||
/// `main.rs`; this pins that the `Err` arm actually wires through to `exit(2)` at
|
||||
/// the dispatch for BOTH family commands. NOT gated — the refusal is pure
|
||||
/// argument parsing, before any data access.
|
||||
#[test]
|
||||
fn family_window_flag_without_real_refuses_with_usage_exit_2() {
|
||||
// (subcommand args, the subcommand token the usage message must name)
|
||||
for (args, cmd) in [
|
||||
(&["sweep", "--from", "100"][..], "sweep"),
|
||||
(&["sweep", "--to", "200"][..], "sweep"),
|
||||
(&["walkforward", "--from", "100"][..], "walkforward"),
|
||||
(&["walkforward", "--to", "200"][..], "walkforward"),
|
||||
] {
|
||||
let dir = temp_cwd("family_window_no_real");
|
||||
let out = Command::new(BIN)
|
||||
.args(args)
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
|
||||
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
|
||||
assert!(out.stdout.is_empty(), "`aura {args:?}` must emit no run on stdout: {:?}", out.stdout);
|
||||
// the refusal carries the subcommand's usage, which names `--real` — the
|
||||
// flag the window needs and was given without.
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains(cmd), "`aura {args:?}` stderr must name `{cmd}`: {stderr:?}");
|
||||
assert!(stderr.contains("--real"), "`aura {args:?}` stderr must name `--real`: {stderr:?}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user