feat(0081): cost-model graph cycle 1 — ConstantCost node + net-R seam
Lands the first cycle of the "Cost-model graph (in R)" milestone: cost is
now a composable in-graph node, not a post-run scalar (C10).
- ConstantCost (aura-std): a flat round-trip cost per trade, emitted in R
as a 3-field record {cost_in_r, cum_cost_in_r, open_cost_in_r} isomorphic
to PositionManagement's R-triple. R-pure: cost_in_r = cost_per_trade /
|entry-stop|; notional cancels, no equity held.
- summarize_r folds the cost stream into net_expectancy_r, replacing its
scalar round_trip_cost (one home for cost, no double-count). Positional
co-temporal join; an empty stream is the gross-R baseline.
- net_r_equity tap (stage1_r_graph): LinComb(4) over [cum_realized_r,
unrealized_r, -cum_cost_in_r, -open_cost_in_r] -> Recorder, a sibling of
r_equity; --cost-per-trade on the run path wires the node + tap + fold.
- A no-cost run is byte-identical: no net_r_equity tap, net == gross, the
C18 golden unchanged.
Tests: exact-subsumption (the node-stream net == mean(r_i - cost_i) over
ALL trades incl. the window-end open one), the in-graph net_r_equity tap +
net < gross E2E, the no-cost golden held. Full suite green; clippy clean.
Implement-time decisions ratified (recorded on #148): the cost stream is
recorded 3-wide (the node's full output; summarize_r reads col 0/2, col 1
feeds the in-graph net curve) — resolving a 2-wide/3-wide inconsistency in
the plan's Step 3; Trade drops its `latched` field (cost now arrives in R),
so r_col ENTRY_PRICE/STOP_PRICE become #[allow(dead_code)] layout contract.
Scoped to the run path (non-reduce); sweep/walkforward/mc pass None — cost
on the reduce-mode sweep path and OOS-pooling cost are a deferred follow-up.
Known pre-existing concern (not this cycle): aura-std lib.rs module doc
still names "realistic broker profiles", retired by the C10 rework.
refs #148
This commit is contained in:
+102
-40
@@ -29,8 +29,9 @@ use aura_registry::{
|
||||
FamilyMember, Generalization, NameKind, PlateauMode, Registry, RunTraces, TraceStore, WriteKind,
|
||||
};
|
||||
use aura_std::{
|
||||
Add, Bias, Delay, Ema, GatedRecorder, Gt, Latch, LinComb, LongOnly, Mul, Recorder, RollingMax,
|
||||
RollingMin, SeriesReducer, SimBroker, Sma, Sqrt, Sub, PM_FIELD_NAMES, PM_RECORD_KINDS,
|
||||
Add, Bias, ConstantCost, Delay, Ema, GatedRecorder, Gt, Latch, LinComb, LongOnly, Mul, Recorder,
|
||||
RollingMax, RollingMin, SeriesReducer, SimBroker, Sma, Sqrt, Sub, PM_FIELD_NAMES,
|
||||
PM_RECORD_KINDS,
|
||||
};
|
||||
use std::sync::mpsc::{self, Receiver};
|
||||
use std::sync::LazyLock;
|
||||
@@ -1199,7 +1200,7 @@ fn stage1_r_sweep_family(trace: Option<&str>, data: &DataSource, grid: &Stage1RG
|
||||
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 bp = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, true, None);
|
||||
let space = bp.param_space();
|
||||
// resolve the open stop slots' exact path-qualified names from the live param-space
|
||||
// (the composite prefix is never hand-synced — match by the stable suffix).
|
||||
@@ -1233,7 +1234,7 @@ fn stage1_r_sweep_family(trace: Option<&str>, data: &DataSource, grid: &Stage1RG
|
||||
let (tx_r, rx_r) = mpsc::channel();
|
||||
let (tx_req, rx_req) = mpsc::channel();
|
||||
let reduce = trace.is_none();
|
||||
let mut h = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, reduce)
|
||||
let mut h = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, reduce, None)
|
||||
.bootstrap_with_cells(point)
|
||||
.expect("stage1-r grid points are kind-checked against param_space");
|
||||
h.run(data.run_sources());
|
||||
@@ -1263,7 +1264,7 @@ fn stage1_r_sweep_family(trace: Option<&str>, data: &DataSource, grid: &Stage1RG
|
||||
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));
|
||||
m.r = Some(summarize_r(&r_rows, &[]));
|
||||
m
|
||||
} else {
|
||||
// trace path (--trace set): raw recorders, persist, summarize — today's code.
|
||||
@@ -1272,10 +1273,10 @@ fn stage1_r_sweep_family(trace: Option<&str>, data: &DataSource, grid: &Stage1RG
|
||||
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
|
||||
let req_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_req.try_iter().collect();
|
||||
if let Some(name) = trace {
|
||||
persist_traces_r(&format!("{name}/{key}"), &manifest, &eq_rows, &ex_rows, &req_rows);
|
||||
persist_traces_r(&format!("{name}/{key}"), &manifest, &eq_rows, &ex_rows, &req_rows, &[]);
|
||||
}
|
||||
let mut m = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
|
||||
m.r = Some(summarize_r(&r_rows, 0.0));
|
||||
m.r = Some(summarize_r(&r_rows, &[]));
|
||||
m
|
||||
};
|
||||
RunReport { manifest, metrics }
|
||||
@@ -1291,7 +1292,7 @@ fn stage1_r_space() -> Vec<ParamSpec> {
|
||||
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()
|
||||
stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, true, None).param_space()
|
||||
}
|
||||
|
||||
/// Windowed reduce-mode stage1-r sweep over `[from, to]` — the in-sample leg of the
|
||||
@@ -1305,7 +1306,7 @@ fn stage1_r_sweep_over(from: Timestamp, to: Timestamp, data: &DataSource, grid:
|
||||
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 bp = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, true, None);
|
||||
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");
|
||||
@@ -1321,7 +1322,7 @@ fn stage1_r_sweep_over(from: Timestamp, to: Timestamp, data: &DataSource, grid:
|
||||
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)
|
||||
let mut h = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, true, None)
|
||||
.bootstrap_with_cells(point)
|
||||
.expect("stage1-r grid points are kind-checked against param_space");
|
||||
let sources = data.windowed_sources(from, to);
|
||||
@@ -1337,7 +1338,7 @@ fn stage1_r_sweep_over(from: Timestamp, to: Timestamp, data: &DataSource, grid:
|
||||
.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));
|
||||
m.r = Some(summarize_r(&r_rows, &[]));
|
||||
RunReport { manifest, metrics: m }
|
||||
})
|
||||
.map(|(fam, lat)| (fam, Some(lat)))
|
||||
@@ -1356,7 +1357,7 @@ fn run_oos_r(
|
||||
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 bp = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None, true, false, None);
|
||||
let space = bp.param_space();
|
||||
let mut h = bp.bootstrap_with_cells(params)
|
||||
.expect("chosen params pre-validated by the in-sample GridSpace::new");
|
||||
@@ -1373,12 +1374,12 @@ fn run_oos_r(
|
||||
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);
|
||||
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));
|
||||
metrics.r = Some(summarize_r(&r_rows, &[]));
|
||||
(equity, RunReport { manifest, metrics })
|
||||
}
|
||||
|
||||
@@ -1434,7 +1435,7 @@ fn stage1_breakout_sweep_family(trace: Option<&str>, data: &DataSource, grid: &S
|
||||
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));
|
||||
m.r = Some(summarize_r(&r_rows, &[]));
|
||||
m
|
||||
} else {
|
||||
let eq_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_eq.try_iter().collect();
|
||||
@@ -1442,10 +1443,10 @@ fn stage1_breakout_sweep_family(trace: Option<&str>, data: &DataSource, grid: &S
|
||||
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
|
||||
let req_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_req.try_iter().collect();
|
||||
if let Some(name) = trace {
|
||||
persist_traces_r(&format!("{name}/{key}"), &manifest, &eq_rows, &ex_rows, &req_rows);
|
||||
persist_traces_r(&format!("{name}/{key}"), &manifest, &eq_rows, &ex_rows, &req_rows, &[]);
|
||||
}
|
||||
let mut m = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
|
||||
m.r = Some(summarize_r(&r_rows, 0.0));
|
||||
m.r = Some(summarize_r(&r_rows, &[]));
|
||||
m
|
||||
};
|
||||
points.push(SweepPoint { params: vec![], report: RunReport { manifest, metrics } });
|
||||
@@ -1506,7 +1507,7 @@ fn stage1_meanrev_sweep_family(trace: Option<&str>, data: &DataSource, grid: &St
|
||||
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));
|
||||
m.r = Some(summarize_r(&r_rows, &[]));
|
||||
m
|
||||
} else {
|
||||
let eq_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_eq.try_iter().collect();
|
||||
@@ -1514,10 +1515,10 @@ fn stage1_meanrev_sweep_family(trace: Option<&str>, data: &DataSource, grid: &St
|
||||
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
|
||||
let req_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_req.try_iter().collect();
|
||||
if let Some(name) = trace {
|
||||
persist_traces_r(&format!("{name}/{key}"), &manifest, &eq_rows, &ex_rows, &req_rows);
|
||||
persist_traces_r(&format!("{name}/{key}"), &manifest, &eq_rows, &ex_rows, &req_rows, &[]);
|
||||
}
|
||||
let mut m = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
|
||||
m.r = Some(summarize_r(&r_rows, 0.0));
|
||||
m.r = Some(summarize_r(&r_rows, &[]));
|
||||
m
|
||||
};
|
||||
points.push(SweepPoint { params: vec![], report: RunReport { manifest, metrics } });
|
||||
@@ -2620,6 +2621,7 @@ fn stage1_r_graph(
|
||||
slow_len: Option<i64>,
|
||||
stop_open: bool,
|
||||
reduce: bool,
|
||||
cost: Option<(f64, mpsc::Sender<(Timestamp, Vec<Scalar>)>, mpsc::Sender<(Timestamp, Vec<Scalar>)>)>,
|
||||
) -> Composite {
|
||||
let mut g = GraphBuilder::new("stage1_r");
|
||||
// SMA-cross signal → Bias (the same signal the pip sample uses).
|
||||
@@ -2693,6 +2695,41 @@ fn stage1_r_graph(
|
||||
g.connect(exec.output("cum_realized_r"), r_equity.input("term[0]"));
|
||||
g.connect(exec.output("unrealized_r"), r_equity.input("term[1]"));
|
||||
g.connect(r_equity.output("value"), req.input("col[0]"));
|
||||
if let Some((cost_per_trade, tx_net, tx_cost)) = cost {
|
||||
let cost_node = g.add(
|
||||
ConstantCost::builder().bind("cost_per_trade", Scalar::f64(cost_per_trade)),
|
||||
);
|
||||
g.connect(exec.output("closed_this_cycle"), cost_node.input("closed"));
|
||||
g.connect(exec.output("open"), cost_node.input("open"));
|
||||
g.connect(exec.output("entry_price"), cost_node.input("entry_price"));
|
||||
g.connect(exec.output("stop_price"), cost_node.input("stop_price"));
|
||||
// net_r_equity = cum_realized_r + unrealized_r - cum_cost_in_r - open_cost_in_r
|
||||
let net_eq = g.add(
|
||||
LinComb::builder(4)
|
||||
.bind("weights[0]", Scalar::f64(1.0))
|
||||
.bind("weights[1]", Scalar::f64(1.0))
|
||||
.bind("weights[2]", Scalar::f64(-1.0))
|
||||
.bind("weights[3]", Scalar::f64(-1.0)),
|
||||
);
|
||||
g.connect(exec.output("cum_realized_r"), net_eq.input("term[0]"));
|
||||
g.connect(exec.output("unrealized_r"), net_eq.input("term[1]"));
|
||||
g.connect(cost_node.output("cum_cost_in_r"), net_eq.input("term[2]"));
|
||||
g.connect(cost_node.output("open_cost_in_r"), net_eq.input("term[3]"));
|
||||
let net_rec = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_net));
|
||||
g.connect(net_eq.output("value"), net_rec.input("col[0]"));
|
||||
// The cost stream `summarize_r` folds: the `ConstantCost` node's full
|
||||
// [cost_in_r, cum_cost_in_r, open_cost_in_r] schema. `summarize_r`'s `cost_col`
|
||||
// reads col 0 (per-close) and col 2 (window-end open) — the lockstep contract
|
||||
// with aura-std's ConstantCost output — so the stream is recorded 3-wide.
|
||||
let cost_rec = g.add(Recorder::builder(
|
||||
vec![ScalarKind::F64, ScalarKind::F64, ScalarKind::F64],
|
||||
Firing::Any,
|
||||
tx_cost,
|
||||
));
|
||||
g.connect(cost_node.output("cost_in_r"), cost_rec.input("col[0]"));
|
||||
g.connect(cost_node.output("cum_cost_in_r"), cost_rec.input("col[1]"));
|
||||
g.connect(cost_node.output("open_cost_in_r"), cost_rec.input("col[2]"));
|
||||
}
|
||||
}
|
||||
g.build().expect("stage1_r wiring resolves")
|
||||
}
|
||||
@@ -2912,9 +2949,11 @@ fn stage1_meanrev_graph(
|
||||
/// `aura run --harness stage1-r [--real <SYM> [--from][--to]] [--trace <n>]`: build the
|
||||
/// dual-tap stage1-r harness, run it on synthetic or real M1 data, fold the pip taps via
|
||||
/// `summarize` and the dense R-record via `summarize_r`, and attach the R block as
|
||||
/// `RunMetrics.r = Some(..)`. Pure/deterministic (C1). round_trip_cost is 0.0 (Stage-1 is
|
||||
/// frictionless; a --cost knob is a follow-on).
|
||||
fn run_stage1_r(data: RunData, trace: Option<&str>) -> RunReport {
|
||||
/// `RunMetrics.r = Some(..)`. Pure/deterministic (C1). `cost` is the optional flat
|
||||
/// round-trip cost per trade (price units): `Some(c)` wires the `ConstantCost` node and
|
||||
/// the `net_r_equity` tap and folds the cost stream into `net_expectancy_r`; `None` is the
|
||||
/// frictionless gross-R baseline (net == gross, no extra tap, byte-unchanged).
|
||||
fn run_stage1_r(data: RunData, trace: Option<&str>, cost: Option<f64>) -> RunReport {
|
||||
if let Some(n) = trace
|
||||
&& let Err(e) = TraceStore::open("runs").ensure_name_free(n, WriteKind::Run)
|
||||
{
|
||||
@@ -2925,7 +2964,10 @@ fn run_stage1_r(data: RunData, trace: Option<&str>) -> RunReport {
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (tx_r, rx_r) = mpsc::channel();
|
||||
let (tx_req, rx_req) = mpsc::channel();
|
||||
let flat = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, Some(2), Some(4), false, false)
|
||||
let (tx_net, rx_net) = mpsc::channel();
|
||||
let (tx_cost, rx_cost) = mpsc::channel();
|
||||
let cost_bundle = cost.map(|c| (c, tx_net, tx_cost));
|
||||
let flat = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, Some(2), Some(4), false, false, cost_bundle)
|
||||
.compile_with_params(&[])
|
||||
.expect("valid stage1-r blueprint");
|
||||
let mut h = Harness::bootstrap(flat).expect("valid stage1-r harness");
|
||||
@@ -2948,6 +2990,11 @@ fn run_stage1_r(data: RunData, trace: Option<&str>) -> RunReport {
|
||||
let ex_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_ex.try_iter().collect();
|
||||
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
|
||||
let req_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_req.try_iter().collect();
|
||||
// The cost taps drain empty when `cost` is `None` (no cost node wired): an empty
|
||||
// `net_rows` suppresses the `net_r_equity` trace and an empty `cost_rows` folds to
|
||||
// net == gross — the byte-unchanged frictionless baseline.
|
||||
let net_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_net.try_iter().collect();
|
||||
let cost_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_cost.try_iter().collect();
|
||||
|
||||
let mut manifest = sim_optimal_manifest(
|
||||
vec![
|
||||
@@ -2963,28 +3010,35 @@ fn run_stage1_r(data: RunData, trace: Option<&str>) -> RunReport {
|
||||
);
|
||||
manifest.broker = stage1_r_broker_label(pip_size);
|
||||
if let Some(name) = trace {
|
||||
persist_traces_r(name, &manifest, &eq_rows, &ex_rows, &req_rows);
|
||||
persist_traces_r(name, &manifest, &eq_rows, &ex_rows, &req_rows, &net_rows);
|
||||
}
|
||||
let mut metrics = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
|
||||
metrics.r = Some(summarize_r(&r_rows, 0.0));
|
||||
metrics.r = Some(summarize_r(&r_rows, &cost_rows));
|
||||
RunReport { manifest, metrics }
|
||||
}
|
||||
|
||||
/// Persist a stage1-r run's three taps: equity (off the SimBroker), exposure (off the
|
||||
/// Bias), and r_equity = cum_realized_r + unrealized_r (off the RiskExecutor). Separate
|
||||
/// from the two-tap `persist_traces` so the pip handlers stay byte-unchanged on disk.
|
||||
/// Persist a stage1-r run's taps: equity (off the SimBroker), exposure (off the Bias),
|
||||
/// r_equity = cum_realized_r + unrealized_r (off the RiskExecutor), and — only on a cost
|
||||
/// run — net_r_equity (gross r_equity minus the cost-in-R taps). Separate from the two-tap
|
||||
/// `persist_traces` so the pip handlers stay byte-unchanged on disk; the `net_r_equity` tap
|
||||
/// is emitted only when `net_rows` is non-empty, so a no-cost run's on-disk trace set is
|
||||
/// byte-unchanged too.
|
||||
fn persist_traces_r(
|
||||
name: &str,
|
||||
manifest: &RunManifest,
|
||||
eq_rows: &[(Timestamp, Vec<Scalar>)],
|
||||
ex_rows: &[(Timestamp, Vec<Scalar>)],
|
||||
req_rows: &[(Timestamp, Vec<Scalar>)],
|
||||
net_rows: &[(Timestamp, Vec<Scalar>)],
|
||||
) {
|
||||
let taps = vec![
|
||||
let mut taps = vec![
|
||||
ColumnarTrace::from_rows("equity", &[ScalarKind::F64], eq_rows),
|
||||
ColumnarTrace::from_rows("exposure", &[ScalarKind::F64], ex_rows),
|
||||
ColumnarTrace::from_rows("r_equity", &[ScalarKind::F64], req_rows),
|
||||
];
|
||||
if !net_rows.is_empty() {
|
||||
taps.push(ColumnarTrace::from_rows("net_r_equity", &[ScalarKind::F64], net_rows));
|
||||
}
|
||||
if let Err(e) = TraceStore::open("runs").write(name, manifest, &taps) {
|
||||
eprintln!("aura: trace persist failed: {e}");
|
||||
std::process::exit(2);
|
||||
@@ -3000,21 +3054,23 @@ enum HarnessKind {
|
||||
Stage1R,
|
||||
}
|
||||
|
||||
/// The parsed `aura run` invocation: a harness, a data source, and an optional trace name.
|
||||
/// The parsed `aura run` invocation: a harness, a data source, an optional trace name, and
|
||||
/// an optional flat round-trip cost per trade (`--cost-per-trade`, stage1-r only this cycle).
|
||||
struct RunArgs {
|
||||
harness: HarnessKind,
|
||||
data: RunData,
|
||||
trace: Option<String>,
|
||||
cost: Option<f64>,
|
||||
}
|
||||
|
||||
/// Parse the `run` tail: `[--harness <name>] [--real <SYM> [--from <ms>] [--to <ms>]]
|
||||
/// [--trace <name>]` in any order, each flag at most once. `--harness sma` is the default
|
||||
/// (`--harness macd` / `--harness stage1-r` select the others); `--from`/`--to` are legal
|
||||
/// only with `--real`. Pure (no I/O, no exit) so the grammar is unit-testable; `main` does
|
||||
/// the side effects.
|
||||
/// [--trace <name>] [--cost-per-trade <f64>]` in any order, each flag at most once.
|
||||
/// `--harness sma` is the default (`--harness macd` / `--harness stage1-r` select the
|
||||
/// others); `--from`/`--to` are legal only with `--real`. Pure (no I/O, no exit) so the
|
||||
/// grammar is unit-testable; `main` does the side effects.
|
||||
fn parse_run_args(rest: &[&str]) -> Result<RunArgs, String> {
|
||||
let usage = || {
|
||||
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>]"
|
||||
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] [--cost-per-trade <f64>]"
|
||||
.to_string()
|
||||
};
|
||||
let mut harness: Option<HarnessKind> = None;
|
||||
@@ -3022,6 +3078,7 @@ fn parse_run_args(rest: &[&str]) -> Result<RunArgs, String> {
|
||||
let mut from: Option<i64> = None;
|
||||
let mut to: Option<i64> = None;
|
||||
let mut trace: Option<String> = None;
|
||||
let mut cost: Option<f64> = None;
|
||||
let mut tail = rest;
|
||||
while let Some((flag, t)) = tail.split_first() {
|
||||
match *flag {
|
||||
@@ -3058,6 +3115,11 @@ fn parse_run_args(rest: &[&str]) -> Result<RunArgs, String> {
|
||||
trace = Some((*value).to_string());
|
||||
tail = t;
|
||||
}
|
||||
"--cost-per-trade" if cost.is_none() => {
|
||||
let (value, t) = t.split_first().ok_or_else(usage)?;
|
||||
cost = Some(value.parse().map_err(|_| usage())?);
|
||||
tail = t;
|
||||
}
|
||||
_ => return Err(usage()),
|
||||
}
|
||||
}
|
||||
@@ -3070,7 +3132,7 @@ fn parse_run_args(rest: &[&str]) -> Result<RunArgs, String> {
|
||||
Some(s) => RunData::Real { symbol: s, from, to },
|
||||
None => RunData::Synthetic,
|
||||
};
|
||||
Ok(RunArgs { harness, data, trace })
|
||||
Ok(RunArgs { harness, data, trace, cost })
|
||||
}
|
||||
|
||||
/// Route a parsed `run` invocation to its harness handler. The compile-time `match` IS
|
||||
@@ -3080,7 +3142,7 @@ fn run_dispatch(args: RunArgs) -> Result<RunReport, String> {
|
||||
Ok(match (args.harness, args.data) {
|
||||
(HarnessKind::Sma, RunData::Synthetic) => run_sample(trace),
|
||||
(HarnessKind::Macd, RunData::Synthetic) => run_macd(trace),
|
||||
(HarnessKind::Stage1R, data) => run_stage1_r(data, trace),
|
||||
(HarnessKind::Stage1R, data) => run_stage1_r(data, trace, args.cost),
|
||||
(HarnessKind::Sma, RunData::Real { symbol, from, to }) => {
|
||||
run_sample_real(&symbol, from, to, trace)
|
||||
}
|
||||
@@ -3091,7 +3153,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 mc --strategy stage1-r [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <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>] [--select <argmax|plateau:mean|plateau:worst>] | aura generalize [--strategy stage1-r] --real <SYM1,SYM2,...> --fast <n> --slow <n> --stop-length <n> --stop-k <f> [--from <ms>] [--to <ms>] [--metric <r-metric>] [--name <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>] [--cost-per-trade <f64>] | 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 mc --strategy stage1-r [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <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>] [--select <argmax|plateau:mean|plateau:worst>] | aura generalize [--strategy stage1-r] --real <SYM1,SYM2,...> --fast <n> --slow <n> --stop-length <n> --stop-k <f> [--from <ms>] [--to <ms>] [--metric <r-metric>] [--name <n>] | aura runs families | aura runs family <id> [rank <metric>]";
|
||||
|
||||
fn main() {
|
||||
// Restore the default SIGPIPE disposition. Rust's runtime sets SIGPIPE to SIG_IGN
|
||||
@@ -4308,7 +4370,7 @@ mod tests {
|
||||
fn run_stage1_r_synthetic_folds_an_r_block() {
|
||||
// the stage1-r harness scores the SMA-cross signal in R: one shell-callable run
|
||||
// yields a RunReport whose metrics.r is Some with a finite SQN and >= 1 trade.
|
||||
let report = run_stage1_r(RunData::Synthetic, None);
|
||||
let report = run_stage1_r(RunData::Synthetic, None, None);
|
||||
let r = report.metrics.r.as_ref().expect("stage1-r run must populate metrics.r");
|
||||
assert!(r.n_trades >= 1, "expected >= 1 trade, got {}", r.n_trades);
|
||||
assert!(r.sqn.is_finite(), "SQN must be finite, got {}", r.sqn);
|
||||
|
||||
Reference in New Issue
Block a user