feat(diag): add per-trade PnL, win rate, hold time, sps to JSONL + log

New diag fields in the "trading" object:
- pnl_cum_usd: cumulative realized PnL in USD (shaped, pre-scale)
- total_trades: lifetime trade count
- win_rate: fraction of positive-PnL closes
- avg_hold_steps: mean hold duration in steps
- raw_reward_sum: per-step sum of shaped rewards

Log line now shows sps (steps/sec), pnl (cumulative $), wr (win rate).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-25 23:45:47 +02:00
parent f5d8f5f056
commit 7b983e5c4f
2 changed files with 38 additions and 3 deletions

View File

@@ -455,6 +455,10 @@ 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 pnl_cum_usd: f64 = 0.0;
let mut win_count: u64 = 0;
let mut total_trades: u64 = 0;
let mut hold_time_sum: f64 = 0.0;
let mut trail_fired_total: u64 = 0;
let mut trail_tighten_total: u64 = 0;
let mut trail_loosen_total: u64 = 0;
@@ -609,6 +613,12 @@ fn main() -> Result<()> {
let outcome_ema_host = read_slice_d_pub(
dev_stream, &trainer.outcome_ema_d, cli.n_backtests,
).context("diag: read outcome_ema_d")?;
let raw_rewards_host = read_slice_d_pub(
dev_stream, &trainer.raw_rewards_d, cli.n_backtests,
).context("diag: read raw_rewards_d")?;
let trade_duration_host = read_slice_d_pub(
dev_stream, &trainer.trade_duration_emit_d, cli.n_backtests,
).context("diag: read trade_duration_emit_d")?;
let mut act_hist = [0u32; N_ACTIONS];
for &a in &actions_host {
@@ -651,6 +661,19 @@ fn main() -> Result<()> {
let reward_abs_max = rewards_host.iter().map(|r| r.abs()).fold(0.0f32, f32::max);
let done_count: u32 = dones_host.iter().map(|&d| if d > 0.5 { 1 } else { 0 }).sum();
// Per-trade cumulative stats (surfer validation).
for b in 0..cli.n_backtests {
if dones_host[b] > 0.5 {
let pnl = raw_rewards_host[b] as f64;
pnl_cum_usd += pnl;
total_trades += 1;
if pnl > 0.0 { win_count += 1; }
hold_time_sum += trade_duration_host[b] as f64;
}
}
let win_rate = if total_trades > 0 { win_count as f64 / total_trades as f64 } else { 0.0 };
let avg_hold = if total_trades > 0 { hold_time_sum / total_trades as f64 } else { 0.0 };
let isv = &trainer.isv_host;
// Per-step counters derived from action histogram + ISV diag slots.
@@ -1055,6 +1078,13 @@ fn main() -> Result<()> {
"gated_count_step": frd_gate_step,
"gated_count_total": frd_gate_total,
},
"trading": {
"pnl_cum_usd": pnl_cum_usd,
"total_trades": total_trades,
"win_rate": win_rate,
"avg_hold_steps": avg_hold,
"raw_reward_sum": raw_rewards_host.iter().sum::<f32>(),
},
// SP20 P3 FRD head diag — per-horizon softmax entropy + argmax-mode
// bucket index (averaged across the batch). At init, Xavier × 0.1
// weights → logits ≈ 0 → entropy ≈ ln(21) = 3.044 and the argmax
@@ -1067,10 +1097,12 @@ fn main() -> Result<()> {
if step % cli.log_every == 0 || step == cli.n_steps - 1 {
diag.flush().context("diag: flush")?;
let elapsed = t_start.elapsed().as_secs_f32();
let sps = if elapsed > 0.0 { (step + 1) as f32 / elapsed } else { 0.0 };
eprintln!(
"step {:>6}/{}: l_q={:.4} l_pi={:.4} l_v={:.4} l_total={:.4} \
γ={:.4} ε={:.4} per_α={:.4} scale={:.4} \
replay={} dones={} rew_sum={:.3} elapsed={:.1}s",
replay={} dones={} rew_sum={:.3} sps={:.0} pnl=${:.0} wr={:.2} elapsed={:.1}s",
step,
cli.n_steps,
stats.l_q,
@@ -1084,7 +1116,10 @@ fn main() -> Result<()> {
trainer.replay.len(),
done_count,
reward_sum,
t_start.elapsed().as_secs_f32()
sps,
pnl_cum_usd,
win_rate,
elapsed,
);
}
last_stats = Some(stats);

View File

@@ -643,7 +643,7 @@ pub struct IntegratedTrainer {
/// `LobSimCuda::prev_realized_pnl_d` (which serves the production
/// decision pipeline, not the RL trainer).
pub prev_realized_pnl_d: CudaSlice<f32>,
raw_rewards_d: CudaSlice<f32>,
pub raw_rewards_d: CudaSlice<f32>,
/// Trainer-owned snapshot of `lobsim.pos.position_lots` for the
/// done-flag detection: `done = (prev != 0 && current == 0)`.
pub prev_position_lots_d: CudaSlice<i32>,