Files
Aura/crates/aura-registry/src/lib.rs
T
Brummel cb32658fd1 feat(0077): prefer a robust parameter plateau over the in-sample peak
Add an opt-in plateau selection objective for walk-forward, recorded on the
same RunManifest.selection carrier #144 introduced. Default argmax is
byte-identical (C23); plateau is reached only via a new --select flag.

What landed:
- FamilySelection / SelectionMode reshape (report.rs): the selection RULE
  (mode) and its ANNOTATION are orthogonal — deflation fields become Option
  (present iff Argmax), plus PlateauMean/PlateauWorst variants and
  neighbourhood_score/n_neighbours (present iff Plateau*). Legacy lines still
  load: the deflation scalars deserialize from bare values via serde default
  (C14/C18). compat.rs embeds FamilySelection by value — reshape flows through
  untouched.
- GridSpace::axis_lens() + SweepBinder::sweep_with_lattice (engine): the grid
  radixes in param_space()/odometer order. sweep() delegates to
  sweep_with_lattice with the lattice dropped, so every existing .sweep()
  caller is byte-unchanged.
- optimize_plateau + PlateauMode + closed_neighbourhood (aura-registry, C9):
  each member scores as the mean/worst of its closed mixed-radix grid
  neighbourhood's metric_value; the winner is the smoothed argmax by the
  metric's direction (earliest-odometer tie, as optimize). Pure, no RNG (C1);
  in-sample only (C2). A higher_is_better helper now sources the per-metric
  direction once, shared by metric_cmp and optimize_plateau.
- CLI --select <argmax|plateau:mean|plateau:worst> (default argmax; unknown
  token exits 2). walkforward_family dispatches via a select_winner helper;
  sweep_over/stage1_r_sweep_over carry the lattice as Option<Vec<usize>>.
  runs-family display gains a plateau(<mode>)=<score> over <n> cells line.

RandomSpace-refuse: walkforward has no --random producer, so the
plateau-without-lattice guard is structural — select_winner returns the
refuse on a None lattice (the caller prints the message and exits 2),
unit-tested at the helper, not via a --random E2E (decided + recorded on #145
comment 1974, with the lattice-seam fork).

Tests: plateau-picks-plateau-not-spike, mean-vs-worst, lower-is-better
direction-flip, closed-neighbourhood golden, single-member degeneracy, C1
determinism; CLI select-parse, select_winner refuse; E2E argmax-byte-identical
(C23), plateau:mean and plateau:worst provenance. Full workspace suite green;
clippy --all-targets -D warnings clean.

closes #145
2026-06-26 16:38:18 +02:00

1099 lines
52 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The run registry (C18): an append-only store of run records — one
//! `(manifest, metrics)` `RunReport` per line in a JSONL file — with a typed
//! read-path (serde) for listing and ranking runs across invocations ("compare
//! experiments over time", which has no home in git or Gitea). Storage is
//! serde_json; display is the caller's concern (the CLI prints via
//! `RunReport::to_json`).
//!
//! Orchestration **families** (sweep / Monte-Carlo / walk-forward, C12/C21) are
//! stored as related records in a sibling family store (`families.jsonl`): each
//! member is a `RunReport` stamped with its `family` name + `run` index (the
//! `family_id` handle is derived from the pair), re-derived as a unit by
//! [`group_families`]. The flat runs store and its API are untouched.
use std::cmp::Ordering;
use std::fmt;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use aura_engine::{
expected_max_of_normals, r_metrics_from_rs, resample_block, FamilySelection, MetricStats,
RunReport, SelectionMode, SplitMix64, SweepFamily, SweepPoint,
};
mod compat;
mod lineage;
pub use lineage::{
group_families, mc_member_reports, sweep_member_reports, walkforward_member_reports, Family,
FamilyKind, FamilyRunRecord,
};
mod trace_store;
pub use trace_store::{FamilyMember, NameKind, RunTraces, TraceStore, TraceStoreError, WriteKind};
/// An append-only run registry over a JSONL file: one serde_json line per
/// `RunReport`.
pub struct Registry {
path: PathBuf,
}
impl Registry {
/// Bind to a JSONL runs path. No I/O — the file is created lazily on first
/// append.
///
/// A `Registry` owns **two** directory-co-located stores: the bound runs file
/// (this `path`) and a fixed-name `families.jsonl` **sibling** in the same
/// directory — written by [`Registry::append_family`] and read by
/// [`Registry::load_family_members`], both via
/// `self.path.with_file_name("families.jsonl")`. Isolation between registries
/// is therefore **per-directory, not per-filename**: two `open` calls with
/// different runs *filenames* in the same directory share one family store. To
/// isolate runs (e.g. per-process or per-test), bind a distinct *directory*,
/// not merely a distinct runs filename.
pub fn open(path: impl AsRef<Path>) -> Registry {
Registry { path: path.as_ref().to_path_buf() }
}
/// Append one record as a single JSON line, creating the file (and its parent
/// directory) if absent.
pub fn append(&self, report: &RunReport) -> Result<(), RegistryError> {
if let Some(parent) = self.path.parent().filter(|p| !p.as_os_str().is_empty()) {
fs::create_dir_all(parent)?;
}
// a RunReport is finite by construction (its f64 fields are finite), so
// serialization is infallible here.
let line = serde_json::to_string(report).expect("a finite RunReport serializes");
let mut file = fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
writeln!(file, "{line}")?;
Ok(())
}
/// Parse every non-empty line back into a typed `RunReport`, in file order. A
/// missing file is an empty registry (`Ok(vec![])`), not an error.
pub fn load(&self) -> Result<Vec<RunReport>, RegistryError> {
let text = match fs::read_to_string(&self.path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(RegistryError::Io(e)),
};
let mut reports = Vec::new();
for (i, raw) in text.lines().enumerate() {
if raw.trim().is_empty() {
continue;
}
// Read via the back-compat mirror: tolerant of both the current
// tagged-`Scalar` param shape (`{"F64":2.0}`) and the legacy pre-0047
// bare-float shape (`2.0`). The forward write path (`append`,
// `RunReport::to_json`) is unchanged — it still emits the tagged form.
let report: RunReport = serde_json::from_str::<compat::RunReportRead>(raw)
.map_err(|source| RegistryError::Parse { line: i + 1, source })?
.into();
reports.push(report);
}
Ok(reports)
}
}
/// Which metric a best-first comparison keys on. Resolves a metric *name* (a
/// string at the API boundary) to a closed kind once, so the per-comparison hot
/// path branches on this rather than re-parsing the name.
#[derive(Clone, Copy)]
enum Metric {
TotalPips,
MaxDrawdown,
BiasSignFlips,
Sqn,
SqnNormalized,
ExpectancyR,
NetExpectancyR,
}
/// Resolve a metric NAME to the closed `Metric` kind (the single name→kind map).
/// An unknown name is a `RegistryError::UnknownMetric`.
fn resolve_metric(name: &str) -> Result<Metric, RegistryError> {
Ok(match name {
"total_pips" => Metric::TotalPips,
"max_drawdown" => Metric::MaxDrawdown,
// accept the new name AND the pre-rename one (CLI back-compat).
"bias_sign_flips" | "exposure_sign_flips" => Metric::BiasSignFlips,
"sqn" => Metric::Sqn,
"sqn_normalized" => Metric::SqnNormalized,
"expectancy_r" => Metric::ExpectancyR,
"net_expectancy_r" => Metric::NetExpectancyR,
other => return Err(RegistryError::UnknownMetric(other.to_string())),
})
}
/// The metric's scalar value for one report. R metrics live in the optional `r`
/// block; a missing block reads `NEG_INFINITY` (the worst rank for the
/// higher-is-better R keys), preserving `metric_cmp`'s documented contract.
fn metric_value(rep: &RunReport, m: Metric) -> f64 {
fn r_get(rep: &RunReport, f: impl Fn(&aura_engine::RMetrics) -> f64) -> f64 {
rep.metrics.r.as_ref().map(f).unwrap_or(f64::NEG_INFINITY)
}
match m {
Metric::TotalPips => rep.metrics.total_pips,
Metric::MaxDrawdown => rep.metrics.max_drawdown,
Metric::BiasSignFlips => rep.metrics.bias_sign_flips as f64,
Metric::Sqn => r_get(rep, |r| r.sqn),
Metric::SqnNormalized => r_get(rep, |r| r.sqn_normalized),
Metric::ExpectancyR => r_get(rep, |r| r.expectancy_r),
Metric::NetExpectancyR => r_get(rep, |r| r.net_expectancy_r),
}
}
/// The per-metric **best-first ordering** of two reports: `Less` means `a` is
/// better than `b`. "Best" is fixed by each metric's meaning: `total_pips` and
/// the four R keys (`sqn`, `sqn_normalized`, `expectancy_r`, `net_expectancy_r`)
/// higher-is-better; `max_drawdown` and `bias_sign_flips` lower-is-better. Every
/// metric compares via `total_cmp` (a total order over `f64`): top-level keys are
/// finite by construction, while the R keys live in an optional `metrics.r` block
/// where a member with `r: None` reads `f64::NEG_INFINITY` (the worst rank for
/// higher-is-better) — `total_cmp` orders that, and any stray `NaN`,
/// deterministically without panicking. An unknown metric name is a
/// `RegistryError::UnknownMetric`.
///
/// The single source of best-first *ordering* — both [`rank_by`] (sort by it) and
/// [`optimize`] (argmax by it) call this. The per-metric direction itself lives in
/// `higher_is_better` (which this and `optimize_plateau` both read); `metric_cmp`
/// turns that direction into a `total_cmp` over the read values. The metric name is
/// resolved once via `resolve_metric`; the returned closure carries the resolved
/// `Metric`, so per-comparison work is just reading each value (`metric_value`) and
/// the key compare.
fn metric_cmp(metric: &str) -> Result<impl Fn(&RunReport, &RunReport) -> Ordering, RegistryError> {
let m = resolve_metric(metric)?;
let hib = higher_is_better(m);
Ok(move |a: &RunReport, b: &RunReport| {
let (va, vb) = (metric_value(a, m), metric_value(b, m));
// higher-is-better ranks the larger value first; lower-is-better the smaller.
if hib { vb.total_cmp(&va) } else { va.total_cmp(&vb) }
})
}
/// Sort `reports` **best-first** by a named metric. "Best" is per-metric, fixed
/// by each metric's meaning (see `metric_cmp`). A stable sort keeps file
/// (insertion) order among ties. An unknown metric name is a
/// `RegistryError::UnknownMetric`.
pub fn rank_by(mut reports: Vec<RunReport>, metric: &str) -> Result<Vec<RunReport>, RegistryError> {
let cmp = metric_cmp(metric)?;
reports.sort_by(|a, b| cmp(a, b));
Ok(reports)
}
/// `rank_by`'s argmax: return the single best `SweepPoint` of a `SweepFamily`
/// under a named metric, by the same fixed per-metric sense of best (see
/// `metric_cmp`). The whole point is carried — its `params` coordinate AND its
/// `RunReport` — because the caller needs the params that won. Ties on the metric
/// resolve to the **earliest** enumeration-order point (the family is already in
/// odometer order; only a strictly-better later point displaces the incumbent).
/// An unknown metric is a `RegistryError::UnknownMetric`. A `SweepFamily` is
/// non-empty by construction (a sweep over a zero-point grid is rejected upstream
/// by `EmptyAxis`), so `reduce` always yields a winner.
pub fn optimize(family: &SweepFamily, metric: &str) -> Result<SweepPoint, RegistryError> {
let cmp = metric_cmp(metric)?;
let winner = family
.points
.iter()
.reduce(|best, p| if cmp(&p.report, &best.report) == Ordering::Less { p } else { best })
.expect("a SweepFamily is non-empty by construction (EmptyAxis is rejected upstream)");
Ok(winner.clone())
}
fn is_r_metric(m: Metric) -> bool {
matches!(m, Metric::Sqn | Metric::SqnNormalized | Metric::ExpectancyR | Metric::NetExpectancyR)
}
/// The metric's optimisation direction: `true` if a larger value is better
/// (`total_pips`, the four R keys), `false` for the lower-is-better keys
/// (`max_drawdown`, `bias_sign_flips`). The single direction source — `metric_cmp`
/// (best-first ordering) and `optimize_plateau` (smoothed argmax + worst-neighbour)
/// both read it, so the per-metric sense lives in exactly one place.
fn higher_is_better(m: Metric) -> bool {
!matches!(m, Metric::MaxDrawdown | Metric::BiasSignFlips)
}
/// The closed grid neighbourhood of flat index `i` over a mixed-radix lattice
/// (`axis_lens`, last axis fastest — the `GridSpace` odometer convention): `{i}`
/// plus each in-range ±1-per-axis cell. Pure index math, deterministic. The order
/// is `i` first then per-axis neighbours; callers use the result only as a set and
/// for its length, so the order is not load-bearing.
fn closed_neighbourhood(i: usize, axis_lens: &[usize]) -> Vec<usize> {
// decompose i to per-axis coords (last axis fastest)
let mut coords = vec![0usize; axis_lens.len()];
let mut rem = i;
for k in (0..axis_lens.len()).rev() {
coords[k] = rem % axis_lens[k];
rem /= axis_lens[k];
}
// recompose a coord vector to its flat index (Horner, last axis fastest)
let recompose = |c: &[usize]| c.iter().zip(axis_lens).fold(0usize, |flat, (&ck, &len)| flat * len + ck);
let mut out = vec![i];
for k in 0..axis_lens.len() {
for delta in [-1isize, 1] {
let ck = coords[k] as isize + delta;
if ck < 0 || ck as usize >= axis_lens[k] { continue; }
let mut nb = coords.clone();
nb[k] = ck as usize;
out.push(recompose(&nb));
}
}
out
}
fn member_trade_rs(rep: &RunReport) -> &[f64] {
rep.metrics.r.as_ref().map(|r| r.trade_rs.as_slice()).unwrap_or(&[])
}
/// Recompute the selected R metric from a trade-R slice (reuses the engine's
/// `r_metrics_from_rs`, the same arithmetic `summarize_r` uses).
fn member_metric_from_rs(rs: &[f64], m: Metric) -> f64 {
let rm = r_metrics_from_rs(rs);
match m {
Metric::Sqn => rm.sqn,
Metric::SqnNormalized => rm.sqn_normalized,
Metric::ExpectancyR => rm.expectancy_r,
Metric::NetExpectancyR => rm.net_expectancy_r,
_ => unreachable!("member_metric_from_rs is R-only"),
}
}
/// The centred best-of-K null-max distribution: each member's `trade_rs` is
/// mean-subtracted (the no-edge null), then for each resample every member is
/// moving-block-resampled (in odometer order, from a per-iteration seed) and the
/// max recomputed metric across members is taken. Deterministic given `seed` (C1).
fn null_best_of_k(family: &SweepFamily, m: Metric, n_resamples: usize, block_len: usize, seed: u64) -> Vec<f64> {
let centred: Vec<Vec<f64>> = family.points.iter().map(|p| {
let rs = member_trade_rs(&p.report);
if rs.is_empty() { return Vec::new(); }
let mean = rs.iter().sum::<f64>() / rs.len() as f64;
rs.iter().map(|x| x - mean).collect()
}).collect();
(0..n_resamples).map(|i| {
let mut rng = SplitMix64::new(seed ^ i as u64);
centred.iter().fold(f64::NEG_INFINITY, |best, rs| {
if rs.is_empty() { return best; }
let bl = block_len.clamp(1, rs.len());
let v = member_metric_from_rs(&resample_block(rs, bl, &mut rng), m);
best.max(v)
})
}).collect()
}
/// Sample standard deviation of the K members' metric values (the `total_pips`
/// dispersion-floor arm). `< 2` members -> `0.0`.
fn member_sd(family: &SweepFamily, m: Metric) -> f64 {
let vals: Vec<f64> = family.points.iter().map(|p| metric_value(&p.report, m)).collect();
let n = vals.len();
if n < 2 { return 0.0; }
let mean = vals.iter().sum::<f64>() / n as f64;
(vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1) as f64).sqrt()
}
/// Plateau-selection aggregate: how a member's grid-neighbourhood metric is
/// reduced to one smoothed score before the argmax. `Mean` averages the closed
/// neighbourhood; `Worst` takes the most-pessimistic neighbour by the metric's
/// direction (biasing toward interior cells, which keep more neighbours).
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PlateauMode {
Mean,
Worst,
}
/// `optimize`'s argmax over the NEIGHBOURHOOD-SMOOTHED surface, plus its plateau
/// provenance. Each member scores as the mean (or worst-case) of its closed grid
/// neighbourhood's `metric_value`; the winner is the best smoothed score by the
/// metric's own direction (earliest-odometer tie, as `optimize`). `axis_lens` are
/// the grid radixes (`param_space()` order, last axis fastest). Pure,
/// deterministic (C1). The returned `SweepPoint` is the winning grid CELL;
/// `raw_winner_metric` is that cell's own metric, `neighbourhood_score` the
/// smoothed value the argmax maximised.
pub fn optimize_plateau(
family: &SweepFamily, axis_lens: &[usize], metric: &str, mode: PlateauMode,
) -> Result<(SweepPoint, FamilySelection), RegistryError> {
let m = resolve_metric(metric)?;
let n = family.points.len();
debug_assert_eq!(
axis_lens.iter().product::<usize>(), n,
"axis_lens product must equal the family size (a valid grid lattice)",
);
let hib = higher_is_better(m);
// smoothed score + closed-neighbourhood size per member, in odometer order
let scored: Vec<(usize, f64, usize)> = (0..n)
.map(|i| {
let nbrs = closed_neighbourhood(i, axis_lens);
let vals: Vec<f64> = nbrs.iter().map(|&j| metric_value(&family.points[j].report, m)).collect();
let score = match mode {
PlateauMode::Mean => vals.iter().sum::<f64>() / vals.len() as f64,
PlateauMode::Worst => {
if hib {
vals.iter().copied().fold(f64::INFINITY, f64::min)
} else {
vals.iter().copied().fold(f64::NEG_INFINITY, f64::max)
}
}
};
(i, score, nbrs.len())
})
.collect();
// argmax the smoothed surface by direction; earliest-odometer tie (only a
// strictly-better later cell displaces the incumbent, as `optimize`).
let &(wi, wscore, wn) = scored
.iter()
.reduce(|best, cur| {
let better = if hib { cur.1 > best.1 } else { cur.1 < best.1 };
if better { cur } else { best }
})
.expect("a SweepFamily is non-empty by construction (EmptyAxis is rejected upstream)");
let winner = family.points[wi].clone();
let raw = metric_value(&winner.report, m);
Ok((
winner,
FamilySelection {
selection_metric: metric.to_string(),
n_trials: n,
raw_winner_metric: raw,
mode: match mode {
PlateauMode::Mean => SelectionMode::PlateauMean,
PlateauMode::Worst => SelectionMode::PlateauWorst,
},
deflated_score: None,
overfit_probability: None,
n_resamples: None,
block_len: None,
seed: None,
neighbourhood_score: Some(wscore),
n_neighbours: Some(wn),
},
))
}
/// `optimize`'s argmax winner PLUS its trials-deflation provenance. The returned
/// `SweepPoint` is byte-identical to `optimize(family, metric)` (additive, C23).
/// R arm: a centred moving-block reality-check (`overfit_probability` =
/// `(count(null ≥ raw) + 1)/(n + 1)`, `deflated_score` = `raw p95(null)`).
/// `total_pips` arm: a closed-form expected-max-of-K dispersion floor, no
/// probability. Deterministic given `seed` (C1).
pub fn optimize_deflated(
family: &SweepFamily, metric: &str, n_resamples: usize, block_len: usize, seed: u64,
) -> Result<(SweepPoint, FamilySelection), RegistryError> {
let winner = optimize(family, metric)?;
let m = resolve_metric(metric)?;
let k = family.points.len();
let raw = metric_value(&winner.report, m);
let (deflated_score, overfit_probability) = if is_r_metric(m) {
let null_max = null_best_of_k(family, m, n_resamples, block_len, seed);
// Keep only the finite best-of-K draws. The null is *not computable* in two
// degenerate cases: zero resamples (`null_max` empty), or no member carries
// `trade_rs` (every member's centred series is empty, so each iteration's
// best-of-K folds to `NEG_INFINITY`). The latter is reachable — `trade_rs`
// is `#[serde(skip)]`, so a family loaded back from the registry has empty
// conduits even when its `r` block (hence `raw`) is finite. Filtering to
// finite values collapses both into one "no usable null" branch, instead of
// letting `p95 = NEG_INFINITY` turn `deflated_score` into `+inf`.
let usable: Vec<f64> = null_max.into_iter().filter(|x| x.is_finite()).collect();
if usable.is_empty() {
// No reality-check is computable: floor to a degenerate-but-defined
// result — no deflation credit (`deflated_score == raw`) and a
// maximally-uncertain overfit probability (1.0), never a nonsensical inf.
(raw, Some(1.0))
} else {
let p95 = MetricStats::from_values(&usable).p95;
let over = (usable.iter().filter(|&&x| x >= raw).count() + 1) as f64
/ (usable.len() + 1) as f64;
(raw - p95, Some(over))
}
} else {
// The dispersion floor SUBTRACTS the expected-max inflation, so it is valid
// only for a higher-is-better metric (a larger value is the better one the
// search inflated). A lower-is-better metric would deflate wrong-signed; the
// two shipped call sites only pass `total_pips`, so this is unreached, but
// `optimize_deflated` is public — guard the assumption rather than leave it
// implicit.
debug_assert!(
!matches!(m, Metric::MaxDrawdown | Metric::BiasSignFlips),
"dispersion-floor arm is for higher-is-better metrics only",
);
(raw - member_sd(family, m) * expected_max_of_normals(k), None)
};
Ok((winner.clone(), FamilySelection {
selection_metric: metric.to_string(), n_trials: k, raw_winner_metric: raw,
mode: SelectionMode::Argmax,
deflated_score: Some(deflated_score), overfit_probability,
n_resamples: Some(n_resamples), block_len: Some(block_len), seed: Some(seed),
neighbourhood_score: None, n_neighbours: None,
}))
}
/// What can go wrong reading or ranking the registry.
#[derive(Debug)]
pub enum RegistryError {
/// An I/O error reading or writing the JSONL file.
Io(std::io::Error),
/// A stored line did not parse as a `RunReport` (1-based line number).
Parse { line: usize, source: serde_json::Error },
/// `rank_by` was given a metric name it does not know.
UnknownMetric(String),
}
impl fmt::Display for RegistryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RegistryError::Io(e) => write!(f, "registry i/o: {e}"),
RegistryError::Parse { line, source } => {
write!(f, "registry parse error at line {line}: {source}")
}
RegistryError::UnknownMetric(m) => write!(
f,
"unknown metric '{m}' (known: total_pips, max_drawdown, bias_sign_flips, sqn, sqn_normalized, expectancy_r, net_expectancy_r)"
),
}
}
}
impl std::error::Error for RegistryError {}
impl From<std::io::Error> for RegistryError {
fn from(e: std::io::Error) -> Self {
RegistryError::Io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use aura_core::{Cell, Scalar, Timestamp};
use aura_engine::RMetrics;
use aura_engine::{RunManifest, RunMetrics, RunReport, SweepFamily, SweepPoint};
fn report_with(total_pips: f64, max_drawdown: f64, flips: u64) -> RunReport {
RunReport {
manifest: RunManifest {
commit: "c".to_string(),
params: vec![("p".to_string(), Scalar::f64(1.0))],
window: (Timestamp(1), Timestamp(2)),
seed: 0,
broker: "b".to_string(),
selection: None,
},
metrics: RunMetrics { total_pips, max_drawdown, bias_sign_flips: flips, r: None },
}
}
/// A family member carrying an R block — mirrors `report_with` but sets
/// `metrics.r = Some(..)`. Only sqn / expectancy_r / net_expectancy_r vary per
/// call (the rank keys); `sqn_normalized` mirrors `sqn`; the rest are fixed.
fn report_with_r(sqn: f64, expectancy_r: f64, net_expectancy_r: f64) -> RunReport {
let mut rep = report_with(0.0, 0.0, 0);
rep.metrics.r = Some(RMetrics {
expectancy_r,
n_trades: 4,
win_rate: 0.5,
avg_win_r: 1.5,
avg_loss_r: -0.5,
profit_factor: 3.0,
max_r_drawdown: 0.5,
n_open_at_end: 0,
sqn,
sqn_normalized: sqn, // mirror sqn: only the rank-key wiring is under test here
net_expectancy_r,
conviction_terciles_r: [0.0, 0.0, 0.0],
trade_rs: Vec::new(),
});
rep
}
#[test]
fn rank_by_sqn_orders_members_descending() {
let reports = vec![
report_with_r(0.5, 0.1, 0.1),
report_with_r(2.0, 0.4, 0.4),
report_with_r(1.0, 0.2, 0.2),
];
let ranked = rank_by(reports, "sqn").expect("rank sqn");
let sqns: Vec<f64> = ranked.iter().map(|r| r.metrics.r.as_ref().unwrap().sqn).collect();
assert_eq!(sqns, vec![2.0, 1.0, 0.5], "sqn must rank highest-first");
}
#[test]
fn rank_by_sqn_normalized_orders_members_descending() {
// report_with_r sets sqn_normalized == sqn, so ranking by the new key
// orders these members highest-first by their sqn value.
let reports = vec![
report_with_r(0.5, 0.1, 0.1),
report_with_r(2.0, 0.4, 0.4),
report_with_r(1.0, 0.2, 0.2),
];
let ranked = rank_by(reports, "sqn_normalized").expect("rank sqn_normalized");
let vals: Vec<f64> = ranked
.iter()
.map(|r| r.metrics.r.as_ref().unwrap().sqn_normalized)
.collect();
assert_eq!(vals, vec![2.0, 1.0, 0.5], "sqn_normalized must rank highest-first");
}
#[test]
fn rank_by_expectancy_and_net_expectancy_r() {
let reports = vec![report_with_r(1.0, 0.1, 0.05), report_with_r(1.0, 0.3, 0.25)];
let by_exp = rank_by(reports.clone(), "expectancy_r").expect("rank expectancy_r");
assert_eq!(by_exp[0].metrics.r.as_ref().unwrap().expectancy_r, 0.3);
let by_net = rank_by(reports, "net_expectancy_r").expect("rank net_expectancy_r");
assert_eq!(by_net[0].metrics.r.as_ref().unwrap().net_expectancy_r, 0.25);
}
#[test]
fn rank_r_metric_sorts_none_member_last() {
// a pip-only member (r: None) ranks below every member with an R block.
let reports = vec![report_with(1.0, 0.5, 1), report_with_r(0.5, 0.1, 0.1)];
let ranked = rank_by(reports, "sqn").expect("rank sqn with a None member");
assert!(ranked[0].metrics.r.is_some(), "the R member ranks first");
assert!(ranked[1].metrics.r.is_none(), "the None member sorts last");
}
#[test]
fn unknown_metric_message_lists_r_metrics() {
let msg = RegistryError::UnknownMetric("nope".to_string()).to_string();
assert!(msg.contains("sqn"), "msg: {msg}");
assert!(msg.contains("sqn_normalized"), "msg: {msg}");
assert!(msg.contains("expectancy_r"), "msg: {msg}");
assert!(msg.contains("net_expectancy_r"), "msg: {msg}");
}
fn temp_path(name: &str) -> PathBuf {
// unique per test + per process; no external tempfile dependency
std::env::temp_dir().join(format!("aura-registry-{}-{}.jsonl", std::process::id(), name))
}
#[test]
fn append_then_load_round_trips_in_order() {
let path = temp_path("roundtrip");
let _ = fs::remove_file(&path);
let reg = Registry::open(&path);
let a = report_with(1.0, 0.5, 1);
let b = report_with(2.0, 0.3, 2);
reg.append(&a).expect("append a");
reg.append(&b).expect("append b");
assert_eq!(reg.load().expect("load"), vec![a, b]);
let _ = fs::remove_file(&path);
}
#[test]
fn load_missing_file_is_empty() {
let path = temp_path("missing");
let _ = fs::remove_file(&path);
let reg = Registry::open(&path);
assert_eq!(reg.load().expect("load missing"), Vec::<RunReport>::new());
}
#[test]
fn corrupt_line_is_a_parse_error_with_line_number() {
let path = temp_path("corrupt");
let _ = fs::remove_file(&path);
let reg = Registry::open(&path);
reg.append(&report_with(1.0, 0.5, 1)).expect("append");
let mut f = fs::OpenOptions::new().append(true).open(&path).expect("open");
writeln!(f, "not json").expect("write corrupt line");
drop(f);
match reg.load() {
Err(RegistryError::Parse { line, .. }) => assert_eq!(line, 2),
other => panic!("expected Parse at line 2, got {other:?}"),
}
let _ = fs::remove_file(&path);
}
/// A `runs.jsonl` line written before the cycle-0047 typed-`Scalar` params
/// migration carries each param value as a **bare JSON number** (`2.0`),
/// where the current format tags it (`{"F64":2.0}`). `load()` is the durable
/// C18 read-path and must stay back-compatible across that wire-shape change:
/// a legacy bare-float line must still load, the bare float read back as the
/// documented `f64` coercion — not erroring at the first param. (The current
/// typed shape is already covered by the round-trip test.)
#[test]
fn load_reads_a_legacy_bare_float_params_line() {
let path = temp_path("legacy_bare_float");
let _ = fs::remove_file(&path);
// An autonomous, minimal legacy line: a single bare-float param is the
// whole trigger (the rest of the RunReport is in the current shape). This
// mirrors the real pre-0047 store, where the first param `2.0` is where
// the typed-Scalar deserializer first chokes.
let legacy = r#"{"manifest":{"commit":"c","params":[["sma_cross.fast",2.0]],"window":[1,2],"seed":0,"broker":"b"},"metrics":{"total_pips":0.0,"max_drawdown":0.0,"exposure_sign_flips":0}}"#;
fs::write(&path, format!("{legacy}\n")).expect("write legacy line");
let reg = Registry::open(&path);
let reports = reg.load().expect("a legacy bare-float params line must still load");
assert_eq!(reports.len(), 1);
// the bare float reads back as the documented f64 coercion
assert_eq!(reports[0].manifest.params, vec![("sma_cross.fast".to_string(), Scalar::f64(2.0))]);
let _ = fs::remove_file(&path);
}
/// Property: the compat read-path carries a manifest's `selection` block
/// through to the canonical `RunReport` — a line written *with* a
/// FamilySelection lifts it intact (not dropped) through `RunReportRead`'s
/// structural mirror and its `From` conversion.
#[test]
fn load_lifts_a_selection_block_through_the_compat_mirror() {
let line = r#"{"manifest":{"commit":"c","params":[],"window":[0,0],"seed":0,"broker":"b","selection":{"selection_metric":"sqn_normalized","n_trials":4,"raw_winner_metric":1.8,"deflated_score":0.2,"overfit_probability":0.06,"mode":"Argmax","n_resamples":1000,"block_len":5,"seed":42}},"metrics":{"total_pips":0.0,"max_drawdown":0.0,"bias_sign_flips":0}}"#;
let rep: RunReport = serde_json::from_str::<crate::compat::RunReportRead>(line).unwrap().into();
let sel = rep.manifest.selection.expect("selection lifted");
assert_eq!(sel.n_trials, 4);
assert_eq!(sel.mode, aura_engine::SelectionMode::Argmax);
}
#[test]
fn rank_by_orders_best_first_per_metric() {
let reports = vec![
report_with(1.0, 0.9, 5),
report_with(3.0, 0.1, 1),
report_with(2.0, 0.5, 3),
];
let by_pips = rank_by(reports.clone(), "total_pips").expect("rank pips");
assert_eq!(by_pips[0].metrics.total_pips, 3.0); // higher-is-better -> desc
let by_dd = rank_by(reports.clone(), "max_drawdown").expect("rank dd");
assert_eq!(by_dd[0].metrics.max_drawdown, 0.1); // lower-is-better -> asc
// the legacy rank-string still resolves (the dual-accept alias).
let by_flips = rank_by(reports.clone(), "exposure_sign_flips").expect("rank flips");
assert_eq!(by_flips[0].metrics.bias_sign_flips, 1); // lower-is-better -> asc
// the new rank-string ranks identically.
let by_flips_new = rank_by(reports.clone(), "bias_sign_flips").expect("rank flips (new name)");
assert_eq!(by_flips_new[0].metrics.bias_sign_flips, 1);
}
#[test]
fn rank_by_unknown_metric_is_an_error() {
let reports = vec![report_with(1.0, 0.5, 1)];
match rank_by(reports, "sharpe") {
Err(RegistryError::UnknownMetric(m)) => assert_eq!(m, "sharpe"),
other => panic!("expected UnknownMetric, got {other:?}"),
}
}
/// `optimize` is `rank_by`'s argmax: over a `SweepFamily` it returns the
/// single winning `SweepPoint` — its `params` coordinate AND its `RunReport`
/// — chosen by the metric's *fixed* sense of best (`total_pips`
/// higher-is-better), and a tie on that metric resolves to the **earliest
/// enumeration-order** point, never a later one. The point carried, not just
/// its metric, is the property: the caller needs the params that won.
#[test]
fn optimize_picks_the_max_metric_point_ties_to_earliest() {
// A hand-built family in odometer order. Two points tie at the maximal
// total_pips (3.0); the EARLIER of the two (index 1, params p=10) must
// win, not the later (index 3, params p=30) — that pins the tie rule.
let point = |p: f64, pips: f64| SweepPoint {
params: vec![Cell::from_f64(p)],
report: report_with(pips, 0.5, 0),
};
let family = SweepFamily {
space: vec![],
points: vec![
point(0.0, 1.0), // 0: also-ran
point(10.0, 3.0), // 1: tied max, earliest -> the winner
point(20.0, 2.0), // 2: also-ran
point(30.0, 3.0), // 3: tied max, but later -> must lose the tie
],
};
let winner = super::optimize(&family, "total_pips").expect("optimize total_pips");
// the maximal-metric point wins...
assert_eq!(winner.report.metrics.total_pips, 3.0);
// ...and ties resolve to the earliest enumeration-order point: its params
// identify point 1, not point 3.
assert_eq!(winner.params, vec![Cell::from_f64(10.0)]);
// the whole winning point is returned, params AND report together.
assert_eq!(winner, family.points[1]);
}
fn temp_family_dir(name: &str) -> PathBuf {
// a unique per-test directory so the families.jsonl sibling never collides
// across tests sharing the temp dir; the registry binds to runs.jsonl inside.
let dir =
std::env::temp_dir().join(format!("aura-registry-fam-{}-{}", std::process::id(), name));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp family dir");
dir.join("runs.jsonl")
}
#[test]
fn lineage_round_trips_one_family() {
let path = temp_family_dir("roundtrip");
let reg = Registry::open(&path);
let reports =
vec![report_with(1.0, 0.5, 1), report_with(2.0, 0.3, 2), report_with(3.0, 0.1, 0)];
let id = reg.append_family("f", FamilyKind::MonteCarlo, &reports).expect("append family");
assert_eq!(id, "f-0");
let families = group_families(reg.load_family_members().expect("load members"));
assert_eq!(families.len(), 1);
let fam = &families[0];
assert_eq!(fam.id, "f-0");
assert_eq!(fam.kind, FamilyKind::MonteCarlo);
// the split (family, run) fields are stamped on each member, and the
// user-facing handle is derived from them (id is "{family}-{run}").
assert_eq!((fam.members[0].family.as_str(), fam.members[0].run), ("f", 0));
assert_eq!(fam.members[0].family_id(), "f-0");
// members re-derived as a unit, ordinal-ordered, equal to the stamped reports
let member_reports: Vec<_> = fam.members.iter().map(|m| m.report.clone()).collect();
assert_eq!(member_reports, reports);
assert_eq!(fam.members.iter().map(|m| m.ordinal).collect::<Vec<_>>(), vec![0, 1, 2]);
}
#[test]
fn per_name_counter_increments_independently() {
let path = temp_family_dir("counter");
let reg = Registry::open(&path);
let reports = vec![report_with(1.0, 0.5, 1)];
let x0 = reg.append_family("x", FamilyKind::Sweep, &reports).expect("x-0");
let x1 = reg.append_family("x", FamilyKind::Sweep, &reports).expect("x-1");
let y0 = reg.append_family("y", FamilyKind::Sweep, &reports).expect("y-0");
assert_eq!((x0.as_str(), x1.as_str(), y0.as_str()), ("x-0", "x-1", "y-0"));
let families = group_families(reg.load_family_members().expect("load"));
assert_eq!(families.len(), 3);
}
#[test]
fn distinct_families_regroup_in_first_seen_order() {
let path = temp_family_dir("regroup");
let reg = Registry::open(&path);
let a = vec![report_with(1.0, 0.5, 1), report_with(2.0, 0.5, 1)];
let b = vec![report_with(3.0, 0.5, 1)];
let ida = reg.append_family("a", FamilyKind::Sweep, &a).expect("a");
let idb = reg.append_family("b", FamilyKind::WalkForward, &b).expect("b");
let families = group_families(reg.load_family_members().expect("load"));
// first-seen file order: a before b
assert_eq!(families.iter().map(|f| f.id.clone()).collect::<Vec<_>>(), vec![ida, idb]);
assert_eq!(families[0].members.len(), 2);
assert_eq!(families[1].kind, FamilyKind::WalkForward);
// members ordinal-sorted within each family
assert_eq!(families[0].members.iter().map(|m| m.ordinal).collect::<Vec<_>>(), vec![0, 1]);
}
#[test]
fn family_store_and_flat_store_are_disjoint() {
let path = temp_family_dir("disjoint");
let reg = Registry::open(&path);
// a family write leaves the flat runs store empty...
reg.append_family("f", FamilyKind::Sweep, &[report_with(1.0, 0.5, 1)]).expect("family");
assert_eq!(reg.load().expect("flat load"), Vec::<RunReport>::new());
// ...and a flat write leaves the family store untouched at one family.
reg.append(&report_with(9.0, 0.5, 1)).expect("flat append");
assert_eq!(reg.load().expect("flat load 2").len(), 1);
assert_eq!(group_families(reg.load_family_members().expect("members")).len(), 1);
}
#[test]
fn rank_a_family_as_a_unit() {
let path = temp_family_dir("rankfam");
let reg = Registry::open(&path);
let reports =
vec![report_with(1.0, 0.5, 1), report_with(3.0, 0.5, 1), report_with(2.0, 0.5, 1)];
reg.append_family("f", FamilyKind::Sweep, &reports).expect("family");
let fam = group_families(reg.load_family_members().expect("members"))
.pop()
.expect("one family");
let member_reports: Vec<RunReport> = fam.members.iter().map(|m| m.report.clone()).collect();
let ranked = rank_by(member_reports, "total_pips").expect("rank");
assert_eq!(ranked[0].metrics.total_pips, 3.0); // best-first within the family
}
fn member(total_pips: f64, trade_rs: Vec<f64>) -> SweepPoint {
// Every RMetrics field is derived from the R slice by `r_metrics_from_rs`
// (the same arithmetic production members carry), then the per-trade
// `trade_rs` conduit is restored: `r_metrics_from_rs` empties it
// (`trade_rs: Vec::new()`), whereas a production member is built by
// `summarize_r`, which RETAINS it — and `optimize_deflated`'s R arm
// resamples exactly that conduit, so a fixture without it cannot reach
// the R-arm path under test.
let mut r = aura_engine::r_metrics_from_rs(&trade_rs);
r.trade_rs = trade_rs;
SweepPoint {
params: vec![],
report: RunReport {
manifest: RunManifest {
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
seed: 0, broker: "b".into(), selection: None,
},
metrics: RunMetrics {
total_pips, max_drawdown: 0.0, bias_sign_flips: 0,
r: Some(r),
},
},
}
}
fn fixture_family_with_r() -> SweepFamily {
SweepFamily { space: vec![], points: vec![
member(10.0, vec![1.0, -0.5, 0.8, -0.2, 0.6]),
member(30.0, vec![2.0, 1.5, -0.3, 1.0, 0.9]), // best by every metric here
member(5.0, vec![-0.5, 0.2, -0.8, 0.1, -0.3]),
]}
}
fn fixture_family_one_strong_edge() -> SweepFamily {
SweepFamily { space: vec![], points: vec![
member(1.0, vec![2.0, 2.5, 1.8, 2.2, 2.1, 1.9]), // strong, consistent +R edge
member(1.0, vec![0.05, -0.1, 0.0, 0.1, -0.05, 0.02]), // ~zero-edge noise
member(1.0, vec![-0.1, 0.1, 0.0, -0.05, 0.05, 0.0]),
]}
}
#[test]
fn optimize_deflated_winner_is_byte_identical_to_optimize() {
let fam = fixture_family_with_r(); // helper: ≥3 members, varied sqn_normalized + trade_rs
for metric in ["total_pips", "sqn_normalized", "expectancy_r"] {
let plain = optimize(&fam, metric).unwrap();
let (defl, _) = optimize_deflated(&fam, metric, 200, 3, 7).unwrap();
assert_eq!(defl, plain, "deflation must not change the winner ({metric})");
}
}
#[test]
fn optimize_deflated_is_reproducible_from_its_seed() {
let fam = fixture_family_with_r();
let a = optimize_deflated(&fam, "sqn_normalized", 500, 4, 42).unwrap().1;
let b = optimize_deflated(&fam, "sqn_normalized", 500, 4, 42).unwrap().1;
assert_eq!(a, b);
}
#[test]
fn optimize_deflated_total_pips_arm_floors_without_a_probability() {
let fam = fixture_family_with_r();
let sel = optimize_deflated(&fam, "total_pips", 100, 3, 1).unwrap().1;
assert!(sel.overfit_probability.is_none());
assert_eq!(sel.selection_metric, "total_pips");
assert!(sel.deflated_score.unwrap() <= sel.raw_winner_metric); // dispersion floor ≤ raw
}
/// Sibling-contract parity with `r_bootstrap`, which defines `n_resamples ==
/// 0` as a valid input that floors to an all-zero result. `optimize_deflated`'s
/// R arm must not panic on an empty null distribution: with zero resamples it
/// returns a degenerate-but-defined provenance — `deflated_score == raw` (the
/// p95 of an empty null is 0), `overfit_probability == 1` (the `(0 + 1)/(0 +
/// 1)` Laplace floor) — never indexing the empty `MetricStats::from_values`.
#[test]
fn optimize_deflated_zero_resamples_floors_like_r_bootstrap() {
let fam = fixture_family_with_r();
let sel = optimize_deflated(&fam, "expectancy_r", 0, 3, 1).unwrap().1;
assert_eq!(sel.n_resamples, Some(0));
assert_eq!(sel.deflated_score.unwrap(), sel.raw_winner_metric, "p95 of an empty null is 0");
assert_eq!(sel.overfit_probability, Some(1.0), "(0+1)/(0+1) Laplace floor");
}
/// Regression: a family whose members carry an `r` block but NO `trade_rs` —
/// the serde-skipped state of a family `load`ed back from the registry — must
/// NOT yield a `+inf` deflated score on the R arm with positive resamples. With
/// no member contributing a centred series, every best-of-K draw is
/// `NEG_INFINITY`; the finite-filter collapses this to the same degenerate floor
/// as zero-resamples (`deflated_score == raw`, `overfit_probability == 1.0`).
#[test]
fn optimize_deflated_no_trade_rs_floors_instead_of_infinity() {
let mut fam = fixture_family_with_r();
for p in &mut fam.points {
if let Some(r) = p.report.metrics.r.as_mut() {
r.trade_rs.clear(); // mimic a serde-loaded member (trade_rs is #[serde(skip)])
}
}
let sel = optimize_deflated(&fam, "expectancy_r", 500, 3, 1).unwrap().1;
assert!(sel.deflated_score.unwrap().is_finite(), "deflated_score must be finite, got {:?}", sel.deflated_score);
assert_eq!(sel.deflated_score.unwrap(), sel.raw_winner_metric);
assert_eq!(sel.overfit_probability, Some(1.0));
}
#[test]
fn optimize_deflated_overfit_probability_is_low_for_a_clear_edge() {
// A family whose winner has a strong real R-edge over near-zero-edge peers:
// the centred best-of-K null rarely reaches the winner's raw metric.
let fam = fixture_family_one_strong_edge();
let sel = optimize_deflated(&fam, "expectancy_r", 1000, 1, 9).unwrap().1;
assert!(sel.overfit_probability.unwrap() < 0.2, "p={:?}", sel.overfit_probability);
assert!(sel.deflated_score.unwrap() > 0.0);
}
/// Every member shares the SAME strong +R edge. An *uncentred* null (each
/// member's own edged trades resampled) would put the best-of-K ≈ the winner ⇒
/// `overfit_probability ≈ 0.5`. The centred null removes each member's mean, so
/// the winner's real edge stands clear of a zero-edge null ⇒ a low probability.
/// This is the discriminator that the mean-subtraction (the statistic's whole
/// point) is load-bearing — removing the centring would flip this assertion.
fn fixture_family_uniform_edge() -> SweepFamily {
SweepFamily { space: vec![], points: vec![
member(1.0, vec![1.8, 2.2, 1.9, 2.1, 2.0, 1.7]),
member(1.0, vec![2.1, 1.9, 2.0, 2.2, 1.8, 2.0]),
member(1.0, vec![1.9, 2.0, 2.1, 1.8, 2.2, 1.9]),
]}
}
#[test]
fn optimize_deflated_centres_the_null_so_a_uniform_edge_is_not_called_overfit() {
let fam = fixture_family_uniform_edge();
let sel = optimize_deflated(&fam, "expectancy_r", 1000, 1, 5).unwrap().1;
assert!(
sel.overfit_probability.unwrap() < 0.2,
"uniform real edge must read as NOT overfit under the centred null; an \
uncentred null would give ≈0.5. p={:?}",
sel.overfit_probability
);
}
/// No member has a real edge — all are small zero-mean noise. The winner is the
/// best-of-K by luck, and the centred best-of-K null reaches its tiny raw edge
/// routinely ⇒ a HIGH overfit probability (the companion to the clear-edge
/// low-p case: the statistic must distinguish luck from edge).
fn fixture_family_all_noise() -> SweepFamily {
SweepFamily { space: vec![], points: vec![
member(1.0, vec![0.3, -0.4, 0.2, -0.1, 0.1, -0.2, 0.15, -0.05]),
member(1.0, vec![-0.2, 0.3, -0.3, 0.1, 0.0, 0.2, -0.1, 0.05]),
member(1.0, vec![0.1, -0.1, 0.25, -0.2, -0.05, 0.15, 0.0, -0.1]),
]}
}
#[test]
fn optimize_deflated_all_noise_family_has_high_overfit_probability() {
let fam = fixture_family_all_noise();
let sel = optimize_deflated(&fam, "expectancy_r", 1000, 1, 5).unwrap().1;
assert!(
sel.overfit_probability.unwrap() > 0.3,
"an all-noise family's lucky winner must read as overfit-prone, p={:?}",
sel.overfit_probability
);
}
/// C2 at the registry boundary: `optimize_deflated` has no access to any
/// out-of-sample report — its provenance is a pure function of the in-sample
/// family the caller passes. `n_trials` is exactly that family's size and
/// `raw_winner_metric` is that family's own argmax value, so the record cannot
/// encode out-of-sample information.
#[test]
fn optimize_deflated_provenance_reflects_only_the_passed_family() {
let fam = fixture_family_with_r();
let (winner, sel) = optimize_deflated(&fam, "sqn_normalized", 200, 3, 1).unwrap();
assert_eq!(sel.n_trials, fam.points.len());
let m = resolve_metric("sqn_normalized").unwrap();
assert_eq!(sel.raw_winner_metric, metric_value(&winner.report, m));
}
/// A 1-D grid (7 cells, odometer order) with two end spikes and a broad middle
/// plateau in `total_pips`. Bare argmax takes an end spike; the plateau-mean
/// argmax takes the interior plateau centre.
fn fixture_grid_spike_vs_plateau() -> SweepFamily {
let pips = [80.0, 5.0, 48.0, 50.0, 49.0, 5.0, 80.0];
SweepFamily { space: vec![], points: pips.iter().map(|&p| member(p, vec![1.0])).collect() }
}
#[test]
fn closed_neighbourhood_2x3_golden() {
// 2x3 lattice, odometer (last axis fastest):
// (0,0)=0 (0,1)=1 (0,2)=2
// (1,0)=3 (1,1)=4 (1,2)=5
let sorted = |i: usize| { let mut v = closed_neighbourhood(i, &[2, 3]); v.sort_unstable(); v };
assert_eq!(sorted(0), vec![0, 1, 3]); // corner (0,0): self + 2
assert_eq!(sorted(4), vec![1, 3, 4, 5]); // interior (1,1): self + 3
assert_eq!(sorted(2), vec![1, 2, 5]); // corner (0,2): self + 2
}
#[test]
fn plateau_picks_the_plateau_not_the_spike() {
let fam = fixture_grid_spike_vs_plateau();
let spike = optimize(&fam, "total_pips").unwrap();
let (centre, sel) = optimize_plateau(&fam, &[7], "total_pips", PlateauMode::Mean).unwrap();
assert_ne!(centre.report.metrics.total_pips, spike.report.metrics.total_pips);
assert_eq!(centre.report.metrics.total_pips, 50.0, "plateau centre is the middle cell");
assert_eq!(sel.mode, SelectionMode::PlateauMean);
assert_eq!(sel.raw_winner_metric, 50.0);
assert!(sel.neighbourhood_score.unwrap() < spike.report.metrics.total_pips,
"smoothed score is below the spike's raw peak");
assert_eq!(sel.n_neighbours, Some(3), "interior cell has self + 2 neighbours");
assert!(sel.deflated_score.is_none() && sel.n_resamples.is_none(),
"deflation fields omitted under plateau");
}
#[test]
fn plateau_worst_is_more_conservative_than_mean() {
let fam = fixture_grid_spike_vs_plateau();
let (_, mean_sel) = optimize_plateau(&fam, &[7], "total_pips", PlateauMode::Mean).unwrap();
let (_, worst_sel) = optimize_plateau(&fam, &[7], "total_pips", PlateauMode::Worst).unwrap();
// Worst scores each cell by its weakest neighbour, never above the mean.
assert!(worst_sel.neighbourhood_score.unwrap() <= mean_sel.neighbourhood_score.unwrap());
assert_eq!(worst_sel.mode, SelectionMode::PlateauWorst);
assert!(worst_sel.n_neighbours.unwrap() >= 2, "winner is an interior cell");
}
#[test]
fn plateau_single_member_equals_optimize() {
let fam = SweepFamily { space: vec![], points: vec![member(42.0, vec![1.0])] };
let plain = optimize(&fam, "total_pips").unwrap();
let (winner, sel) = optimize_plateau(&fam, &[1], "total_pips", PlateauMode::Mean).unwrap();
assert_eq!(winner.report.metrics.total_pips, plain.report.metrics.total_pips);
assert_eq!(sel.n_neighbours, Some(1), "a 1-cell grid's closed neighbourhood is itself");
assert_eq!(sel.neighbourhood_score, Some(42.0));
assert_eq!(sel.raw_winner_metric, 42.0);
}
#[test]
fn optimize_plateau_is_deterministic() {
let fam = fixture_grid_spike_vs_plateau();
let a = optimize_plateau(&fam, &[7], "total_pips", PlateauMode::Worst).unwrap().1;
let b = optimize_plateau(&fam, &[7], "total_pips", PlateauMode::Worst).unwrap().1;
assert_eq!(a, b);
}
/// A 1-D grid (7 cells, odometer order) in `max_drawdown` — the canonical
/// lower-is-better key. Two end *dips* (drawdown 1.0, the lure for a bare
/// lower-is-better argmax) flank a broad middle plateau of moderately-low
/// drawdown (≈30), with worse shoulders. A member carries only `max_drawdown`;
/// `total_pips`/`r` are inert here.
fn fixture_grid_dip_vs_plateau_lower_is_better() -> SweepFamily {
let dds = [1.0, 60.0, 30.0, 30.0, 31.0, 60.0, 1.0];
let point = |dd: f64| SweepPoint {
params: vec![],
report: report_with(0.0, dd, 0),
};
SweepFamily { space: vec![], points: dds.iter().map(|&dd| point(dd)).collect() }
}
/// Direction-flip coverage: on a lower-is-better metric (`max_drawdown`),
/// plateau selection exercises the `else` (smaller-is-better) argmax branch AND
/// the `Worst` arm's `NEG_INFINITY`/`max` fold. Bare argmax takes an end dip
/// (the global minimum drawdown); the plateau-mean argmax takes the interior
/// plateau centre, whose neighbourhood mean is the lowest. This is the
/// lower-is-better companion the higher-is-better plateau tests do not reach.
#[test]
fn plateau_lower_is_better_picks_the_plateau_not_the_dip() {
let fam = fixture_grid_dip_vs_plateau_lower_is_better();
let dip = optimize(&fam, "max_drawdown").unwrap();
assert_eq!(dip.report.metrics.max_drawdown, 1.0, "bare argmax takes the end dip");
let (centre, mean_sel) =
optimize_plateau(&fam, &[7], "max_drawdown", PlateauMode::Mean).unwrap();
// the plateau centre is an interior cell, not the end dip
assert_ne!(centre.report.metrics.max_drawdown, 1.0);
assert_eq!(mean_sel.mode, SelectionMode::PlateauMean);
assert_eq!(mean_sel.n_neighbours, Some(3), "interior cell has self + 2 neighbours");
// the winning cell's neighbourhood mean beats the end dip's (whose 60.0
// shoulder drags its mean up), so the smaller-is-better argmax did not pick
// the dip.
assert!(
mean_sel.neighbourhood_score.unwrap() < (1.0 + 60.0) / 2.0,
"plateau mean is below the end dip's neighbourhood mean; score={:?}",
mean_sel.neighbourhood_score,
);
// Worst arm: each cell scores by its highest (worst) drawdown neighbour
// (the NEG_INFINITY/max fold). It is never below the mean for a
// lower-is-better metric, and still avoids the dip whose worst neighbour is
// 60.0.
let (_, worst_sel) =
optimize_plateau(&fam, &[7], "max_drawdown", PlateauMode::Worst).unwrap();
assert_eq!(worst_sel.mode, SelectionMode::PlateauWorst);
assert!(
worst_sel.neighbourhood_score.unwrap() >= mean_sel.neighbourhood_score.unwrap(),
"worst (max drawdown) ≥ mean for a lower-is-better metric",
);
assert!(
worst_sel.neighbourhood_score.unwrap() < 60.0,
"the plateau interior's worst neighbour is below the dip's 60.0 shoulder",
);
}
}