feat(cli,engine): multi-column opening on every real-data path (#231 task 4)
The six Close-only open sites route through the resolved binding's
column set via open_columns (probe_window, open_real_source,
run_sources/windowed_sources, the campaign member open, the trace
re-run open) — a strategy declaring high/low/close roles gets exactly
those columns, merged in the canonical C4 order. Synthetic data refuses
any binding beyond {close} with the honest --real remedy (the walk
generates a close series only); the sweep/mc family builders ride their
established Err contract, run/walkforward/reproduce refuse exit-1.
Proof at two layers: an engine e2e composes an OHLC blueprint from data
and runs it over four inline VecSources to hand-computed rows (the
merge-order pin), and an archive-gated CLI e2e sweeps a high/low
blueprint over GER40 opening the High and Low columns.
Verified: engine OHLC e2e green, full workspace suite green, clippy -D
warnings clean.
refs #231
This commit is contained in:
@@ -1427,6 +1427,162 @@ fn reproduce_real_walkforward_family_does_not_panic() {
|
||||
);
|
||||
}
|
||||
|
||||
// Fixture blueprints for the verb-level multi-column binding coverage below
|
||||
// (#231 task 4 quality follow-up): a CLOSED high/low blueprint (`run`
|
||||
// requires zero free knobs) and an OPEN high/low blueprint with two
|
||||
// SMA-length axes (`walkforward` requires >= 1 axis). Written to a fresh
|
||||
// temp file per test rather than a tracked `examples/` fixture — these exist
|
||||
// only to drive the refusal / real-open contract, not as authoring samples.
|
||||
const HL_RANGE_CLOSED_BLUEPRINT: &str = r#"{
|
||||
"format_version": 1,
|
||||
"blueprint": {
|
||||
"name": "hl_range",
|
||||
"nodes": [ {"primitive":{"type":"Sub"}} ],
|
||||
"edges": [],
|
||||
"input_roles": [
|
||||
{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},
|
||||
{"name":"low","targets":[{"node":0,"slot":1}],"source":"F64"}
|
||||
],
|
||||
"output": [{"node":0,"field":0,"name":"bias"}]
|
||||
}
|
||||
}"#;
|
||||
const HL_SIGNAL_OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"hl_signal","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
|
||||
/// Property (#231 task 4 quality follow-up): `aura run` over a multi-column
|
||||
/// blueprint with NO `--real` (the synthetic default) refuses through the
|
||||
/// verb-level exit-1 + stderr contract — `binding::synthetic_refusal`'s exact
|
||||
/// prose, not merely its `Result` shape (the unit-mod test one layer down
|
||||
/// covers the `Err`-returning builders only). Data-free: the guard fires
|
||||
/// directly after binding resolution, before any archive access, so no
|
||||
/// gating is needed.
|
||||
#[test]
|
||||
fn run_synthetic_refuses_a_multi_column_blueprint_before_data_access() {
|
||||
let dir = temp_cwd("run_synthetic_multicolumn_refusal");
|
||||
let fixture = dir.join("hl_range.json");
|
||||
std::fs::write(&fixture, HL_RANGE_CLOSED_BLUEPRINT).expect("write fixture");
|
||||
let out = Command::new(BIN).args(["run", fixture.to_str().unwrap()]).output().expect("spawn aura run");
|
||||
assert_eq!(out.status.code(), Some(1), "status: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert_eq!(
|
||||
stderr.trim_end(),
|
||||
"aura: strategy \"hl_range\" consumes columns beyond close (high, low) — synthetic \
|
||||
data generates a close series only; run with --real <SYMBOL>"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property (#231 task 4 quality follow-up): the sibling exit-process arm at
|
||||
/// `blueprint_walkforward_family` — `aura walkforward` over a multi-column
|
||||
/// blueprint with no `--real` — refuses identically. Data-free (the guard
|
||||
/// fires directly after binding resolution, before any window/archive
|
||||
/// access); run inside a temp cwd purely by the project's verb-test
|
||||
/// convention (no store write is expected on this refusal path either).
|
||||
#[test]
|
||||
fn walkforward_synthetic_refuses_a_multi_column_blueprint_before_data_access() {
|
||||
let dir = temp_cwd("walkforward_synthetic_multicolumn_refusal");
|
||||
let fixture = dir.join("hl_signal.json");
|
||||
std::fs::write(&fixture, HL_SIGNAL_OPEN_BLUEPRINT).expect("write fixture");
|
||||
let out = Command::new(BIN)
|
||||
.args([
|
||||
"walkforward", fixture.to_str().unwrap(),
|
||||
"--axis", "hl_signal.fast.length=2,4",
|
||||
"--axis", "hl_signal.slow.length=8",
|
||||
])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura walkforward");
|
||||
assert_eq!(out.status.code(), Some(1), "status: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert_eq!(
|
||||
stderr.trim_end(),
|
||||
"aura: strategy \"hl_signal\" consumes columns beyond close (high, low) — synthetic \
|
||||
data generates a close series only; run with --real <SYMBOL>"
|
||||
);
|
||||
assert!(!dir.join("runs").exists(), "a refused synthetic family must not start a run store");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property (#231 task 4 quality follow-up): the positive counterpart of the
|
||||
/// two refusals above — a multi-column blueprint over `--real GER40` opens
|
||||
/// BOTH the high and low columns end to end (`open_real_source` ->
|
||||
/// `resolve_run_data` -> `run_signal_r`) and produces a real `RunReport`,
|
||||
/// never the single-close weld the pre-task-4 code hardcoded. Gated on the
|
||||
/// local GER40 archive (skip-on-no-data convention).
|
||||
#[test]
|
||||
fn run_real_multi_column_blueprint_opens_high_and_low_columns() {
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let dir = temp_cwd("run_real_multicolumn_open");
|
||||
let fixture = dir.join("hl_range.json");
|
||||
std::fs::write(&fixture, HL_RANGE_CLOSED_BLUEPRINT).expect("write fixture");
|
||||
let out = Command::new(BIN)
|
||||
.args([
|
||||
"run", fixture.to_str().unwrap(),
|
||||
"--real", "GER40",
|
||||
"--from", GER40_SEPT2024_FROM_MS,
|
||||
"--to", GER40_SEPT2024_TO_MS,
|
||||
])
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
assert_eq!(
|
||||
out.status.code(), Some(0),
|
||||
"a high/low blueprint over real GER40 data must run cleanly, got status={:?} stderr={}",
|
||||
out.status, String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(
|
||||
stdout.trim_start().starts_with("{\"manifest\":"),
|
||||
"exit 0 must carry a JSON report, got: {stdout}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property (#231 task 4, "every real-data path" — `run --real` above covers
|
||||
/// only `open_real_source`/`run_signal_r`; `sweep --real` walks a DISTINCT
|
||||
/// real-data path, `blueprint_sweep_family` -> `blueprint_sweep_over` ->
|
||||
/// `DataSource::windowed_sources`, which independently needed `binding.
|
||||
/// columns()` threaded through in task 4): a multi-column (high/low) OPEN
|
||||
/// blueprint swept over `--real GER40` opens both columns per member and
|
||||
/// produces one member line per grid point, not a source-count-mismatch panic
|
||||
/// or a fallback to a single close source. Gated on the local GER40 archive.
|
||||
#[test]
|
||||
fn sweep_real_multi_column_blueprint_opens_high_and_low_columns() {
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let (dir, _g) = fresh_project();
|
||||
let fixture_dir = temp_cwd("sweep_real_multicolumn_open");
|
||||
let fixture = fixture_dir.join("hl_signal.json");
|
||||
std::fs::write(&fixture, HL_SIGNAL_OPEN_BLUEPRINT).expect("write fixture");
|
||||
let out = std::process::Command::new(BIN)
|
||||
.args([
|
||||
"sweep", fixture.to_str().unwrap(),
|
||||
"--real", "GER40",
|
||||
"--from", GER40_SEPT2024_FROM_MS,
|
||||
"--to", GER40_SEPT2024_TO_MS,
|
||||
"--axis", "hl_signal.fast.length=2,4",
|
||||
"--axis", "hl_signal.slow.length=8",
|
||||
])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
assert_eq!(lines.len(), 2, "one member line per grid point: {stdout}");
|
||||
for line in &lines {
|
||||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||||
assert!(
|
||||
v["report"]["manifest"].is_object(),
|
||||
"each member carries a real report, not a source-mismatch fault: {line}"
|
||||
);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&fixture_dir);
|
||||
}
|
||||
|
||||
/// Property (#210 c0110 fieldtest, "the wrapped-name refusal mangles the axis
|
||||
/// the user typed"): the dissolved real-data blueprint sweep types `--axis`
|
||||
/// against the WRAPPED probe namespace (`sma_signal.fast.length`, what
|
||||
|
||||
Reference in New Issue
Block a user