feat(research): std::walk_forward vocabulary corrected to machinery-true fields (0107 task 1)
folds/in_sample_bars/out_of_sample_bars mapped to nothing WindowRoller::new accepts; the block now carries in_sample_ms/ out_of_sample_ms/step_ms/mode (WfMode: rolling|anchored) — the roller's three lengths in the campaign window's own epoch-ms unit plus both shipped RollModes (#198 decision 2). New intrinsic fault ZeroWalkForwardLength (path-addressed) refuses zero lengths at the doc tier, earlier than the roller's NonPositiveLength. Golden canonical pin, PROCESS_FIXTURE, introspection tables, and the aura-cli prose/fixture twins moved together; fieldtest corpus untouched (historical record — stored walk_forward docs get new content ids by design). Gates: aura-research 14/14, aura-cli green, workspace build clean. refs #198
This commit is contained in:
+156
-16
@@ -56,9 +56,10 @@ pub enum StageBlock {
|
||||
Gate { all: Vec<Predicate> },
|
||||
#[serde(rename = "std::walk_forward")]
|
||||
WalkForward {
|
||||
folds: u32,
|
||||
in_sample_bars: u64,
|
||||
out_of_sample_bars: u64,
|
||||
in_sample_ms: u64,
|
||||
out_of_sample_ms: u64,
|
||||
step_ms: u64,
|
||||
mode: WfMode,
|
||||
metric: String,
|
||||
select: SelectRule,
|
||||
},
|
||||
@@ -83,6 +84,17 @@ pub enum SelectRule {
|
||||
PlateauWorst,
|
||||
}
|
||||
|
||||
/// Walk-forward roll mode — wire form "rolling" | "anchored" (the two shipped
|
||||
/// engine `RollMode`s: fixed-length rolling in-sample vs anchored-origin
|
||||
/// growing in-sample).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum WfMode {
|
||||
#[serde(rename = "rolling")]
|
||||
Rolling,
|
||||
#[serde(rename = "anchored")]
|
||||
Anchored,
|
||||
}
|
||||
|
||||
/// One typed gate predicate: `<metric> <cmp> <value>`.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -116,6 +128,8 @@ pub enum Cmp {
|
||||
pub enum SlotKind {
|
||||
MetricName,
|
||||
SelectRule,
|
||||
/// Walk-forward roll mode (`WfMode` wire strings).
|
||||
WfMode,
|
||||
Bool,
|
||||
U32,
|
||||
U64,
|
||||
@@ -164,9 +178,10 @@ pub const PROCESS_BLOCKS: &[BlockSchema] = &[
|
||||
id: "std::walk_forward",
|
||||
doc: "rolling in-sample optimize + out-of-sample test",
|
||||
slots: &[
|
||||
SlotInfo { name: "folds", kind: SlotKind::U32, required: true },
|
||||
SlotInfo { name: "in_sample_bars", kind: SlotKind::U64, required: true },
|
||||
SlotInfo { name: "out_of_sample_bars", kind: SlotKind::U64, required: true },
|
||||
SlotInfo { name: "in_sample_ms", kind: SlotKind::U64, required: true },
|
||||
SlotInfo { name: "out_of_sample_ms", kind: SlotKind::U64, required: true },
|
||||
SlotInfo { name: "step_ms", kind: SlotKind::U64, required: true },
|
||||
SlotInfo { name: "mode", kind: SlotKind::WfMode, required: true },
|
||||
SlotInfo { name: "metric", kind: SlotKind::MetricName, required: true },
|
||||
SlotInfo { name: "select", kind: SlotKind::SelectRule, required: true },
|
||||
],
|
||||
@@ -248,6 +263,18 @@ fn select_from(
|
||||
.map_err(|_| format!("block {block}: unknown select rule \"{s}\""))
|
||||
}
|
||||
|
||||
fn mode_from(
|
||||
m: &serde_json::Map<String, serde_json::Value>,
|
||||
block: &str,
|
||||
) -> Result<WfMode, String> {
|
||||
let s = require_str(m, "mode", block)?;
|
||||
// WfMode's own derived Deserialize (which carries the #[serde(rename)]
|
||||
// wire strings) is the type oracle, mirroring select_from: no second
|
||||
// hardcoded copy of the two roll-mode wire strings.
|
||||
serde_json::from_value(serde_json::Value::String(s.clone()))
|
||||
.map_err(|_| format!("block {block}: unknown mode \"{s}\""))
|
||||
}
|
||||
|
||||
fn stage_from_value(v: &serde_json::Value) -> Result<StageBlock, String> {
|
||||
let m = v
|
||||
.as_object()
|
||||
@@ -280,9 +307,10 @@ fn stage_from_value(v: &serde_json::Value) -> Result<StageBlock, String> {
|
||||
Ok(StageBlock::Gate { all })
|
||||
}
|
||||
"std::walk_forward" => Ok(StageBlock::WalkForward {
|
||||
folds: require_u32(m, "folds", &id)?,
|
||||
in_sample_bars: require_u64(m, "in_sample_bars", &id)?,
|
||||
out_of_sample_bars: require_u64(m, "out_of_sample_bars", &id)?,
|
||||
in_sample_ms: require_u64(m, "in_sample_ms", &id)?,
|
||||
out_of_sample_ms: require_u64(m, "out_of_sample_ms", &id)?,
|
||||
step_ms: require_u64(m, "step_ms", &id)?,
|
||||
mode: mode_from(m, &id)?,
|
||||
metric: require_str(m, "metric", &id)?,
|
||||
select: select_from(m, &id)?,
|
||||
}),
|
||||
@@ -564,6 +592,7 @@ pub enum DocFault {
|
||||
EmptyConjunction { stage: usize },
|
||||
GateFirst,
|
||||
GateAfterTerminalStage { stage: usize },
|
||||
ZeroWalkForwardLength { stage: usize, field: &'static str },
|
||||
// campaign side
|
||||
EmptyInstruments,
|
||||
NoWindow,
|
||||
@@ -589,13 +618,27 @@ pub fn validate_process(doc: &ProcessDoc) -> Vec<DocFault> {
|
||||
let mut terminal_seen = false;
|
||||
for (i, stage) in doc.pipeline.iter().enumerate() {
|
||||
match stage {
|
||||
StageBlock::Sweep { metric, .. }
|
||||
| StageBlock::Generalize { metric }
|
||||
| StageBlock::WalkForward { metric, .. } => {
|
||||
StageBlock::Sweep { metric, .. } | StageBlock::Generalize { metric } => {
|
||||
if !is_known_metric(metric) {
|
||||
faults.push(DocFault::UnknownMetric { stage: i, metric: metric.clone() });
|
||||
}
|
||||
}
|
||||
StageBlock::WalkForward { in_sample_ms, out_of_sample_ms, step_ms, metric, .. } => {
|
||||
if !is_known_metric(metric) {
|
||||
faults.push(DocFault::UnknownMetric { stage: i, metric: metric.clone() });
|
||||
}
|
||||
// WindowRoller::new refuses NonPositiveLength at run time; the
|
||||
// doc tier refuses the same zeroes earlier, path-addressed.
|
||||
for (field, value) in [
|
||||
("in_sample_ms", *in_sample_ms),
|
||||
("out_of_sample_ms", *out_of_sample_ms),
|
||||
("step_ms", *step_ms),
|
||||
] {
|
||||
if value == 0 {
|
||||
faults.push(DocFault::ZeroWalkForwardLength { stage: i, field });
|
||||
}
|
||||
}
|
||||
}
|
||||
StageBlock::Gate { all } => {
|
||||
if i == 0 {
|
||||
faults.push(DocFault::GateFirst);
|
||||
@@ -704,6 +747,7 @@ pub fn slot_kind_label(kind: SlotKind) -> &'static str {
|
||||
match kind {
|
||||
SlotKind::MetricName => "metric name (see metric_vocabulary)",
|
||||
SlotKind::SelectRule => "select rule: argmax | plateau:mean | plateau:worst",
|
||||
SlotKind::WfMode => "one of: rolling | anchored",
|
||||
SlotKind::Bool => "bool",
|
||||
SlotKind::U32 => "non-negative integer (u32)",
|
||||
SlotKind::U64 => "non-negative integer (u64)",
|
||||
@@ -884,8 +928,8 @@ mod tests {
|
||||
{ "block": "std::gate", "all": [
|
||||
{ "metric": "net_expectancy_r", "cmp": "gt", "value": 0.0 },
|
||||
{ "metric": "overfit_probability", "cmp": "lt", "value": 0.1 } ] },
|
||||
{ "block": "std::walk_forward", "folds": 4, "in_sample_bars": 4000,
|
||||
"out_of_sample_bars": 1000, "metric": "net_expectancy_r", "select": "argmax" }
|
||||
{ "block": "std::walk_forward", "in_sample_ms": 4000, "out_of_sample_ms": 1000,
|
||||
"step_ms": 1000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" }
|
||||
]
|
||||
}"#;
|
||||
|
||||
@@ -911,6 +955,24 @@ mod tests {
|
||||
}
|
||||
other => panic!("stage 1 is not a gate: {other:?}"),
|
||||
}
|
||||
match &doc.pipeline[2] {
|
||||
StageBlock::WalkForward {
|
||||
in_sample_ms,
|
||||
out_of_sample_ms,
|
||||
step_ms,
|
||||
mode,
|
||||
metric,
|
||||
select,
|
||||
} => {
|
||||
assert_eq!(*in_sample_ms, 4000);
|
||||
assert_eq!(*out_of_sample_ms, 1000);
|
||||
assert_eq!(*step_ms, 1000);
|
||||
assert_eq!(*mode, WfMode::Rolling);
|
||||
assert_eq!(metric, "net_expectancy_r");
|
||||
assert_eq!(*select, SelectRule::Argmax);
|
||||
}
|
||||
other => panic!("stage 2 is not a walk_forward: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1045,8 +1107,8 @@ mod tests {
|
||||
r#""pipeline":[{"block":"std::sweep","metric":"net_expectancy_r","select":"plateau:worst","deflate":true},"#,
|
||||
r#"{"block":"std::gate","all":[{"metric":"net_expectancy_r","cmp":"gt","value":0.0},"#,
|
||||
r#"{"metric":"overfit_probability","cmp":"lt","value":0.1}]},"#,
|
||||
r#"{"block":"std::walk_forward","folds":4,"in_sample_bars":4000,"out_of_sample_bars":1000,"#,
|
||||
r#""metric":"net_expectancy_r","select":"argmax"}]}"#
|
||||
r#"{"block":"std::walk_forward","in_sample_ms":4000,"out_of_sample_ms":1000,"step_ms":1000,"#,
|
||||
r#""mode":"rolling","metric":"net_expectancy_r","select":"argmax"}]}"#
|
||||
);
|
||||
assert_eq!(canonical, golden);
|
||||
assert!(!canonical.ends_with('\n'));
|
||||
@@ -1140,6 +1202,76 @@ mod tests {
|
||||
.contains(&DocFault::GateAfterTerminalStage { stage: 2 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walk_forward_parses_both_modes_and_refuses_bad_slots() {
|
||||
// "anchored" is the other accepted wire mode
|
||||
let anchored = PROCESS_FIXTURE.replacen("\"mode\": \"rolling\"", "\"mode\": \"anchored\"", 1);
|
||||
let doc = parse_process(&anchored).expect("anchored parses");
|
||||
assert!(matches!(
|
||||
&doc.pipeline[2],
|
||||
StageBlock::WalkForward { mode: WfMode::Anchored, .. }
|
||||
));
|
||||
// an unknown mode string refuses through WfMode's serde oracle
|
||||
let bad_mode = PROCESS_FIXTURE.replacen("\"mode\": \"rolling\"", "\"mode\": \"expanding\"", 1);
|
||||
let err = parse_process(&bad_mode).expect_err("unknown mode refused");
|
||||
assert!(matches!(err, DocError::Malformed(msg) if msg.contains("unknown mode \"expanding\"")));
|
||||
// the retired 0106 vocabulary ("folds") is an unknown slot now
|
||||
let folds = PROCESS_FIXTURE.replacen("\"in_sample_ms\": 4000", "\"folds\": 4", 1);
|
||||
let err = parse_process(&folds).expect_err("folds refused");
|
||||
assert!(matches!(err, DocError::Malformed(msg) if msg.contains("unknown slot \"folds\"")));
|
||||
// a missing required length slot is named
|
||||
let missing = PROCESS_FIXTURE.replacen("\"step_ms\": 1000, ", "", 1);
|
||||
let err = parse_process(&missing).expect_err("missing step_ms refused");
|
||||
assert!(
|
||||
matches!(err, DocError::Malformed(msg) if msg.contains("missing required slot \"step_ms\""))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_process_reports_each_zero_walk_forward_length() {
|
||||
let ok = parse_process(PROCESS_FIXTURE).unwrap();
|
||||
let zeroed = ProcessDoc {
|
||||
pipeline: vec![
|
||||
ok.pipeline[0].clone(),
|
||||
StageBlock::WalkForward {
|
||||
in_sample_ms: 0,
|
||||
out_of_sample_ms: 0,
|
||||
step_ms: 0,
|
||||
mode: WfMode::Rolling,
|
||||
metric: "net_expectancy_r".into(),
|
||||
select: SelectRule::Argmax,
|
||||
},
|
||||
],
|
||||
..ok.clone()
|
||||
};
|
||||
assert_eq!(
|
||||
validate_process(&zeroed),
|
||||
vec![
|
||||
DocFault::ZeroWalkForwardLength { stage: 1, field: "in_sample_ms" },
|
||||
DocFault::ZeroWalkForwardLength { stage: 1, field: "out_of_sample_ms" },
|
||||
DocFault::ZeroWalkForwardLength { stage: 1, field: "step_ms" },
|
||||
]
|
||||
);
|
||||
let one = ProcessDoc {
|
||||
pipeline: vec![
|
||||
ok.pipeline[0].clone(),
|
||||
StageBlock::WalkForward {
|
||||
in_sample_ms: 4000,
|
||||
out_of_sample_ms: 1000,
|
||||
step_ms: 0,
|
||||
mode: WfMode::Anchored,
|
||||
metric: "net_expectancy_r".into(),
|
||||
select: SelectRule::Argmax,
|
||||
},
|
||||
],
|
||||
..ok.clone()
|
||||
};
|
||||
assert_eq!(
|
||||
validate_process(&one),
|
||||
vec![DocFault::ZeroWalkForwardLength { stage: 1, field: "step_ms" }]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_campaign_accepts_the_fixture_and_reports_each_fault() {
|
||||
let ok = parse_campaign(CAMPAIGN_FIXTURE).unwrap();
|
||||
@@ -1197,6 +1329,14 @@ mod tests {
|
||||
let sweep = describe_block("std::sweep").expect("sweep describable");
|
||||
assert!(sweep.slots.iter().any(|s| s.name == "metric" && s.required));
|
||||
assert!(sweep.slots.iter().any(|s| s.name == "deflate" && !s.required));
|
||||
let wf = describe_block("std::walk_forward").expect("walk_forward describable");
|
||||
let names: Vec<&str> = wf.slots.iter().map(|s| s.name).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
vec!["in_sample_ms", "out_of_sample_ms", "step_ms", "mode", "metric", "select"]
|
||||
);
|
||||
let mode = wf.slots.iter().find(|s| s.name == "mode").expect("mode slot");
|
||||
assert_eq!(slot_kind_label(mode.kind), "one of: rolling | anchored");
|
||||
assert!(describe_block("std::strategy").is_some());
|
||||
assert!(describe_block("std::nope").is_none());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user