From e8b6878d3bf8e5b7e9cee9479bdedeb71c57f77b Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 23:12:36 +0200 Subject: [PATCH] feat(research,cli,docs): state cost and risk knob units in introspect and the glossary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit campaign introspect --unwired now annotates the four silently misreadable knobs with unit/semantics sub-lines: cost_per_trade is a price-unit numerator charged as cost/|entry−stop| in R (not a cost in R); slip_vol_mult multiplies the per-cycle vol estimate; vol.length smooths the estimator and does not set the stop's timescale; vol.k scales the stop distance (the risk-unit lever). Rendered additively via a new OpenSlot.notes field, so the sibling tests pinning the slot hint lines verbatim stay green byte-identically. The glossary's cost-model and risk-regime entries mirror the same semantics inline (two-sentence convention kept), the risk entry pointing at #262 for the timescale-matched variant. closes #265 --- crates/aura-cli/src/research_docs.rs | 3 ++ crates/aura-research/src/lib.rs | 44 ++++++++++++++++++++++++---- docs/glossary.md | 7 +++-- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/crates/aura-cli/src/research_docs.rs b/crates/aura-cli/src/research_docs.rs index ec8bd19..d8e321e 100644 --- a/crates/aura-cli/src/research_docs.rs +++ b/crates/aura-cli/src/research_docs.rs @@ -278,6 +278,9 @@ fn describe_one_block(id: &str) -> Result<(), String> { fn print_open_slots(slots: &[aura_research::OpenSlot]) { for s in slots { println!("open slot: {} ({})", s.path, s.hint); + for note in &s.notes { + println!(" {note}"); + } } if slots.is_empty() { println!("no open slots"); diff --git a/crates/aura-research/src/lib.rs b/crates/aura-research/src/lib.rs index 21f5ad9..9de5278 100644 --- a/crates/aura-research/src/lib.rs +++ b/crates/aura-research/src/lib.rs @@ -978,10 +978,22 @@ pub struct OpenSlot { pub path: String, /// Short human hint, e.g. "required, one of: content_id, identity_id". pub hint: String, + /// Additional unit/semantics notes rendered as indented sub-lines under + /// the slot's main line (#265) — additive, so the `hint` string itself + /// (pinned verbatim by earlier tests) never changes. + pub notes: Vec, } fn open(path: impl Into, hint: impl Into) -> OpenSlot { - OpenSlot { path: path.into(), hint: hint.into() } + OpenSlot { path: path.into(), hint: hint.into(), notes: Vec::new() } +} + +/// Like `open`, but carries additional unit/semantics notes (#265) rendered +/// as indented sub-lines under the slot — the `hint` string stays exactly as +/// `open` would have produced it, so sibling tests pinning `hint` verbatim +/// are unaffected. +fn open_with_notes(path: impl Into, hint: impl Into, notes: Vec) -> OpenSlot { + OpenSlot { path: path.into(), hint: hint.into(), notes } } /// List the open slots of a (possibly partial) process document. Only the @@ -1064,18 +1076,26 @@ pub fn open_slots_campaign(text: &str) -> Result, DocError> { // how it went undiscovered (#216). Absent, empty, or non-array = open. let risk_bound = v.get("risk").and_then(|r| r.as_array()).is_some_and(|a| !a.is_empty()); if !risk_bound { - slots.push(open( + slots.push(open_with_notes( "risk", "optional, list of stop regimes { vol: { length, k } }; absent = one default regime", + vec![ + "vol.length smooths the per-cycle vol estimator; it does not set the stop's timescale.".to_string(), + "vol.k scales the stop distance (the risk-unit lever).".to_string(), + ], )); } // The optional cost slot, mirroring the risk axis (#216's discoverability // lesson applied to #234): absent, empty, or non-array = open. let cost_bound = v.get("cost").and_then(|c| c.as_array()).is_some_and(|a| !a.is_empty()); if !cost_bound { - slots.push(open( + slots.push(open_with_notes( "cost", "optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)", + vec![ + "constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(), + "vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(), + ], )); } match v.get("strategies").and_then(|s| s.as_array()) { @@ -1932,13 +1952,21 @@ mod tests { assert_eq!( open_slots_campaign(CAMPAIGN_FIXTURE).unwrap(), vec![ - open( + open_with_notes( "risk", "optional, list of stop regimes { vol: { length, k } }; absent = one default regime", + vec![ + "vol.length smooths the per-cycle vol estimator; it does not set the stop's timescale.".to_string(), + "vol.k scales the stop distance (the risk-unit lever).".to_string(), + ], ), - open( + open_with_notes( "cost", "optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)", + vec![ + "constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(), + "vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(), + ], ), ] ); @@ -2012,9 +2040,13 @@ mod tests { assert_ne!(with_risk, CAMPAIGN_FIXTURE, "replacen must match the fixture's seed field"); assert_eq!( open_slots_campaign(&with_risk).unwrap(), - vec![open( + vec![open_with_notes( "cost", "optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)", + vec![ + "constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(), + "vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(), + ], )] ); let with_both = CAMPAIGN_FIXTURE.replacen( diff --git a/docs/glossary.md b/docs/glossary.md index 51cfcf1..de3be55 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -73,7 +73,7 @@ A feed-forward, order-independent research axis for scaling risk by bias strengt ### cost model **Avoid:** realistic broker -A composable downstream **C9 graph of cost nodes**, in **R**, that **approximates** (never claims) a broker's cost: each cost node reads the state it depends on (price, realized-volatility, a C11-recorded rate source, the executor's per-trade R-records) and emits a **cost-in-R** stream subtracted from gross R to yield **net R** (per-trade factors deduct at close, per-cycle-held factors accrue over the hold). It generalizes / subsumes the scalar `round_trip_cost` (its degenerate constant-per-trade case), lives in `aura-std` / `aura-composites`, is optional (zero-cost baseline), and demands every factor be a labelled stress-parameter **or** data-grounded (over-modelling is the anti-pattern). +A composable downstream **C9 graph of cost nodes**, in **R**, that **approximates** (never claims) a broker's cost: each cost node reads the state it depends on (price, realized-volatility, a C11-recorded rate source, the executor's per-trade R-records) and emits a **cost-in-R** stream subtracted from gross R to yield **net R** (per-trade factors deduct at close, per-cycle-held factors accrue over the hold). It generalizes / subsumes the scalar `round_trip_cost` (its degenerate constant-per-trade case — `cost_per_trade` is a **price-unit** numerator, R-normalized per trade as `cost/|entry−stop|`, not a cost in R), lives in `aura-std` / `aura-composites`, is optional (zero-cost baseline), and demands every factor be a labelled stress-parameter **or** data-grounded (over-modelling is the anti-pattern). ### cross-instrument generalization **Avoid:** cross-symbol pooling, pooled generalization @@ -235,7 +235,10 @@ One entry of a campaign document's structural risk axis (`risk`): a serializable protective-stop regime (sole variant `vol{length,k}`) the matrix runs every cell under, so cells differ by execution discipline, never by signal. Absent or empty = one implicit default regime; the regime's stop -defines the risk unit R. +defines the risk unit R — in `vol{length,k}` (stop = k·√EMA(Δ², length) over +m1 cycles) `length` only smooths the vol estimator while `k` scales the stop +distance, so the stop's timescale stays one cycle (#262 tracks a +timescale-matched variant). ### run **Avoid:** —