feat(0075): walk-forward strategy-selectable + R-reporting (iter 1)
Make `aura walkforward --strategy stage1-r [--real <SYM>]` roll IS->OOS windows, sweep the stage1-r grid in-sample, pick the winner by an R metric (sqn_normalized), run it out-of-sample, and report per-window + pooled OOS R-metrics. The bare SMA walkforward path is byte-identical (its dispatch arm is verbatim today's body; the pooled `oos_r` block is emitted only when a window carries an `r` block). Engine: - RMetrics gains an in-memory `trade_rs: Vec<f64>` (realised R per closed trade), excluded from serde (`#[serde(skip)]`) and from a hand-written PartialEq, so the C18 wire shape and every existing equality assertion / round-trip stay unchanged. summarize_r now retains the per-trade R vector it used to drop. - New `r_metrics_from_rs(&[f64])` reduces a flat (pooled across-window) R series to RMetrics. Its R-distribution arithmetic is copied verbatim from summarize_r (the byte-pinned floats must not be algebraically refactored); the two copies are guarded in lockstep by a cross-reducer equality test. net_expectancy_r = expectancy_r (exact at the Stage-1 cost=0 invariant); conviction_terciles_r = [0,0,0] (per-trade conviction is not pooled). CLI: - walkforward gains --strategy + the four stage1-r grid flags (reusing parse_csv_list / Stage1RGrid); walkforward_family is strategy-dispatched. - Windowed stage1-r helpers: stage1_r_sweep_over (reduce-mode folded IS sweep, O(trades)/member) and run_oos_r (non-reduce OOS run, for the stitched pip-equity curve), plus stage1_r_space. Frictionless Stage-1 R (costs are Stage-2). Verified: cargo build clean, full `cargo test --workspace` green (SMA walkforward, synthetic mc, stage1_r_single_run_output_golden, and the C18 round-trips all preserved), clippy -D warnings clean. Monte-Carlo R-bootstrap is iter 2. refs #139
This commit is contained in:
+224
-44
@@ -14,14 +14,14 @@
|
||||
mod render;
|
||||
use render::{ChartData, ChartMeta, ChartMode, ReduceKind, Series};
|
||||
|
||||
use aura_core::{zip_params, Cell, Firing, Scalar, ScalarKind, Timestamp};
|
||||
use aura_core::{zip_params, Cell, Firing, ParamSpec, Scalar, ScalarKind, Timestamp};
|
||||
use aura_composites::{risk_executor, risk_executor_vol_open, StopRule};
|
||||
use aura_engine::{
|
||||
f64_field, join_on_ts, monte_carlo, param_stability, summarize, summarize_r, walk_forward,
|
||||
window_of, ColumnarTrace, Composite, Edge, FlatGraph, GraphBuilder, Harness, JoinedRow,
|
||||
McAggregate, McFamily, RollMode, RunManifest, RunMetrics, RunReport, SourceSpec, SweepFamily,
|
||||
SweepPoint, SyntheticSpec, Target, VecSource, WalkForwardResult, WindowBounds, WindowRoller,
|
||||
WindowRun,
|
||||
f64_field, join_on_ts, monte_carlo, param_stability, r_metrics_from_rs, summarize, summarize_r,
|
||||
walk_forward, window_of, ColumnarTrace, Composite, Edge, FlatGraph, GraphBuilder, Harness,
|
||||
JoinedRow, McAggregate, McFamily, RollMode, RunManifest, RunMetrics, RunReport, SourceSpec,
|
||||
SweepFamily, SweepPoint, SyntheticSpec, Target, VecSource, WalkForwardResult, WindowBounds,
|
||||
WindowRoller, WindowRun,
|
||||
};
|
||||
use aura_registry::{
|
||||
group_families, mc_member_reports, optimize, rank_by, sweep_member_reports,
|
||||
@@ -1274,6 +1274,104 @@ fn stage1_r_sweep_family(trace: Option<&str>, data: &DataSource, grid: &Stage1RG
|
||||
.expect("the stage1-r named grid matches the stage1-r param-space")
|
||||
}
|
||||
|
||||
/// The param-space of the OPEN stage1-r blueprint (all four knobs free) — the kinds
|
||||
/// the per-window `WindowRun::chosen_params` are read against (C7). Mirrors the
|
||||
/// throwaway-build param_space resolution inside `stage1_r_sweep_family`.
|
||||
fn stage1_r_space() -> Vec<ParamSpec> {
|
||||
let (tx_eq, _) = mpsc::channel();
|
||||
let (tx_ex, _) = mpsc::channel();
|
||||
let (tx_r, _) = mpsc::channel();
|
||||
let (tx_req, _) = mpsc::channel();
|
||||
stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, true).param_space()
|
||||
}
|
||||
|
||||
/// Windowed reduce-mode stage1-r sweep over `[from, to]` — the in-sample leg of the
|
||||
/// stage1-r walk-forward. Identical grid/axis/fold logic to `stage1_r_sweep_family`,
|
||||
/// but windowed (`windowed_sources`) and always folded (O(trades)/member): each
|
||||
/// member's RunReport carries `metrics.r = Some(summarize_r(..))`, so the family is
|
||||
/// rankable by an R metric.
|
||||
fn stage1_r_sweep_over(from: Timestamp, to: Timestamp, data: &DataSource, grid: &Stage1RGrid) -> SweepFamily {
|
||||
let pip = data.pip_size();
|
||||
let (tx_eq, _) = mpsc::channel();
|
||||
let (tx_ex, _) = mpsc::channel();
|
||||
let (tx_r, _) = mpsc::channel();
|
||||
let (tx_req, _) = mpsc::channel();
|
||||
let bp = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, true);
|
||||
let space = bp.param_space();
|
||||
let stop_length_axis = space.iter().map(|p| p.name.clone())
|
||||
.find(|n| n.ends_with(STOP_LENGTH_SUFFIX)).expect("open stage1-r vol-stop exposes a stop_length axis");
|
||||
let stop_k_axis = space.iter().map(|p| p.name.clone())
|
||||
.find(|n| n.ends_with(STOP_K_SUFFIX)).expect("open stage1-r vol-stop exposes a stop_k axis");
|
||||
bp
|
||||
.axis("fast.length", grid.fast.clone())
|
||||
.axis("slow.length", grid.slow.clone())
|
||||
.axis(&stop_length_axis, grid.stop_length.clone())
|
||||
.axis(&stop_k_axis, grid.stop_k.clone())
|
||||
.sweep(|point| {
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (tx_r, rx_r) = mpsc::channel();
|
||||
let (tx_req, _rx_req) = mpsc::channel();
|
||||
let mut h = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, true)
|
||||
.bootstrap_with_cells(point)
|
||||
.expect("stage1-r grid points are kind-checked against param_space");
|
||||
let sources = data.windowed_sources(from, to);
|
||||
let window = window_of(&sources).expect("non-empty in-sample window");
|
||||
h.run(sources);
|
||||
let mut named: Vec<(String, Scalar)> = zip_params(&space, point).into_iter()
|
||||
.map(|(n, v)| (stage1_r_friendly_name(&n), v)).collect();
|
||||
named.push(("bias_scale".to_string(), Scalar::f64(0.5)));
|
||||
let mut manifest = sim_optimal_manifest(named, window, 0, pip);
|
||||
manifest.broker = stage1_r_broker_label(pip);
|
||||
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
|
||||
let (total_pips, max_drawdown) = rx_eq.try_iter().next()
|
||||
.map(|(_, row)| (row[0].as_f64(), row[1].as_f64())).unwrap_or((0.0, 0.0));
|
||||
let bias_sign_flips = rx_ex.try_iter().next().map(|(_, row)| row[2].as_i64() as u64).unwrap_or(0);
|
||||
let mut m = RunMetrics { total_pips, max_drawdown, bias_sign_flips, r: None };
|
||||
m.r = Some(summarize_r(&r_rows, 0.0));
|
||||
RunReport { manifest, metrics: m }
|
||||
})
|
||||
.expect("the stage1-r named grid matches the stage1-r param-space")
|
||||
}
|
||||
|
||||
/// Run the chosen stage1-r params over an OOS window; return the recorded pip-equity
|
||||
/// segment (for stitching) and the OOS RunReport whose `metrics.r` carries both the
|
||||
/// R metrics and the per-trade `trade_rs`. Non-reduce (raw recorders): one window's
|
||||
/// curve is bounded, and `stitch` needs the full pip-equity series.
|
||||
fn run_oos_r(
|
||||
params: &[Cell], from: Timestamp, to: Timestamp, trace: Option<&str>, data: &DataSource,
|
||||
) -> (Vec<(Timestamp, f64)>, RunReport) {
|
||||
let pip = data.pip_size();
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (tx_r, rx_r) = mpsc::channel();
|
||||
let (tx_req, rx_req) = mpsc::channel();
|
||||
let bp = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, false);
|
||||
let space = bp.param_space();
|
||||
let mut h = bp.bootstrap_with_cells(params)
|
||||
.expect("chosen params pre-validated by the in-sample GridSpace::new");
|
||||
let sources = data.windowed_sources(from, to);
|
||||
let window = window_of(&sources).expect("non-empty out-of-sample window");
|
||||
h.run(sources);
|
||||
let eq_rows = rx_eq.try_iter().collect::<Vec<_>>();
|
||||
let ex_rows = rx_ex.try_iter().collect::<Vec<_>>();
|
||||
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
|
||||
let req_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_req.try_iter().collect();
|
||||
let mut named: Vec<(String, Scalar)> = zip_params(&space, params).into_iter()
|
||||
.map(|(n, v)| (stage1_r_friendly_name(&n), v)).collect();
|
||||
named.push(("bias_scale".to_string(), Scalar::f64(0.5)));
|
||||
let mut manifest = sim_optimal_manifest(named, window, 0, pip);
|
||||
manifest.broker = stage1_r_broker_label(pip);
|
||||
if let Some(name) = trace {
|
||||
persist_traces_r(&format!("{name}/oos{}", from.0), &manifest, &eq_rows, &ex_rows, &req_rows);
|
||||
}
|
||||
let equity = f64_field(&eq_rows, 0);
|
||||
let exposure = f64_field(&ex_rows, 0);
|
||||
let mut metrics = summarize(&equity, &exposure);
|
||||
metrics.r = Some(summarize_r(&r_rows, 0.0));
|
||||
(equity, RunReport { manifest, metrics })
|
||||
}
|
||||
|
||||
/// `aura sweep --strategy stage1-breakout`: sweep the breakout harness over a channel ×
|
||||
/// stop grid. One `channel` length drives BOTH rolling nodes (parameter-ganging, #61),
|
||||
/// so the family iterates the cartesian product MANUALLY with a fully-bound graph per
|
||||
@@ -1450,6 +1548,21 @@ enum Strategy {
|
||||
Stage1MeanRev,
|
||||
}
|
||||
|
||||
impl Strategy {
|
||||
/// The CLI `--strategy` token this variant parses from — the inverse of the
|
||||
/// `parse_*_args` match arms. Used to echo the offending strategy in error
|
||||
/// messages so they name the actual input.
|
||||
fn cli_token(self) -> &'static str {
|
||||
match self {
|
||||
Strategy::SmaCross => "sma",
|
||||
Strategy::Momentum => "momentum",
|
||||
Strategy::Stage1R => "stage1-r",
|
||||
Strategy::Stage1Breakout => "stage1-breakout",
|
||||
Strategy::Stage1MeanRev => "stage1-meanrev",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a comma-separated list of `T` (each item parsed via `FromStr`), rejecting
|
||||
/// any item that fails to parse — the shared validator for the stage1-r grid flags
|
||||
/// (#137). The caller folds the `Err` into the subcommand `usage()` so a malformed
|
||||
@@ -1520,13 +1633,18 @@ fn parse_sweep_args(
|
||||
Ok((strategy, name, persist, real.finish(&usage)?, grid))
|
||||
}
|
||||
|
||||
/// Parse the `walkforward` tail: `[--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n> | --trace <n>]`.
|
||||
/// Defaults: synthetic, name "walkforward", no persist. `--name`/`--trace` are
|
||||
/// Parse the `walkforward` tail:
|
||||
/// `[--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n> | --trace <n>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>]`.
|
||||
/// Defaults: SMA-cross, synthetic, name "walkforward", no persist. `--name`/`--trace`
|
||||
/// mutually exclusive; `--from`/`--to` require `--real`. Pure (no I/O / exit).
|
||||
fn parse_walkforward_args(rest: &[&str]) -> Result<(String, bool, DataChoice), String> {
|
||||
let usage = || "walkforward [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n> | --trace <n>]".to_string();
|
||||
let mut name: Option<(String, bool)> = None; // (name, persist)
|
||||
fn parse_walkforward_args(
|
||||
rest: &[&str],
|
||||
) -> Result<(Strategy, String, bool, DataChoice, Stage1RGrid), String> {
|
||||
let usage = || "walkforward [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n> | --trace <n>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>]".to_string();
|
||||
let mut strategy = Strategy::SmaCross;
|
||||
let mut name: Option<(String, bool)> = None;
|
||||
let mut real = RealWindowGrammar::default();
|
||||
let mut grid = Stage1RGrid::default();
|
||||
let mut tail = rest;
|
||||
while let Some((flag, t)) = tail.split_first() {
|
||||
let (value, t) = t.split_first().ok_or_else(usage)?;
|
||||
@@ -1535,14 +1653,28 @@ fn parse_walkforward_args(rest: &[&str]) -> Result<(String, bool, DataChoice), S
|
||||
continue;
|
||||
}
|
||||
match *flag {
|
||||
"--strategy" => {
|
||||
strategy = match *value {
|
||||
"sma" => Strategy::SmaCross,
|
||||
"momentum" => Strategy::Momentum,
|
||||
"stage1-r" => Strategy::Stage1R,
|
||||
"stage1-breakout" => Strategy::Stage1Breakout,
|
||||
"stage1-meanrev" => Strategy::Stage1MeanRev,
|
||||
_ => return Err(usage()),
|
||||
};
|
||||
}
|
||||
"--name" if name.is_none() => name = Some(((*value).to_string(), false)),
|
||||
"--trace" if name.is_none() => name = Some(((*value).to_string(), true)),
|
||||
"--fast" => grid.fast = parse_csv_list(value).map_err(|()| usage())?,
|
||||
"--slow" => grid.slow = parse_csv_list(value).map_err(|()| usage())?,
|
||||
"--stop-length" => grid.stop_length = parse_csv_list(value).map_err(|()| usage())?,
|
||||
"--stop-k" => grid.stop_k = parse_csv_list(value).map_err(|()| usage())?,
|
||||
_ => return Err(usage()),
|
||||
}
|
||||
tail = t;
|
||||
}
|
||||
let (name, persist) = name.unwrap_or_else(|| ("walkforward".to_string(), false));
|
||||
Ok((name, persist, real.finish(&usage)?))
|
||||
Ok((strategy, name, persist, real.finish(&usage)?, grid))
|
||||
}
|
||||
|
||||
/// Render a family-member stdout line: the assigned `family_id` plus the embedded
|
||||
@@ -1612,7 +1744,7 @@ fn run_sweep(strategy: Strategy, name: &str, persist: bool, data: DataSource, gr
|
||||
/// print each carrying the assigned id, then the stitched summary line. With
|
||||
/// `--trace`, also persist each OOS member's streams under
|
||||
/// `runs/traces/<n>/oos<ns>/` (opt-in). Deterministic (C1).
|
||||
fn run_walkforward(name: &str, persist: bool, data: DataSource) {
|
||||
fn run_walkforward(strategy: Strategy, name: &str, persist: bool, data: DataSource, grid: &Stage1RGrid) {
|
||||
if persist
|
||||
&& let Err(e) = TraceStore::open("runs").ensure_name_free(name, WriteKind::Family)
|
||||
{
|
||||
@@ -1620,7 +1752,7 @@ fn run_walkforward(name: &str, persist: bool, data: DataSource) {
|
||||
std::process::exit(2);
|
||||
}
|
||||
let reg = default_registry();
|
||||
let result = walkforward_family(persist.then_some(name), &data);
|
||||
let result = walkforward_family(strategy, persist.then_some(name), &data, grid);
|
||||
let id =
|
||||
match reg.append_family(name, FamilyKind::WalkForward, &walkforward_member_reports(&result))
|
||||
{
|
||||
@@ -1638,9 +1770,14 @@ fn run_walkforward(name: &str, persist: bool, data: DataSource) {
|
||||
|
||||
/// The built-in rolling walk-forward: 24-bar in-sample, 12-bar out-of-sample,
|
||||
/// stepping 12 (contiguous OOS tiling), over the 60-bar synthetic span -> 3
|
||||
/// windows. Each window sweeps the built-in grid in-sample, optimizes by
|
||||
/// total_pips (axis 2), and runs the chosen params out-of-sample.
|
||||
fn walkforward_family(trace: Option<&str>, data: &DataSource) -> WalkForwardResult {
|
||||
/// windows. Each window sweeps a grid in-sample, optimizes by a metric, and runs
|
||||
/// the chosen params out-of-sample — both strategy-dispatched: the `SmaCross` arm
|
||||
/// sweeps the SMA sample grid and optimizes by `total_pips` (axis 2); the
|
||||
/// `Stage1R` arm sweeps the stage1-r grid and optimizes by `sqn_normalized`.
|
||||
/// Other strategies have no walk-forward form yet (exit 2).
|
||||
fn walkforward_family(
|
||||
strategy: Strategy, trace: Option<&str>, data: &DataSource, grid: &Stage1RGrid,
|
||||
) -> WalkForwardResult {
|
||||
let span = data.wf_full_span();
|
||||
let (is_len, oos_len, step) = data.wf_window_sizes();
|
||||
let roller = match WindowRoller::new(span, is_len, oos_len, step, RollMode::Rolling) {
|
||||
@@ -1650,19 +1787,39 @@ fn walkforward_family(trace: Option<&str>, data: &DataSource) -> WalkForwardResu
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
let space = sample_blueprint_with_sinks(data.pip_size()).0.param_space();
|
||||
walk_forward(roller, space, |w: WindowBounds| {
|
||||
let is_family = sweep_over(w.is.0, w.is.1, data);
|
||||
let best = optimize(&is_family, "total_pips").expect("total_pips is a known metric");
|
||||
let (oos_equity, oos_report) = run_oos(&best.params, w.oos.0, w.oos.1, trace, data);
|
||||
WindowRun {
|
||||
// The tag-free sweep winner is the chosen point; its kinds live on
|
||||
// WalkForwardResult.space (computed once above from the same blueprint).
|
||||
chosen_params: best.params,
|
||||
oos_equity,
|
||||
oos_report,
|
||||
match strategy {
|
||||
Strategy::SmaCross => {
|
||||
let space = sample_blueprint_with_sinks(data.pip_size()).0.param_space();
|
||||
walk_forward(roller, space, |w: WindowBounds| {
|
||||
let is_family = sweep_over(w.is.0, w.is.1, data);
|
||||
let best = optimize(&is_family, "total_pips").expect("total_pips is a known metric");
|
||||
let (oos_equity, oos_report) = run_oos(&best.params, w.oos.0, w.oos.1, trace, data);
|
||||
WindowRun {
|
||||
// The tag-free sweep winner is the chosen point; its kinds live on
|
||||
// WalkForwardResult.space (computed once above from the same blueprint).
|
||||
chosen_params: best.params,
|
||||
oos_equity,
|
||||
oos_report,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
Strategy::Stage1R => {
|
||||
let space = stage1_r_space();
|
||||
walk_forward(roller, space, |w: WindowBounds| {
|
||||
let is_family = stage1_r_sweep_over(w.is.0, w.is.1, data, grid);
|
||||
let best = optimize(&is_family, "sqn_normalized").expect("sqn_normalized is a known metric");
|
||||
let (oos_equity, oos_report) = run_oos_r(&best.params, w.oos.0, w.oos.1, trace, data);
|
||||
WindowRun { chosen_params: best.params, oos_equity, oos_report }
|
||||
})
|
||||
}
|
||||
other => {
|
||||
eprintln!(
|
||||
"aura: walkforward has no form for strategy '{}'",
|
||||
other.cli_token()
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sweep the built-in named grid over an in-sample window, sourcing the in-memory
|
||||
@@ -1733,14 +1890,22 @@ fn run_oos(
|
||||
/// (C14).
|
||||
fn walkforward_summary_json(result: &WalkForwardResult) -> String {
|
||||
let total = result.stitched_oos_equity.last().map(|&(_, v)| v).unwrap_or(0.0);
|
||||
serde_json::json!({
|
||||
"walkforward": {
|
||||
"windows": result.windows.len(),
|
||||
"stitched_total_pips": total,
|
||||
"param_stability": param_stability(result),
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
// pool the per-window OOS per-trade R series in roll order, then reduce.
|
||||
let pooled_rs: Vec<f64> = result.windows.iter()
|
||||
.flat_map(|w| w.run.oos_report.metrics.r.as_ref().map(|r| r.trade_rs.clone()).unwrap_or_default())
|
||||
.collect();
|
||||
let mut obj = serde_json::json!({
|
||||
"windows": result.windows.len(),
|
||||
"stitched_total_pips": total,
|
||||
"param_stability": param_stability(result),
|
||||
});
|
||||
if result.windows.iter().any(|w| w.run.oos_report.metrics.r.is_some()) {
|
||||
// RMetrics serializes its scalar fields (trade_rs is serde-skipped, so the
|
||||
// oos_r block is the clean R-metric summary of the pooled series).
|
||||
obj["oos_r"] = serde_json::to_value(r_metrics_from_rs(&pooled_rs))
|
||||
.expect("RMetrics serializes");
|
||||
}
|
||||
serde_json::json!({ "walkforward": obj }).to_string()
|
||||
}
|
||||
|
||||
/// A longer deterministic stream than `showcase_prices` — enough for several
|
||||
@@ -1772,7 +1937,7 @@ fn walkforward_window_source(from: Timestamp, to: Timestamp) -> VecSource {
|
||||
/// helper (mirrors `sweep_report`).
|
||||
#[cfg(test)]
|
||||
fn walkforward_report() -> String {
|
||||
let result = walkforward_family(None, &DataSource::Synthetic);
|
||||
let result = walkforward_family(Strategy::SmaCross, None, &DataSource::Synthetic, &Stage1RGrid::default());
|
||||
let mut out = String::new();
|
||||
for w in &result.windows {
|
||||
out.push_str(&w.run.oos_report.to_json());
|
||||
@@ -2589,7 +2754,7 @@ fn run_dispatch(args: RunArgs) -> Result<RunReport, String> {
|
||||
}
|
||||
|
||||
const USAGE: &str =
|
||||
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] | aura chart <name> [--tap <t>] [--panels] | aura graph | aura sweep [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] | aura mc [--name <n>|--trace <n>] | aura walkforward [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] | aura runs families | aura runs family <id> [rank <metric>]";
|
||||
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] | aura chart <name> [--tap <t>] [--panels] | aura graph | aura sweep [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] | aura mc [--name <n>|--trace <n>] | aura walkforward [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] | aura runs families | aura runs family <id> [rank <metric>]";
|
||||
|
||||
fn main() {
|
||||
// Collect argv and match the whole vector: every accepted form is exhaustive,
|
||||
@@ -2635,8 +2800,8 @@ fn main() {
|
||||
}
|
||||
},
|
||||
["walkforward", rest @ ..] => match parse_walkforward_args(rest) {
|
||||
Ok((name, persist, choice)) => {
|
||||
run_walkforward(&name, persist, DataSource::from_choice(choice))
|
||||
Ok((strategy, name, persist, choice, grid)) => {
|
||||
run_walkforward(strategy, &name, persist, DataSource::from_choice(choice), &grid)
|
||||
}
|
||||
Err(msg) => {
|
||||
eprintln!("aura: {msg}");
|
||||
@@ -3148,7 +3313,7 @@ mod tests {
|
||||
.append_family(
|
||||
"walkforward",
|
||||
FamilyKind::WalkForward,
|
||||
&walkforward_member_reports(&walkforward_family(None, &DataSource::Synthetic)),
|
||||
&walkforward_member_reports(&walkforward_family(Strategy::SmaCross, None, &DataSource::Synthetic, &Stage1RGrid::default())),
|
||||
)
|
||||
.expect("walkforward family");
|
||||
assert_eq!((sid.as_str(), mid.as_str(), wid.as_str()), ("sweep-0", "mc-0", "walkforward-0"));
|
||||
@@ -3650,15 +3815,30 @@ mod tests {
|
||||
/// and rejects two name flags or a `--real` missing its symbol.
|
||||
#[test]
|
||||
fn parse_walkforward_args_defaults_and_accepts_real() {
|
||||
assert_eq!(parse_walkforward_args(&[]), Ok(("walkforward".to_string(), false, DataChoice::Synthetic)));
|
||||
assert_eq!(
|
||||
parse_walkforward_args(&[]),
|
||||
Ok((Strategy::SmaCross, "walkforward".to_string(), false, DataChoice::Synthetic, Stage1RGrid::default()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_walkforward_args(&["--real", "EURUSD", "--trace", "w"]),
|
||||
Ok(("w".to_string(), true, DataChoice::Real { symbol: "EURUSD".to_string(), from_ms: None, to_ms: None }))
|
||||
Ok((Strategy::SmaCross, "w".to_string(), true,
|
||||
DataChoice::Real { symbol: "EURUSD".to_string(), from_ms: None, to_ms: None },
|
||||
Stage1RGrid::default()))
|
||||
);
|
||||
assert!(parse_walkforward_args(&["--name", "a", "--trace", "b"]).is_err());
|
||||
assert!(parse_walkforward_args(&["--real"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_walkforward_args_accepts_strategy_and_grid_flags() {
|
||||
let r = parse_walkforward_args(&["--strategy", "stage1-r", "--fast", "5,10", "--stop-k", "2.0,3.0"]);
|
||||
let (strategy, _, _, _, grid) = r.expect("valid stage1-r walkforward args");
|
||||
assert_eq!(strategy, Strategy::Stage1R);
|
||||
assert_eq!(grid.fast, vec![5, 10]);
|
||||
assert_eq!(grid.stop_k, vec![2.0, 3.0]);
|
||||
assert!(parse_walkforward_args(&["--strategy", "bogus"]).is_err());
|
||||
}
|
||||
|
||||
/// `parse_walkforward_args` mirrors the shared `RealWindowGrammar` real/window strictness:
|
||||
/// an empty `--real` symbol and a repeated `--real`/`--from`/`--to` are usage
|
||||
/// errors — the same real-grammar rejection its sibling `parse_sweep_args` gives.
|
||||
|
||||
Reference in New Issue
Block a user