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:
@@ -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");
|
||||||
|
|||||||
@@ -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
@@ -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/|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
|
### 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:** —
|
||||||
|
|||||||
Reference in New Issue
Block a user