refactor(stage1-r): name the conviction column conviction_at_entry

Rename the dense PositionManagement record's column 9 from `bias_at_entry_abs`
to `conviction_at_entry` (FIELD_NAMES, the `r_col`/test index constants, the
cross-crate layout guard, and the doc comments).

Why: the column holds |bias| at entry, which C10 defines AS conviction (bias =
direction + conviction magnitude; ledger INDEX.md ~458). The record already names
the other half of that decomposition by its domain concept (`direction` = sign,
not `bias_sign`), so naming the magnitude half mechanically (`bias_at_entry_abs`)
was an inconsistency; `conviction_at_entry` speaks the ledger's ubiquitous
language and parallels `direction`, while `_at_entry` preserves the
frozen-snapshot precision.

Considered and rejected: collapsing `(direction, bias_at_entry_abs)` into one
signed `bias_at_entry` and dropping `direction` as redundant — it is not
redundant. On a reversal row col 4 (`direction`) tracks the reopened leg while
col 9 tracks the closed trade's entry conviction, so the two describe different
trades and cannot be losslessly merged; they are also distinct semantic axes
(current-position direction vs per-trade entry conviction).

Scope: domain name at the data boundary, mechanical name internally — the
internal `bias.abs()` variables (`Open.bias_abs`, the fold's `Trade.bias_abs`)
keep their computation-faithful names, exactly the raw-internal / domain-external
split the record uses elsewhere.

Behaviour-preserving: cargo build --workspace clean; cargo test --workspace 500
passed, 0 failed (unchanged); clippy --all-targets -D warnings clean. The layout
guard (`r_col_indices_match_producer_field_layout`) confirms the renamed string.

refs #117
This commit is contained in:
2026-06-24 10:25:18 +02:00
parent b4e84335c4
commit 92591e5dc0
3 changed files with 12 additions and 12 deletions
+7 -7
View File
@@ -50,7 +50,7 @@ pub struct RMetrics {
pub n_open_at_end: u64, // positions force-closed at window end (counted, not hidden)
pub sqn: f64, // √n · mean_R / sample-stdev_R; n<2 or zero-variance -> 0.0
pub net_expectancy_r: f64, // mean(R - round_trip_cost / latched_dist) — churn-honest
pub conviction_terciles_r: [f64; 3], // E[R] by |bias_at_entry| tercile (asc); <3 trades -> [0,0,0]
pub conviction_terciles_r: [f64; 3], // E[R] by conviction_at_entry tercile (asc); <3 trades -> [0,0,0]
}
// Dense `PositionManagement` record column indices — the lockstep contract with
@@ -61,7 +61,7 @@ mod r_col {
pub const REALIZED_R: usize = 1;
pub const ENTRY_PRICE: usize = 6;
pub const STOP_PRICE: usize = 7;
pub const BIAS_AT_ENTRY_ABS: usize = 9;
pub const CONVICTION_AT_ENTRY: usize = 9;
pub const OPEN: usize = 11;
pub const UNREALIZED_R: usize = 12;
}
@@ -85,7 +85,7 @@ pub fn summarize_r(record: &[(Timestamp, Vec<Scalar>)], round_trip_cost: f64) ->
if row[r_col::CLOSED].as_bool() {
trades.push(Trade {
r: row[r_col::REALIZED_R].as_f64(),
bias_abs: row[r_col::BIAS_AT_ENTRY_ABS].as_f64(),
bias_abs: row[r_col::CONVICTION_AT_ENTRY].as_f64(),
latched: (row[r_col::ENTRY_PRICE].as_f64() - row[r_col::STOP_PRICE].as_f64()).abs(),
});
}
@@ -96,7 +96,7 @@ pub fn summarize_r(record: &[(Timestamp, Vec<Scalar>)], round_trip_cost: f64) ->
{
trades.push(Trade {
r: last[r_col::UNREALIZED_R].as_f64(),
bias_abs: last[r_col::BIAS_AT_ENTRY_ABS].as_f64(),
bias_abs: last[r_col::CONVICTION_AT_ENTRY].as_f64(),
latched: (last[r_col::ENTRY_PRICE].as_f64() - last[r_col::STOP_PRICE].as_f64()).abs(),
});
n_open_at_end = 1;
@@ -154,7 +154,7 @@ pub fn summarize_r(record: &[(Timestamp, Vec<Scalar>)], round_trip_cost: f64) ->
.map(|t| t.r - if t.latched > 0.0 { round_trip_cost / t.latched } else { 0.0 })
.sum();
let net_expectancy_r = net_sum / n as f64;
// conviction terciles: sort by |bias_at_entry| ascending, split into three contiguous
// conviction terciles: sort by conviction_at_entry ascending, split into three contiguous
// near-equal-count buckets (floor boundaries i*n/3), E[R] per bucket. < 3 trades -> 0s.
let conviction_terciles_r = if n < 3 {
[0.0; 3]
@@ -666,7 +666,7 @@ mod tests {
}
// A fuller closed-trade dense row: also sets entry_price (6), stop_price (7) and
// bias_at_entry_abs (9) — the geometry summarize_r recovers latched_dist and
// conviction_at_entry (9) — the geometry summarize_r recovers latched_dist and
// conviction from. Width up to UNREALIZED_R+1 (summarize_r never reads col 13).
fn r_row_full(realized: f64, entry: f64, stop: f64, bias_abs: f64) -> (Timestamp, Vec<Scalar>) {
let mut v = vec![Scalar::f64(0.0); r_col::UNREALIZED_R + 1];
@@ -674,7 +674,7 @@ mod tests {
v[r_col::REALIZED_R] = Scalar::f64(realized);
v[r_col::ENTRY_PRICE] = Scalar::f64(entry);
v[r_col::STOP_PRICE] = Scalar::f64(stop);
v[r_col::BIAS_AT_ENTRY_ABS] = Scalar::f64(bias_abs);
v[r_col::CONVICTION_AT_ENTRY] = Scalar::f64(bias_abs);
v[r_col::OPEN] = Scalar::bool(false);
(Timestamp(0), v)
}
+4 -4
View File
@@ -38,7 +38,7 @@ const OPEN: usize = 11;
const UNREALIZED_R: usize = 12;
const ENTRY_PRICE: usize = 6;
const STOP_PRICE: usize = 7;
const BIAS_AT_ENTRY_ABS: usize = 9;
const CONVICTION_AT_ENTRY: usize = 9;
const SIZE: usize = 10;
/// Property: the real producer->consumer seam composes. Driving `FixedStop` ->
@@ -264,14 +264,14 @@ fn r_col_indices_match_producer_field_layout() {
assert_eq!(PM_RECORD_KINDS[REALIZED_R], ScalarKind::F64);
assert_eq!(PM_RECORD_KINDS[UNREALIZED_R], ScalarKind::F64);
// iter-2 reads: entry_price (6), stop_price (7), bias_at_entry_abs (9) — the geometry
// iter-2 reads: entry_price (6), stop_price (7), conviction_at_entry (9) — the geometry
// summarize_r recovers latched_dist (net-of-cost) and conviction (terciles) from.
assert_eq!(PM_FIELD_NAMES[ENTRY_PRICE], "entry_price");
assert_eq!(PM_FIELD_NAMES[STOP_PRICE], "stop_price");
assert_eq!(PM_FIELD_NAMES[BIAS_AT_ENTRY_ABS], "bias_at_entry_abs");
assert_eq!(PM_FIELD_NAMES[CONVICTION_AT_ENTRY], "conviction_at_entry");
assert_eq!(PM_RECORD_KINDS[ENTRY_PRICE], ScalarKind::F64);
assert_eq!(PM_RECORD_KINDS[STOP_PRICE], ScalarKind::F64);
assert_eq!(PM_RECORD_KINDS[BIAS_AT_ENTRY_ABS], ScalarKind::F64);
assert_eq!(PM_RECORD_KINDS[CONVICTION_AT_ENTRY], ScalarKind::F64);
// iter-2 size column (10): the Sizer's `size` flows here, and the sibling
// `risk_executor.rs` fixture asserts its R-invariance by reading this index — so the
+1 -1
View File
@@ -35,7 +35,7 @@ pub const FIELD_NAMES: [&str; WIDTH] = [
"entry_price",
"stop_price",
"exit_price",
"bias_at_entry_abs",
"conviction_at_entry",
"size",
"open",
"unrealized_r",