feat(aura-cli): exec --override on the campaign leg + zero-trade note migration
Slice 3 of the #319 retirement. The campaign leg's --override injects a single-value axis over the named bound param into every strategy entry — after intrinsic validation, before the referential gate, so an unknown path speaks validate_campaign_refs' existing did-you-mean prose and a kind mismatch its existing fault; collision with a document-declared axis refuses (an override overrides a bound value, never an axis, exit 2). The stored bytes and the recorded campaign_id stay those of the original document; the audit record of an override is the raw member manifests. The walk-forward zero-trade aura: note: diagnostic now has a campaign- path producer call (present_campaign per-cell), so it survives the quintet's removal at today's strength. refs #319
This commit is contained in:
@@ -492,3 +492,226 @@ fn exec_override_unknown_path_refuses_with_the_override_prose() {
|
||||
"stdout/stderr: {out}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task 4: `--override` on the campaign leg.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A bound param not covered by the document's own axes (`bias.scale`,
|
||||
/// `examples/r_sma.json`'s `Bias` node — `seed_blueprint` only reopens
|
||||
/// `fast.length`/`slow.length`, so `bias.scale` stays bound at 0.5 in the
|
||||
/// stored blueprint): `--override bias.scale=0.75` reaches every realized
|
||||
/// member's manifest raw (recorded in `params`, dropped from `defaults`) —
|
||||
/// mirrors `exec_blueprint_override_reopens_and_records_the_value_in_params`
|
||||
/// on the campaign leg.
|
||||
#[test]
|
||||
fn exec_campaign_override_reaches_the_member_manifests_raw() {
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir.clone()),
|
||||
ScratchPath::File(dir.join("ovreach.process.json")),
|
||||
ScratchPath::File(dir.join("ovreach.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "exec-override-reach-seed");
|
||||
let proc_id = register_process_doc(&dir, "ovreach.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
||||
write_doc(&dir, "ovreach.campaign.json", &doc);
|
||||
|
||||
let (out, code) =
|
||||
run_code_in(&dir, &["exec", "ovreach.campaign.json", "--override", "bias.scale=0.75"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
let members: Vec<serde_json::Value> = out
|
||||
.lines()
|
||||
.filter(|l| l.starts_with(r#"{"family_id":"#))
|
||||
.map(|l| serde_json::from_str(l).expect("member line parses"))
|
||||
.collect();
|
||||
assert!(!members.is_empty(), "at least one member line: {out}");
|
||||
for m in &members {
|
||||
let params = m["report"]["manifest"]["params"].as_array().expect("params array");
|
||||
assert!(
|
||||
params
|
||||
.iter()
|
||||
.any(|e| e[0] == "bias.scale" && e[1] == serde_json::json!({"F64": 0.75})),
|
||||
"the overridden value must land in every member's params: {m}"
|
||||
);
|
||||
let defaults = m["report"]["manifest"]["defaults"].as_array().expect("defaults array");
|
||||
assert!(
|
||||
!defaults.iter().any(|e| e[0] == "bias.scale"),
|
||||
"an overridden bound param drops out of defaults: {m}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An override path colliding with a document-declared axis (`fast.length`,
|
||||
/// declared by `campaign_doc_json_for`) refuses: an override overrides a
|
||||
/// bound value, never an axis.
|
||||
#[test]
|
||||
fn exec_campaign_override_colliding_with_a_document_axis_refuses() {
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir.clone()),
|
||||
ScratchPath::File(dir.join("ovcollide.process.json")),
|
||||
ScratchPath::File(dir.join("ovcollide.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "exec-override-collide-seed");
|
||||
let proc_id = register_process_doc(&dir, "ovcollide.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
||||
write_doc(&dir, "ovcollide.campaign.json", &doc);
|
||||
|
||||
let (out, code) =
|
||||
run_code_in(&dir, &["exec", "ovcollide.campaign.json", "--override", "fast.length=9"]);
|
||||
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
||||
assert!(out.contains("--override"), "stdout/stderr: {out}");
|
||||
assert!(out.contains("declared axis"), "stdout/stderr: {out}");
|
||||
}
|
||||
|
||||
/// An override path naming no param of any strategy (a typo'd raw name)
|
||||
/// falls out of `validate_campaign_refs`'s existing `AxisNotInParamSpace`
|
||||
/// did-you-mean prose — pinned literally in `research_docs.rs` (:797-822);
|
||||
/// here only the family of the message is asserted.
|
||||
#[test]
|
||||
fn exec_campaign_override_unknown_path_speaks_the_did_you_mean_family() {
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir.clone()),
|
||||
ScratchPath::File(dir.join("ovtypo.process.json")),
|
||||
ScratchPath::File(dir.join("ovtypo.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "exec-override-typo-seed");
|
||||
let proc_id = register_process_doc(&dir, "ovtypo.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
||||
write_doc(&dir, "ovtypo.campaign.json", &doc);
|
||||
|
||||
let (out, code) =
|
||||
run_code_in(&dir, &["exec", "ovtypo.campaign.json", "--override", "fast.lenght=9"]);
|
||||
assert_ne!(code, Some(0), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("did you mean") || out.contains("not in the param space"),
|
||||
"stdout/stderr: {out}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task 5: zero-trade note migration to the campaign walk-forward leg.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A `[std::grid, std::walk_forward]` process — the #256 fork B shape a
|
||||
/// hand-authored (non-sugar) op-script uses — copied verbatim from
|
||||
/// `research_docs.rs::GRID_THEN_WF_PROCESS_DOC`.
|
||||
const GRID_THEN_WF_PROCESS_DOC: &str = r#"{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "grid-then-walkforward",
|
||||
"pipeline": [
|
||||
{ "block": "std::grid" },
|
||||
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
|
||||
"step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" }
|
||||
]
|
||||
}"#;
|
||||
|
||||
/// `campaign_doc_json_for`'s shape with the axis VALUES parameterized (Task 5
|
||||
/// needs the degenerate zero-trade point — both SMA lengths pinned equal —
|
||||
/// which the fixed 2/4 and 8/16 defaults don't reach).
|
||||
fn campaign_doc_json_walkforward(
|
||||
instrument: &str,
|
||||
bp_id: &str,
|
||||
proc_id: &str,
|
||||
window: (i64, i64),
|
||||
fast_values: &str,
|
||||
slow_values: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "run-seam-wf",
|
||||
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [{fast_values}] }},
|
||||
"slow.length": {{ "kind": "I64", "values": [{slow_values}] }} }} }} ],
|
||||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||||
"seed": 7,
|
||||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||||
}}"#,
|
||||
from = window.0,
|
||||
to = window.1,
|
||||
)
|
||||
}
|
||||
|
||||
/// E2E (#313, campaign path): a walk-forward whose every window records zero
|
||||
/// trades emits exactly one zero-trade note on `exec`'s campaign leg — the
|
||||
/// same producer `cli_run.rs::aura_walkforward_all_zero_trade_windows_emit_one_note`
|
||||
/// pins on the retiring synthetic-verb path, now reached through a
|
||||
/// hand-authored `[std::grid, std::walk_forward]` campaign document. Equal
|
||||
/// fast/slow lengths make the SMA difference constantly zero, so no window
|
||||
/// trades — the same degenerate point, over real (SYMA) rather than
|
||||
/// synthetic data (the property is data-source-agnostic).
|
||||
#[test]
|
||||
fn exec_campaign_walkforward_all_zero_trade_windows_emit_one_note() {
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir.clone()),
|
||||
ScratchPath::File(dir.join("wfzero.process.json")),
|
||||
ScratchPath::File(dir.join("wfzero.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "exec-wf-zero-seed");
|
||||
let proc_id = register_process_doc(&dir, "wfzero.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||||
let doc = campaign_doc_json_walkforward(
|
||||
"SYMA",
|
||||
&bp_id,
|
||||
&proc_id,
|
||||
(1709251200000, 1719791999999),
|
||||
"8",
|
||||
"8",
|
||||
);
|
||||
write_doc(&dir, "wfzero.campaign.json", &doc);
|
||||
|
||||
let (out, code) = run_code_in(&dir, &["exec", "wfzero.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
assert_eq!(
|
||||
out.matches("recorded zero trades").count(),
|
||||
1,
|
||||
"exactly one zero-trade note: {out}"
|
||||
);
|
||||
assert!(out.contains("aura: note: "), "the note carries the class marker: {out}");
|
||||
}
|
||||
|
||||
/// E2E (#313 negative twin, campaign path): a walk-forward with at least one
|
||||
/// traded window emits no zero-trade note — the fixture is the same document
|
||||
/// shape as the zero-trade test above, but with the axes campaign_doc_json_for
|
||||
/// already uses (fast∈{2,4}, slow∈{8,16} — known to produce trades over the
|
||||
/// synthetic SYMA archive, per `campaign_run_synthetic_e2e_cost_block_nets_the_per_survivor_bootstrap`).
|
||||
#[test]
|
||||
fn exec_campaign_walkforward_with_trades_emits_no_zero_trade_note() {
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir.clone()),
|
||||
ScratchPath::File(dir.join("wftrades.process.json")),
|
||||
ScratchPath::File(dir.join("wftrades.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "exec-wf-trades-seed");
|
||||
let proc_id = register_process_doc(&dir, "wftrades.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||||
let doc = campaign_doc_json_walkforward(
|
||||
"SYMA",
|
||||
&bp_id,
|
||||
&proc_id,
|
||||
(1709251200000, 1719791999999),
|
||||
"2, 4",
|
||||
"8, 16",
|
||||
);
|
||||
write_doc(&dir, "wftrades.campaign.json", &doc);
|
||||
|
||||
let (out, code) = run_code_in(&dir, &["exec", "wftrades.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
assert!(!out.contains("recorded zero trades"), "a traded run emits no zero-trade note: {out}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user