feat(research,cli,docs): state cost and risk knob units in introspect and the glossary

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
This commit is contained in:
2026-07-13 23:12:36 +02:00
parent e12b4a730b
commit e8b6878d3b
3 changed files with 46 additions and 8 deletions
+3
View File
@@ -278,6 +278,9 @@ fn describe_one_block(id: &str) -> Result<(), String> {
fn print_open_slots(slots: &[aura_research::OpenSlot]) { fn print_open_slots(slots: &[aura_research::OpenSlot]) {
for s in slots { for s in slots {
println!("open slot: {} ({})", s.path, s.hint); println!("open slot: {} ({})", s.path, s.hint);
for note in &s.notes {
println!(" {note}");
}
} }
if slots.is_empty() { if slots.is_empty() {
println!("no open slots"); println!("no open slots");
+38 -6
View File
@@ -978,10 +978,22 @@ pub struct OpenSlot {
pub path: String, pub path: String,
/// Short human hint, e.g. "required, one of: content_id, identity_id". /// Short human hint, e.g. "required, one of: content_id, identity_id".
pub hint: String, 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<String>,
} }
fn open(path: impl Into<String>, hint: impl Into<String>) -> OpenSlot { fn open(path: impl Into<String>, hint: impl Into<String>) -> 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<String>, hint: impl Into<String>, notes: Vec<String>) -> OpenSlot {
OpenSlot { path: path.into(), hint: hint.into(), notes }
} }
/// List the open slots of a (possibly partial) process document. Only the /// List the open slots of a (possibly partial) process document. Only the
@@ -1064,18 +1076,26 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
// how it went undiscovered (#216). Absent, empty, or non-array = open. // 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()); let risk_bound = v.get("risk").and_then(|r| r.as_array()).is_some_and(|a| !a.is_empty());
if !risk_bound { if !risk_bound {
slots.push(open( slots.push(open_with_notes(
"risk", "risk",
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime", "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 // The optional cost slot, mirroring the risk axis (#216's discoverability
// lesson applied to #234): absent, empty, or non-array = open. // 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()); let cost_bound = v.get("cost").and_then(|c| c.as_array()).is_some_and(|a| !a.is_empty());
if !cost_bound { if !cost_bound {
slots.push(open( slots.push(open_with_notes(
"cost", "cost",
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)", "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()) { match v.get("strategies").and_then(|s| s.as_array()) {
@@ -1932,13 +1952,21 @@ mod tests {
assert_eq!( assert_eq!(
open_slots_campaign(CAMPAIGN_FIXTURE).unwrap(), open_slots_campaign(CAMPAIGN_FIXTURE).unwrap(),
vec![ vec![
open( open_with_notes(
"risk", "risk",
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime", "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", "cost",
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)", "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_ne!(with_risk, CAMPAIGN_FIXTURE, "replacen must match the fixture's seed field");
assert_eq!( assert_eq!(
open_slots_campaign(&with_risk).unwrap(), open_slots_campaign(&with_risk).unwrap(),
vec![open( vec![open_with_notes(
"cost", "cost",
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)", "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( let with_both = CAMPAIGN_FIXTURE.replacen(
+5 -2
View File
@@ -73,7 +73,7 @@ A feed-forward, order-independent research axis for scaling risk by bias strengt
### cost model ### cost model
**Avoid:** realistic broker **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/|entrystop|`, 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 ### cross-instrument generalization
**Avoid:** cross-symbol pooling, pooled 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 serializable protective-stop regime (sole variant `vol{length,k}`) the matrix
runs every cell under, so cells differ by execution discipline, never by runs every cell under, so cells differ by execution discipline, never by
signal. Absent or empty = one implicit default regime; the regime's stop 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 ### run
**Avoid:** — **Avoid:** —