feat(0085): per-cycle-held accrual — CarryCost + ChargeMode (approach B)
Cycle 5 of milestone #148 (cost-model graph in R): the per-cycle-held accrual mechanism — C10's "per-cycle-held factors accrue over the hold". A carry/funding cost is charged for every cycle a position is held, so the net_r_equity equity curve bleeds continuously over the hold (approach B, "ja, mach B") rather than stepping at close. Mechanism (logged on #148): ChargeMode { AtClose, PerHeldCycle } is a property of the cost factor, read by the one shared CostRunner (no second runner type). The AtClose arm is the pre-cycle-5 eval tokens VERBATIM (IEEE-754 byte-identity). The PerHeldCycle arm accrues `per` into `acc` each held cycle, dumps the accrued total into `cum` at close (resetting acc), and marks the open position via a growing open_cost_in_r. The bleed lives in open_cost_in_r, which the net_r_equity tap already subtracts — so summarize_r and the CLI net_eq wiring are UNCHANGED, and the cycle-0083/0084 net_expectancy_r goldens (-614.3134020253314 flat, -615.0304388396047 composed) + the C18 no-cost golden stay byte-identical. CarryCost is a ConstantCost twin differing only in charge_mode() -> PerHeldCycle (a labelled stress parameter, the flat base of the accrual family). Wired through the existing cost_graph/CostSum aggregation; a per-trade ConstantCost and a per-held-cycle CarryCost compose in one run (the costs sum per-field — the composed golden is exactly the sum of the two single-cost charges). New run-path --carry-per-cycle flag (sweep/walkforward/mc pass None, as the existing cost flags do). Tests: 2 RED-first unit B-proofs (open_cost grows over the hold, dumps the total at close, acc resets between trades — the discriminator a scalar net_expectancy_r cannot see); the CarryCost twin's 5 unit tests; 2 captured net_expectancy_r goldens (-768.2095026600887 carry, -1383.7939051991182 composed); a net_r_equity-bleeds-over- the-hold integration test (the terminal open hold's r_equity-net_r_equity gap strictly grows); plus negative-rate-refused-exit-2, monotone-in-rate, and zero-rate-exactly-free guards. Verified: cargo test --workspace (0 failures), cargo build --workspace, cargo clippy --workspace --all-targets -- -D warnings clean. summarize_r / aura-analysis untouched. Deferred to later #148 cycles (logged there): notional-based carry, the calendar-aware overnight swap, sweep-path cost. refs #148
This commit is contained in:
@@ -29,7 +29,7 @@ use aura_registry::{
|
||||
FamilyMember, Generalization, NameKind, PlateauMode, Registry, RunTraces, TraceStore, WriteKind,
|
||||
};
|
||||
use aura_std::{
|
||||
Add, Bias, ConstantCost, Delay, Ema, GatedRecorder, Gt, Latch, LinComb, LongOnly, Mul,
|
||||
Add, Bias, CarryCost, ConstantCost, Delay, Ema, GatedRecorder, Gt, Latch, LinComb, LongOnly, Mul,
|
||||
Recorder, RollingMax, RollingMin, SeriesReducer, SimBroker, Sma, Sqrt, Sub, VolSlippageCost,
|
||||
PM_FIELD_NAMES, PM_RECORD_KINDS,
|
||||
};
|
||||
@@ -2576,6 +2576,7 @@ const SLIP_VOL_LENGTH: i64 = 5;
|
||||
struct CostConfig {
|
||||
const_cost: Option<f64>, // --cost-per-trade
|
||||
slip_vol_mult: Option<f64>, // --slip-vol-mult
|
||||
carry_per_cycle: Option<f64>, // --carry-per-cycle
|
||||
}
|
||||
|
||||
/// Which data a `run` drives a harness on: the built-in synthetic stream, or real M1
|
||||
@@ -2741,6 +2742,9 @@ fn stage1_r_graph(
|
||||
vol_slot = Some(cost_nodes.len());
|
||||
cost_nodes.push(VolSlippageCost::builder().bind("slip_vol_mult", Scalar::f64(svm)));
|
||||
}
|
||||
if let Some(cpc) = cfg.carry_per_cycle {
|
||||
cost_nodes.push(CarryCost::builder().bind("carry_per_cycle", Scalar::f64(cpc)));
|
||||
}
|
||||
// A single cost_graph composite fans the shared PM-geometry into the active
|
||||
// cost nodes and sums their per-field charges into the run's cost streams.
|
||||
let cg = g.add(cost_graph(cost_nodes));
|
||||
@@ -3007,6 +3011,7 @@ fn run_stage1_r(
|
||||
trace: Option<&str>,
|
||||
const_cost: Option<f64>,
|
||||
slip_vol_mult: Option<f64>,
|
||||
carry_per_cycle: Option<f64>,
|
||||
) -> RunReport {
|
||||
if let Some(n) = trace
|
||||
&& let Err(e) = TraceStore::open("runs").ensure_name_free(n, WriteKind::Run)
|
||||
@@ -3020,8 +3025,8 @@ fn run_stage1_r(
|
||||
let (tx_req, rx_req) = mpsc::channel();
|
||||
let (tx_net, rx_net) = mpsc::channel();
|
||||
let (tx_cost, rx_cost) = mpsc::channel();
|
||||
let cost_bundle = if const_cost.is_some() || slip_vol_mult.is_some() {
|
||||
Some((CostConfig { const_cost, slip_vol_mult }, tx_net, tx_cost))
|
||||
let cost_bundle = if const_cost.is_some() || slip_vol_mult.is_some() || carry_per_cycle.is_some() {
|
||||
Some((CostConfig { const_cost, slip_vol_mult, carry_per_cycle }, tx_net, tx_cost))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -3120,6 +3125,7 @@ struct RunArgs {
|
||||
trace: Option<String>,
|
||||
cost: Option<f64>,
|
||||
slip_vol_mult: Option<f64>,
|
||||
carry_per_cycle: Option<f64>,
|
||||
}
|
||||
|
||||
/// Parse the `run` tail: `[--harness <name>] [--real <SYM> [--from <ms>] [--to <ms>]]
|
||||
@@ -3129,7 +3135,7 @@ struct RunArgs {
|
||||
/// 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>] [--cost-per-trade <f64>] [--slip-vol-mult <f64>]"
|
||||
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] [--cost-per-trade <f64>] [--slip-vol-mult <f64>] [--carry-per-cycle <f64>]"
|
||||
.to_string()
|
||||
};
|
||||
let mut harness: Option<HarnessKind> = None;
|
||||
@@ -3139,6 +3145,7 @@ fn parse_run_args(rest: &[&str]) -> Result<RunArgs, String> {
|
||||
let mut trace: Option<String> = None;
|
||||
let mut cost: Option<f64> = None;
|
||||
let mut slip_vol_mult: Option<f64> = None;
|
||||
let mut carry_per_cycle: Option<f64> = None;
|
||||
let mut tail = rest;
|
||||
while let Some((flag, t)) = tail.split_first() {
|
||||
match *flag {
|
||||
@@ -3193,6 +3200,15 @@ fn parse_run_args(rest: &[&str]) -> Result<RunArgs, String> {
|
||||
slip_vol_mult = Some(v);
|
||||
tail = t;
|
||||
}
|
||||
"--carry-per-cycle" if carry_per_cycle.is_none() => {
|
||||
let (value, t) = t.split_first().ok_or_else(usage)?;
|
||||
let v: f64 = value.parse().map_err(|_| usage())?;
|
||||
if v < 0.0 {
|
||||
return Err(usage());
|
||||
}
|
||||
carry_per_cycle = Some(v);
|
||||
tail = t;
|
||||
}
|
||||
_ => return Err(usage()),
|
||||
}
|
||||
}
|
||||
@@ -3205,7 +3221,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, cost, slip_vol_mult })
|
||||
Ok(RunArgs { harness, data, trace, cost, slip_vol_mult, carry_per_cycle })
|
||||
}
|
||||
|
||||
/// Route a parsed `run` invocation to its harness handler. The compile-time `match` IS
|
||||
@@ -3215,7 +3231,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, args.cost, args.slip_vol_mult),
|
||||
(HarnessKind::Stage1R, data) => run_stage1_r(data, trace, args.cost, args.slip_vol_mult, args.carry_per_cycle),
|
||||
(HarnessKind::Sma, RunData::Real { symbol, from, to }) => {
|
||||
run_sample_real(&symbol, from, to, trace)
|
||||
}
|
||||
@@ -3226,7 +3242,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>] [--cost-per-trade <f64>] [--slip-vol-mult <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>]";
|
||||
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] [--cost-per-trade <f64>] [--slip-vol-mult <f64>] [--carry-per-cycle <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
|
||||
@@ -4443,7 +4459,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, None, None);
|
||||
let report = run_stage1_r(RunData::Synthetic, None, None, 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