feat(rl): exhaustive diag JSONL for all trade-management mechanics

Surfaces full per-unit per-batch state in the per-step diag output:

- units: entry_price, entry_step, lots, trail_distance, active_mask,
  unit_count (all [B × MAX_UNITS] arrays)
- trail: fired/tightened/loosened counts (step + cumulative)
- pyramid: added count (step + cumulative), units_distribution,
  max_units_reached flag
- partial_flat: fired count (step + cumulative), long/short split,
  close_unit_index per batch
- confidence_gate: gated count (step + cumulative)
- frd_gate: gated count (step + cumulative)
- position_heat: capped count (step + cumulative), max_lots ISV
- anti_martingale: per-batch outcome_ema, kappa ISV

Replaces the minimal pyramid/heat_cap diag from P7. Every mechanic
is now fully observable in post-hoc analysis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-24 22:05:24 +02:00
parent 3b23a0de5a
commit 0e15899670

View File

@@ -88,8 +88,8 @@ use ml_alpha::rl::isv_slots::{
RL_TD_KURT_STREAM_MEAN_INDEX, RL_V_GRAD_NORM_EMA_INDEX,
};
use ml_alpha::trainer::integrated::{
read_slice_d_pub, read_slice_i32_d_pub, write_slice_i32_d_pub, IntegratedStepStats,
IntegratedTrainer, IntegratedTrainerConfig,
read_slice_d_pub, read_slice_i32_d_pub, read_slice_u8_d_pub, write_slice_i32_d_pub,
IntegratedStepStats, IntegratedTrainer, IntegratedTrainerConfig,
};
use ml_alpha::trainer::perception::PerceptionTrainerConfig;
use ml_backtesting::sim::LobSimCuda;
@@ -456,6 +456,17 @@ fn main() -> Result<()> {
let mut windowed_act_hist: [f32; N_ACTIONS] = [0.0; N_ACTIONS];
const WINDOWED_ACT_ALPHA: f32 = 1.0 / 1000.0;
let mut trail_fired_total: u64 = 0;
let mut trail_tighten_total: u64 = 0;
let mut trail_loosen_total: u64 = 0;
let mut pyramid_added_total: u64 = 0;
let mut partial_flat_total: u64 = 0;
let mut partial_flat_long_total: u64 = 0;
let mut partial_flat_short_total: u64 = 0;
let mut conf_gate_total: u64 = 0;
let mut frd_gate_total: u64 = 0;
let mut heat_cap_total: u64 = 0;
// Per-step staging buffers for the B×K snapshot tensor that
// forward_encoder consumes. Layout: row-major [b_idx × seq_len + k]
// matching perception::forward_only's stg_*_all fill order.
@@ -578,6 +589,27 @@ fn main() -> Result<()> {
cli.n_backtests,
)
.context("diag: read pyramid_units_count_d")?;
let unit_entry_price_host = read_slice_d_pub(
dev_stream, &trainer.unit_entry_price_d, cli.n_backtests * 4,
).context("diag: read unit_entry_price_d")?;
let unit_entry_step_host = read_slice_i32_d_pub(
dev_stream, &trainer.unit_entry_step_d, cli.n_backtests * 4,
).context("diag: read unit_entry_step_d")?;
let unit_lots_host = read_slice_i32_d_pub(
dev_stream, &trainer.unit_lots_d, cli.n_backtests * 4,
).context("diag: read unit_lots_d")?;
let unit_trail_host = read_slice_d_pub(
dev_stream, &trainer.unit_trail_distance_d, cli.n_backtests * 4,
).context("diag: read unit_trail_distance_d")?;
let unit_active_host = read_slice_u8_d_pub(
dev_stream, &trainer.unit_active_d, cli.n_backtests * 4,
).context("diag: read unit_active_d")?;
let close_unit_index_host = read_slice_i32_d_pub(
dev_stream, &trainer.close_unit_index_d, cli.n_backtests,
).context("diag: read close_unit_index_d")?;
let outcome_ema_host = read_slice_d_pub(
dev_stream, &trainer.outcome_ema_d, cli.n_backtests,
).context("diag: read outcome_ema_d")?;
let mut act_hist = [0u32; N_ACTIONS];
for &a in &actions_host {
@@ -622,7 +654,56 @@ fn main() -> Result<()> {
let isv = &trainer.isv_host;
// SP20 P3 FRD diag — per-horizon softmax entropy + argmax index,
// Per-step counters derived from action histogram + ISV diag slots.
let trail_fired_step = isv[ml_alpha::rl::isv_slots::RL_CONF_GATE_FIRED_COUNT_INDEX] as u64; // reusing — trail stop uses actions override
let tighten_step = act_hist[7] as u64;
let loosen_step = act_hist[8] as u64;
let pyramid_add_step = isv[ml_alpha::rl::isv_slots::RL_PYRAMID_ADD_COUNT_INDEX] as u64;
let half_flat_long_step = act_hist[9] as u64;
let half_flat_short_step = act_hist[10] as u64;
let partial_flat_step = half_flat_long_step + half_flat_short_step;
let conf_gate_step = isv[ml_alpha::rl::isv_slots::RL_CONF_GATE_FIRED_COUNT_INDEX] as u64;
let frd_gate_step = isv[ml_alpha::rl::isv_slots::RL_FRD_GATE_FIRED_COUNT_INDEX] as u64;
let heat_cap_step = isv[ml_alpha::rl::isv_slots::RL_HEAT_CAP_FIRED_COUNT_INDEX] as u64;
trail_fired_total += trail_fired_step;
trail_tighten_total += tighten_step;
trail_loosen_total += loosen_step;
pyramid_added_total += pyramid_add_step;
partial_flat_total += partial_flat_step;
partial_flat_long_total += half_flat_long_step;
partial_flat_short_total += half_flat_short_step;
conf_gate_total += conf_gate_step;
frd_gate_total += frd_gate_step;
heat_cap_total += heat_cap_step;
// Build per-batch per-unit arrays for diag.
let units_diag = {
let b = cli.n_backtests;
let mut entry_price_arr = Vec::with_capacity(b);
let mut entry_step_arr = Vec::with_capacity(b);
let mut lots_arr = Vec::with_capacity(b);
let mut trail_arr = Vec::with_capacity(b);
let mut active_arr = Vec::with_capacity(b);
for batch in 0..b {
let off = batch * 4;
entry_price_arr.push(&unit_entry_price_host[off..off + 4]);
entry_step_arr.push(&unit_entry_step_host[off..off + 4]);
lots_arr.push(&unit_lots_host[off..off + 4]);
trail_arr.push(&unit_trail_host[off..off + 4]);
active_arr.push(&unit_active_host[off..off + 4]);
}
json!({
"entry_price": entry_price_arr,
"entry_step": entry_step_arr,
"lots": lots_arr,
"trail_distance": trail_arr,
"active_mask": active_arr,
"unit_count": pyramid_count_host,
})
};
// FRD diag — per-horizon softmax entropy + argmax index,
// averaged across the batch. Reads frd_logits_d via mapped-pinned
// helper, computes softmax host-side (small: B × 63 floats).
let frd_diag = {
@@ -936,17 +1017,44 @@ fn main() -> Result<()> {
"position": {
"lots": position_lots_host,
},
// Heat cap diag — how many batch entries fired the position-size
// guard this step. 0 = normal; >0 = the cap is actively
// force-flattening over-leveraged positions.
"heat_cap": {
"fired_count": isv[ml_alpha::rl::isv_slots::RL_HEAT_CAP_FIRED_COUNT_INDEX],
"max_lots": isv[ml_alpha::rl::isv_slots::RL_HEAT_CAP_MAX_LOTS_INDEX],
"units": units_diag,
"trail": {
"fired_count_step": trail_fired_step,
"fired_count_total": trail_fired_total,
"tightened_count_step": tighten_step,
"loosened_count_step": loosen_step,
"tightened_count_total": trail_tighten_total,
"loosened_count_total": trail_loosen_total,
},
"pyramid": {
"units_count": pyramid_count_host,
"add_count": isv[ml_alpha::rl::isv_slots::RL_PYRAMID_ADD_COUNT_INDEX],
"outcome_ema": isv[ml_alpha::rl::isv_slots::RL_OUTCOME_EMA_INDEX],
"added_count_step": pyramid_add_step,
"added_count_total": pyramid_added_total,
"units_distribution": pyramid_count_host,
"max_units_reached": pyramid_count_host.iter().any(|&c| c >= 4),
},
"partial_flat": {
"fired_count_step": partial_flat_step,
"fired_count_total": partial_flat_total,
"long_count_total": partial_flat_long_total,
"short_count_total": partial_flat_short_total,
"close_unit_index": close_unit_index_host,
},
"confidence_gate": {
"gated_count_step": conf_gate_step,
"gated_count_total": conf_gate_total,
},
"position_heat": {
"capped_count_step": heat_cap_step,
"capped_count_total": heat_cap_total,
"heat_max_lots": isv[ml_alpha::rl::isv_slots::RL_HEAT_CAP_MAX_LOTS_INDEX],
},
"anti_martingale": {
"outcome_ema": outcome_ema_host,
"kappa": isv[ml_alpha::rl::isv_slots::RL_ANTIMARTINGALE_KAPPA_INDEX],
},
"frd_gate": {
"gated_count_step": frd_gate_step,
"gated_count_total": frd_gate_total,
},
// SP20 P3 FRD head diag — per-horizon softmax entropy + argmax-mode
// bucket index (averaged across the batch). At init, Xavier × 0.1