spec(sp12): per-trade event-driven reward composition (v3)

Three architectural changes in one unified design to push the model from
HFT noise extraction (62% trade rate, sharpe-gaming) toward MFT alpha-hunting:

1. Asymmetric bounded cap (-10/+5) — restores loss aversion erased by
   SP11 symmetric cap. 2:1 ratio matches Kahneman/Tversky prospect theory.
   Anchor: pearl_audit_unboundedness_for_implicit_asymmetry.

2. Min-hold soft penalty with temperature curriculum — patience requirement
   at exit. Soft factor = deficit/(deficit+T), T anneals 50→5 over 50 epochs.
   Forces commitment without paralysis in early training.

3. Zero per-bar shaping (gate micro/opp_cost on events) — eliminates
   continuous-reward gradient that pulls toward continuous exposure.
   Anchor: pearl_event_driven_reward_density_alignment.

Combined: reward fires only on trade events with prospect-theory loss
aversion + commitment requirement. Pure per-trade event-driven Q-learning
properly aligned with per-trade P&L objective.

~50 LOC across 3-4 files. No new ISV slots in Phase 1 (constants only).
Cost ~€1.30 (€0.30 smoke + €1.00 30-epoch validation).

Empirical motivation: train-multi-seed-pmbwn 50-epoch on commit 6a259942e
showed sharpe-gaming pattern (PnL -30% over 8 epochs while sharpe held).
SP11 cap fix unmasked the per-bar shaping bias plus erased loss-aversion
that the unbounded loss path was implicitly providing.

Continues on sp11-reward-as-controlled-subsystem branch — SP12 is
architectural continuation of SP11, not separate work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-04 18:41:57 +02:00
parent 6a259942e9
commit ecab584c32

View File

@@ -0,0 +1,275 @@
# SP12 — Per-Trade Event-Driven Reward Composition
**Date**: 2026-05-04 (v3 — unified single-spec solution)
**Branch**: continues on `sp11-reward-as-controlled-subsystem` (SP12 is architectural continuation of SP11; no fork)
**Builds on**: SP11 B1b symmetric reward cap (commits `35db31089`, `348f6078b`, `6a259942e`)
**Anchor pearls**:
- `pearl_event_driven_reward_density_alignment` (the architectural insight)
- `pearl_audit_unboundedness_for_implicit_asymmetry` (the asymmetric-cap component)
**Supersedes**: SP12 v1 (kitchen sink) and v2 (asymmetric cap only). v3 is the unified
architectural design that addresses all three causes of HFT-pull as a single coherent
solution.
## TL;DR
Reshape reward composition to be **purely per-trade event-driven** (matches objective
density). Three changes in one commit:
1. **Asymmetric bounded cap** (loss aversion at exit events)
2. **Min-hold soft penalty with temperature** (commitment at exit events)
3. **Zero per-bar shaping rewards** (no continuous reward gradient)
Combined: model only gets reward on trade events, with prospect-theory loss aversion and
patience requirement. ~50 LOC. Direct attack on MFT goal.
## Background
50-epoch validation `train-multi-seed-pmbwn` on commit `6a259942e` revealed sharpe-gaming:
| Epoch | sharpe | total_pnl | trade_count | win_rate |
|-------|--------|-----------|-------------|----------|
| 0 | 75.19 | 0.5015 | 134,348 | 0.4601 |
| 8 | 75.42 | 0.3502 | 123,970 | 0.4637 |
Sharpe held while PnL declined 30%. Trade rate 62% (134k/214k bars) — pure HFT noise
extraction territory. User goal: MFT-leaning trading (~20-30% trade rate, 55-60% win rate,
larger per-trade PnL, longer holds).
## Three causes of HFT pull (architecturally distinct)
| Cause | Mechanism | Pearl | Phase 1 fix |
|-------|-----------|-------|-------------|
| 1. Lost loss aversion | SP11 symmetric cap erased pre-fix downside-asymmetry → risk-neutral policy | `pearl_audit_unboundedness_for_implicit_asymmetry` | Asymmetric cap |
| 2. Per-bar reward gradient | `micro_reward` + `opp_cost` fire every bar → model rewards continuous exposure | `pearl_event_driven_reward_density_alignment` | Zero per-bar shaping |
| 3. No commitment requirement | Same-bar entry/exit possible without penalty → fast scalps | (none yet — may emerge from validation) | Min-hold soft penalty |
Each acts at a different point in the reward pipeline. Together they constitute the unified
per-trade event-driven design.
## The unified design
### Design 1 — Asymmetric bounded cap (loss aversion)
**File**: `crates/ml/src/cuda_pipeline/experience_kernels.cu` line ~2788
**File**: `crates/ml/src/cuda_pipeline/backtest_plan_kernel.cu` matching site
```cuda
// SP11 (current): symmetric bounded
float capped_pnl = fmaxf(-10.0f, fminf(base_reward, 10.0f));
// SP12 v3: asymmetric — 2:1 prospect theory loss aversion
float capped_pnl = fmaxf(REWARD_NEG_CAP, fminf(base_reward, REWARD_POS_CAP));
// -10.0f +5.0f
```
**Rationale**: Kahneman/Tversky meta-analysis pegs human loss aversion at ~2.0-2.25.
Calibration to a known psychological constant, not arbitrary tuning. Preserves SP11's
stability work (still bounded both sides). Restores selectivity without requiring controllers.
### Design 2 — Min-hold soft penalty with temperature curriculum
**File**: `crates/ml/src/cuda_pipeline/experience_kernels.cu` — at exit branch
```cuda
if (exiting_trade && hold_time < min_hold_target) {
float deficit = min_hold_target - hold_time;
/* Soft saturation: factor → 1 as deficit → ∞, factor → 0 as deficit → 0.
* Temperature controls smoothness — higher T = wider transition. */
float soft_factor = deficit / (deficit + temperature);
float penalty = penalty_max * soft_factor;
reward -= penalty;
}
```
**Parameters** (Phase 1 — static constants):
- `min_hold_target = 30` bars (MFT-leaning; 100 = full MFT, 10 = HFT-tolerant)
- `temperature` = annealing schedule: `T_start=50, T_end=5, decay over 50 epochs`
- `penalty_max = 3.0` (60% of asymmetric pos_cap; meaningful but not paralyzing)
**Why temperature annealing**: early training needs forgiveness (model can experiment with
short trades to learn reward gradients). Late training needs sharpness (enforce MFT
commitment). Standard simulated-annealing curriculum:
```
T(epoch) = T_end + (T_start - T_end) * exp(-epoch / decay_rate)
≈ 50 at epoch 0, 5 at epoch 50, smooth transition between
```
ISV slot for temperature is constructor-set per epoch (not adaptive controller). Avoids
oscillation, gives clean curriculum.
### Design 3 — Zero per-bar shaping (event-driven density)
**File**: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
In the per-bar reward composition path, gate `micro_reward` and `opp_cost` on `exiting_trade
|| entering_trade`:
```cuda
// SP11 (current): fires every bar with position
r_micro = -shaping_scale * holding_cost_rate * fabsf(position);
// SP12 v3: fires only on trade events
if (exiting_trade || entering_trade) {
r_micro = -shaping_scale * holding_cost_rate * fabsf(position) * hold_time;
/* Apply accumulated cost as one-shot at trade boundary instead of per-bar.
* Mathematically equivalent under γ=1, slightly different under γ<1 due to
* discount, but discount difference is small for short trades. */
}
```
Same pattern for `r_micro` (micro_reward) — gate on event, not continuous.
**Rationale**: per-bar shaping creates the implicit "exposure-positive" gradient
(`pearl_event_driven_reward_density_alignment`). Removing it eliminates HFT pull at the
source. The `opp_cost` accumulation effect is preserved (lump-sum at exit) without the
per-bar density bias.
### Combined: per-trade event-driven reward chain
After all three changes, reward fires at:
| Event | Reward |
|-------|--------|
| Entering trade (hold_time = 0) | 0 (or small entry friction in Phase 2) |
| Within trade (no event) | **0** (this is the architectural change) |
| Trail-fire forced exit | `capped_pnl_asymmetric ± min_hold_penalty + accumulated_opp_cost` |
| Voluntary exit | `capped_pnl_asymmetric ± min_hold_penalty + accumulated_opp_cost` |
| Stop-loss / capital floor | existing penalty (unchanged) |
This is the cleanest possible per-trade event-driven reward design.
## Implementation phases
### Phase 1 — single atomic commit (~50 LOC)
All three changes together as one atomic commit per `feedback_no_partial_refactor`:
**Files**:
- `crates/ml/src/cuda_pipeline/experience_kernels.cu` — asymmetric cap (line 2788) + min-hold
penalty at exit + zero per-bar shaping (gate micro/opp_cost on events)
- `crates/ml/src/cuda_pipeline/backtest_plan_kernel.cu` — asymmetric cap matching change
- `crates/ml/src/cuda_pipeline/state_layout.cuh` (or equivalent header) — new constants:
`REWARD_POS_CAP`, `REWARD_NEG_CAP`, `MIN_HOLD_TARGET`, `MIN_HOLD_PENALTY_MAX`,
`MIN_HOLD_TEMPERATURE_START`, `MIN_HOLD_TEMPERATURE_END`, `MIN_HOLD_TEMPERATURE_DECAY`
- `crates/ml/src/trainers/dqn/trainer/training_loop.rs` — pass current temperature to env_step
kernel based on epoch (annealing computation)
**LOC estimate**: ~50 across all files.
**Tests**:
- GPU oracle test confirming `capped_pnl(20) = 5`, `capped_pnl(-20) = -10`
- GPU oracle test confirming min-hold penalty: `hold_time=0, target=30, T=10 → penalty=penalty_max × 30/40 = 0.75 × penalty_max`
- GPU oracle test confirming per-bar reward = 0 when `!exiting && !entering` and in-position
### Phase 2 (deferred — only if Phase 1 results indicate need)
ISV-driven adaptation:
- `MIN_HOLD_TARGET_INDEX` — adaptive based on observed best-PF hold-time bucket
- `LOSS_AVERSION_RATIO_INDEX` — adaptive based on observed trade rate
Don't build speculatively. Phase 1 results dictate.
## Validation
### Smoke (5-epoch L40S, ~€0.30)
**Must pass**:
- Reward distribution: `max(reward) ≤ +5 + ε`, `min(reward) ≥ -10 - max_min_hold_penalty - ε`
- Slot 63 PopArt EMA stays bounded (will be lower than SP11 since most bars now have reward=0)
- Mean(weights) ≈ 1.0
- No NaN/inf
- Training converges (loss not exploding)
- Trade rate visibly different (smoke can't fully assess MFT but trend should be visible)
### Full validation (30-epoch L40S, ~€1.00)
Compare against SP11 baseline `train-multi-seed-pmbwn` 8-epoch trajectory (the only baseline
we have):
**Behavioral targets** (success):
- trade_count drops 40-70% (target: 40-80k vs 134k baseline)
- mean hold_time increases (target: 30-100 bars vs current ~5-10 bars)
- win_rate ≥ 52% (vs SP11 baseline 46%)
- profit_factor ≥ 1.4 (vs SP11 baseline 1.25)
- total_pnl ≥ 0.45 (vs SP11 baseline declining toward 0.35; longer trades should yield bigger per-trade PnL)
- max_drawdown ≤ 0.0030 (loss aversion may slightly increase per-trade DD, fine)
**Acceptable**:
- sharpe in 30-80 range (sharpe-gaming was the problem; absolute PnL is the goal)
**Failure modes**:
- Training diverges or loss explodes → reward density change too aggressive; need gentler
transition (e.g., ramp shaping_scale instead of zero-ing)
- Model paralyzed (trade_count → 0) → constraints too strong; reduce min_hold_target or
pos_cap aggressiveness
- No behavior change → constraints not reaching policy; debug ISV plumbing or smaller
Q-target effects than expected
- Slot 63 inflation OR NaN → re-introduces SP11 pathology. STOP, debug.
## Operating principles (NON-NEGOTIABLE)
- **feedback_no_atomicadd**: not relevant, no new producers
- **feedback_no_partial_refactor**: Phase 1 single atomic commit
- **feedback_isv_for_adaptive_bounds**: Phase 1 uses Invariant-1 numerical anchors as
static constants. Phase 2 (if triggered) lifts to ISV-driven controllers per pearl.
- **feedback_no_quickfixes**: this IS the proper architectural fix per both pearls
- **feedback_no_feature_flags**: behavior change unconditional
- **pearl_event_driven_reward_density_alignment**: drives the per-bar removal
- **pearl_audit_unboundedness_for_implicit_asymmetry**: drives the asymmetric cap
## Estimated cost
**Phase 1**:
- Implementation: ~1-2 hours (50 LOC across 3-4 files + 3 GPU oracle tests)
- Smoke validation: ~€0.30 (5 epochs)
- Full validation: ~€1.00 (30 epochs)
- **Total: ~€1.30**
**Phase 2 (if triggered)**:
- ISV slots + 1-2 controllers: ~€2-3 additional
vs original v1 (€4) and v2 (€1.30 partial). v3 is comparable cost to v2 but addresses ALL
three causes in one shot.
## Open questions
1. **Static constants in Phase 1 — values?**
- Asymmetric cap: -10 / +5 (2:1 prospect theory). My take: keep.
- min_hold_target: 30 bars (MFT-leaning). User goal "between HFT and MFT" suggests this
is the right ballpark. Could be 20 (more HFT) or 50 (more MFT). My take: 30 as default.
- penalty_max: 3.0 (60% of pos_cap). My take: keep.
- Temperature schedule: T_start=50, T_end=5, decay=20 epochs. My take: keep.
2. **Should opp_cost preserve as lump-sum at exit, or zero entirely?**
- Lump-sum: `accumulated_opp_cost = -k × |position| × hold_time` applied at exit. Preserves
the carry-cost concept.
- Zero: just remove opp_cost entirely. Simpler.
- My take: lump-sum at exit. Carry cost is a real economic concept; preserving it as
event-driven respects pearl_event_driven_reward_density_alignment without losing the
signal.
3. **micro_reward — zero entirely or move to event?**
- micro_reward was a per-bar "looking good" shaping reward. Its role is dubious.
- Zero entirely: simplest, aligned with pearl.
- Move to event: complex, signal value unclear post-removal.
- My take: zero entirely. Per pearl, per-bar shaping for credit assignment is the
anti-pattern. Q-learning will handle credit assignment via TD targets.
4. **Min-hold penalty applied to ALL exits or only voluntary?**
- All exits: trail-fire forced exits ALSO get penalty if early. Could conflict with
stop-loss design.
- Voluntary only: trail-fire / stop-loss exempted.
- My take: voluntary only. Trail-stop is a forced exit by design — penalizing it would
fight the existing risk-management logic.
5. **Worktree fork strategy?**
- SP12 builds on SP11 cap fix. Fork `sp12-event-driven-reward` from
`sp11-reward-as-controlled-subsystem` HEAD (`6a259942e`).
- Keep SP11 worktree clean for any retroactive amendments.
## Decision needed
Approve v3 spec → write minimal implementation plan → dispatch implementer with worktree
fork → smoke → 30-epoch validate → close-out commit (audit doc + memory pearl
empirical-validation update).