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:
@@ -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<String>,
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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.
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user