chore(cli,ledger): #217 cycle-close audit — coverage backfill + stop-knob helper
Cycle-close audit for the stop-default cycle (af8564dRED +2102305GREEN). Architect review: what holds — the argv extractors and the campaign member runner default to the same single-sourced regime constants; the multi-value refusal and the C10/C20 regime semantics survive; manifests stamp the resolved stop (C18). Three drift items, all resolved fix-path here: - generalize's default path was shipped untested; added the generalize_args_from_defaults_missing_knobs_to_the_regime unit test mirroring mc's sibling. - The None=>default / Some=>parse+single-value stop block recurred 4x across the walkforward/mc extractors; extracted into the generic stop_knob_or helper (refusal strings byte-identical; generalize's typed unwrap_or extractor deliberately not unified — no parsing there). - Ledger stop-defaulting note now records the two default representations: sweep binds no regime (late-resolved by the member runner), the three verbs bind the default eagerly into the document — same R behaviour, deliberately different content ids. Regression gate green, no baseline moved: full workspace suite 0 failures across all test-result groups; clippy -D warnings clean; cargo doc 0 warnings. No spec/plan working files existed (tdd entry path — the committed executable spec is the contract). refs #217
This commit is contained in:
+49
-40
@@ -2307,6 +2307,29 @@ fn select_is_plateau(select: Option<&str>) -> bool {
|
|||||||
matches!(select.map(parse_select), Some(Ok(Selection::Plateau(_))))
|
matches!(select.map(parse_select), Some(Ok(Selection::Plateau(_))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a single optional stop knob (`--stop-length`/`--stop-k`): an absent flag
|
||||||
|
/// defaults to `default`; a present one parses via [`parse_csv_list`] and refuses
|
||||||
|
/// unless it holds exactly one value (the stop is a risk regime, not a swept axis).
|
||||||
|
/// Single-sourced across `walkforward_args_from`/`mc_args_from` (#217 follow-up) —
|
||||||
|
/// `generalize_args_from`'s typed `Option<i64>`/`Option<f64>` fields need no parse
|
||||||
|
/// step, so it keeps its own plain `unwrap_or`.
|
||||||
|
fn stop_knob_or<T: Copy + std::str::FromStr>(
|
||||||
|
raw: Option<&str>,
|
||||||
|
default: T,
|
||||||
|
regime: &impl Fn() -> String,
|
||||||
|
) -> Result<T, String> {
|
||||||
|
match raw {
|
||||||
|
None => Ok(default),
|
||||||
|
Some(s) => {
|
||||||
|
let v: Vec<T> = parse_csv_list(s).map_err(|()| regime())?;
|
||||||
|
if v.len() != 1 {
|
||||||
|
return Err(regime());
|
||||||
|
}
|
||||||
|
Ok(v[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert `WalkforwardCmd` into the resolved argument shape the walkforward sugar
|
/// Convert `WalkforwardCmd` into the resolved argument shape the walkforward sugar
|
||||||
/// consumes (the dissolved `.json --real` branch). Single instrument, single-value
|
/// consumes (the dissolved `.json --real` branch). Single instrument, single-value
|
||||||
/// stop (Fork A: the stop is a risk regime, not a swept axis); `parse_csv_list` is
|
/// stop (Fork A: the stop is a risk regime, not a swept axis); `parse_csv_list` is
|
||||||
@@ -2328,26 +2351,8 @@ fn walkforward_args_from(
|
|||||||
Some(s) => s.to_string(),
|
Some(s) => s.to_string(),
|
||||||
};
|
};
|
||||||
let regime = || "walkforward: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string();
|
let regime = || "walkforward: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string();
|
||||||
let stop_length = match a.stop_length.as_deref() {
|
let stop_length = stop_knob_or(a.stop_length.as_deref(), R_SMA_STOP_LENGTH, ®ime)?;
|
||||||
None => R_SMA_STOP_LENGTH,
|
let stop_k = stop_knob_or(a.stop_k.as_deref(), R_SMA_STOP_K, ®ime)?;
|
||||||
Some(s) => {
|
|
||||||
let v: Vec<i64> = parse_csv_list(s).map_err(|()| regime())?;
|
|
||||||
if v.len() != 1 {
|
|
||||||
return Err(regime());
|
|
||||||
}
|
|
||||||
v[0]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let stop_k = match a.stop_k.as_deref() {
|
|
||||||
None => R_SMA_STOP_K,
|
|
||||||
Some(s) => {
|
|
||||||
let v: Vec<f64> = parse_csv_list(s).map_err(|()| regime())?;
|
|
||||||
if v.len() != 1 {
|
|
||||||
return Err(regime());
|
|
||||||
}
|
|
||||||
v[0]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let name = match (a.name.as_deref(), a.trace.as_deref()) {
|
let name = match (a.name.as_deref(), a.trace.as_deref()) {
|
||||||
(Some(_), Some(_)) => {
|
(Some(_), Some(_)) => {
|
||||||
return Err("walkforward: --name and --trace are mutually exclusive".to_string());
|
return Err("walkforward: --name and --trace are mutually exclusive".to_string());
|
||||||
@@ -2383,26 +2388,8 @@ fn mc_args_from(
|
|||||||
return Err("mc --real: --name/--trace are not accepted (the R-bootstrap records without a family name)".to_string());
|
return Err("mc --real: --name/--trace are not accepted (the R-bootstrap records without a family name)".to_string());
|
||||||
}
|
}
|
||||||
let regime = || "mc: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string();
|
let regime = || "mc: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string();
|
||||||
let stop_length = match a.stop_length.as_deref() {
|
let stop_length = stop_knob_or(a.stop_length.as_deref(), R_SMA_STOP_LENGTH, ®ime)?;
|
||||||
None => R_SMA_STOP_LENGTH,
|
let stop_k = stop_knob_or(a.stop_k.as_deref(), R_SMA_STOP_K, ®ime)?;
|
||||||
Some(s) => {
|
|
||||||
let v: Vec<i64> = parse_csv_list(s).map_err(|()| regime())?;
|
|
||||||
if v.len() != 1 {
|
|
||||||
return Err(regime());
|
|
||||||
}
|
|
||||||
v[0]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let stop_k = match a.stop_k.as_deref() {
|
|
||||||
None => R_SMA_STOP_K,
|
|
||||||
Some(s) => {
|
|
||||||
let v: Vec<f64> = parse_csv_list(s).map_err(|()| regime())?;
|
|
||||||
if v.len() != 1 {
|
|
||||||
return Err(regime());
|
|
||||||
}
|
|
||||||
v[0]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let block_len = a.block_len.map(|v| v as u32).unwrap_or(1);
|
let block_len = a.block_len.map(|v| v as u32).unwrap_or(1);
|
||||||
let resamples = a.resamples.map(|v| v as u32).unwrap_or(1000);
|
let resamples = a.resamples.map(|v| v as u32).unwrap_or(1000);
|
||||||
let seed = a.seed.unwrap_or(1);
|
let seed = a.seed.unwrap_or(1);
|
||||||
@@ -4216,4 +4203,26 @@ mod tests {
|
|||||||
let err = mc_args_from(&a).unwrap_err();
|
let err = mc_args_from(&a).unwrap_err();
|
||||||
assert!(err.contains("single risk regime"), "stop-regime refusal: {err}");
|
assert!(err.contains("single risk regime"), "stop-regime refusal: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Property (#217): a missing `--stop-length`/`--stop-k` no longer refuses —
|
||||||
|
/// each independently defaults to the single-sourced [`R_SMA_STOP_LENGTH`]/
|
||||||
|
/// [`R_SMA_STOP_K`] regime (length 3, k 2.0).
|
||||||
|
#[test]
|
||||||
|
fn generalize_args_from_defaults_missing_knobs_to_the_regime() {
|
||||||
|
let a = GeneralizeCmd {
|
||||||
|
blueprint: Some("candidate.json".to_string()),
|
||||||
|
real: Some("GER40,USDJPY".to_string()),
|
||||||
|
axis: vec!["k=1".to_string()],
|
||||||
|
stop_length: None,
|
||||||
|
stop_k: None,
|
||||||
|
from: None,
|
||||||
|
to: None,
|
||||||
|
metric: None,
|
||||||
|
name: None,
|
||||||
|
};
|
||||||
|
let (_, _, stop_length, stop_k, ..) =
|
||||||
|
generalize_args_from(&a).expect("stop-less generalize resolves");
|
||||||
|
assert_eq!(stop_length, R_SMA_STOP_LENGTH, "omitted --stop-length defaults to the regime");
|
||||||
|
assert_eq!(stop_k, R_SMA_STOP_K, "omitted --stop-k defaults to the regime");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -960,7 +960,15 @@ methodology (finding the best regime = the same walk-forward / worst-case-R
|
|||||||
validation, run once per regime, and picking the most robust — a comparison, not
|
validation, run once per regime, and picking the most robust — a comparison, not
|
||||||
a search). Each member manifest stamps its resolved stop (default included),
|
a search). Each member manifest stamps its resolved stop (default included),
|
||||||
closing the C18 gap; absent/empty `risk` = one implicit default regime,
|
closing the C18 gap; absent/empty `risk` = one implicit default regime,
|
||||||
absent-serializing for content-id parity. Deferred (documented): regime-aware
|
absent-serializing for content-id parity. Two default *representations*
|
||||||
|
coexist by design (#217): a dissolved sweep binds no regime at all
|
||||||
|
(`risk: []`), late-resolved per member by `stop_rule_for_regime` at run
|
||||||
|
time, while walkforward/mc/generalize — whose `--stop-length`/`--stop-k`
|
||||||
|
became optional — bind the default regime *eagerly* into the campaign
|
||||||
|
document (`risk: [Vol{length:3,k:2.0}]`). Same R behaviour either way, but
|
||||||
|
deliberately different document content ids: a stop-less verb invocation's
|
||||||
|
document equals its explicit `--stop-length 3 --stop-k 2.0` spelling, not
|
||||||
|
a stop-less sweep's document. Deferred (documented): regime-aware
|
||||||
**trace** persistence — the trace re-run and cell-key dir naming still assume the
|
**trace** persistence — the trace re-run and cell-key dir naming still assume the
|
||||||
default stop, since `CellRealization` carries no regime (`#212`); the core
|
default stop, since `CellRealization` carries no regime (`#212`); the core
|
||||||
run/stamp/generalize path is unaffected.
|
run/stamp/generalize path is unaffected.
|
||||||
|
|||||||
Reference in New Issue
Block a user