Final 3 inline duplicates replaced with shared function calls: - execute_trade(): notional cash model (was inline cash/position update) - check_trailing_stop(): regime-adaptive trailing stop decision - enforce_hold(): hold enforcement decision (action aliasing kept inline) Training kernel now uses ALL 10 shared functions from trade_physics.cuh: decode_exposure_index, compute_target_position, decode_order_type, execute_trade, compute_tx_cost, enforce_hold, check_trailing_stop, check_capital_floor, apply_margin_cap, update_hold_time Zero inline duplicates remain. Both training and backtest kernels use identical trade physics — no more train/eval mismatches. Smoke test: Sharpe=4.89, 443 trades, no SIGSEGV. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
222 lines
9.8 KiB
Markdown
222 lines
9.8 KiB
Markdown
# Training Kernel Unification — Final Inline Duplicates
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace the 3 remaining inline duplicates in `experience_kernels.cu` with shared `trade_physics.cuh` calls, making the training kernel fully consistent with the backtest kernel.
|
|
|
|
**Architecture:** Each task replaces one inline block. The hold enforcement task is the most delicate because the training kernel has extra action-aliasing logic that must be preserved.
|
|
|
|
**Tech Stack:** CUDA C (.cu/.cuh), build.rs precompilation
|
|
|
|
---
|
|
|
|
## Remaining Inline Duplicates
|
|
|
|
| Block | Training (inline) | Shared header | Backtest | Status |
|
|
|-------|-------------------|---------------|----------|--------|
|
|
| Trade execution | `cash -= delta * raw_close; cash -= tx_cost; position = target` (line 695) | `execute_trade()` | Uses shared ✓ | **INLINE** |
|
|
| Trailing stop | Lines 756-784, regime-adaptive distance | `check_trailing_stop()` | Uses shared ✓ | **INLINE** |
|
|
| Hold enforcement | Lines 795-821, action-aliasing fix | `enforce_hold()` | Uses shared ✓ | **INLINE** (has extras) |
|
|
|
|
---
|
|
|
|
### Task 1: Wire training kernel trade execution to `execute_trade()`
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
|
|
|
|
The training kernel's trade block (around lines 688-700) is:
|
|
```c
|
|
float delta = target_position - position;
|
|
float tx_cost = 0.0f;
|
|
if (delta != 0.0f) {
|
|
int order_type_idx = decode_order_type(action_idx, b1_size, b2_size);
|
|
tx_cost = compute_tx_cost(delta, raw_close, tx_cost_multiplier, 0.0f,
|
|
max_position, order_type_idx, spread_scale);
|
|
cash -= delta * raw_close;
|
|
cash -= tx_cost;
|
|
position = target_position;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 1: Replace with `execute_trade()` call**
|
|
|
|
Replace the above with:
|
|
```c
|
|
int order_type_idx = decode_order_type(action_idx, b1_size, b2_size);
|
|
float tx_cost = execute_trade(&position, &cash, target_position, raw_close,
|
|
tx_cost_multiplier, 0.0f, max_position,
|
|
order_type_idx, spread_scale);
|
|
```
|
|
|
|
Note: `spread_scale` (CUSUM-based) is already computed before this point. It gets passed as `spread_scale_override` to `execute_trade`, which forwards it to `compute_tx_cost`. The `0.0f` is `spread_cost` (training doesn't use bid-ask spread, only the CUSUM-based model).
|
|
|
|
**IMPORTANT**: The variable `delta` is used later in the kernel (for P&L computation, reversal detection, etc.). After the refactor, `delta` is no longer computed locally. Add it back:
|
|
```c
|
|
float delta = target_position - position; /* save before execute_trade modifies position */
|
|
int order_type_idx = decode_order_type(action_idx, b1_size, b2_size);
|
|
float tx_cost = execute_trade(&position, &cash, target_position, raw_close,
|
|
tx_cost_multiplier, 0.0f, max_position,
|
|
order_type_idx, spread_scale);
|
|
```
|
|
|
|
Wait — `execute_trade` modifies `position` via the pointer. If we compute `delta` before calling `execute_trade`, `delta` has the pre-trade value. But `position` after the call is already updated. Check if `delta` is used after the trade block. Search for uses of `delta` after line 700.
|
|
|
|
If `delta` IS used later (e.g., for `raw_pnl = position * (raw_next - raw_close)` — but that uses the NEW position, not delta), just ensure the refactor doesn't break the data flow.
|
|
|
|
Actually, looking at the code: `delta` is only used inside the `if (delta != 0.0f)` block and not after. The variable `tx_cost` IS used after (for reward shaping). So the refactor is safe:
|
|
|
|
```c
|
|
float pre_trade_position = position; /* save for reversal detection */
|
|
int order_type_idx = decode_order_type(action_idx, b1_size, b2_size);
|
|
float tx_cost = execute_trade(&position, &cash, target_position, raw_close,
|
|
tx_cost_multiplier, 0.0f, max_position,
|
|
order_type_idx, spread_scale);
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `SQLX_OFFLINE=true cargo check -p ml`
|
|
|
|
- [ ] **Step 3: Verify `delta` and `position` usage after trade block**
|
|
|
|
Grep for `delta` after line 700. If used, compute it before `execute_trade`. If not, the refactor is clean.
|
|
|
|
---
|
|
|
|
### Task 2: Wire training kernel trailing stop to `check_trailing_stop()`
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
|
|
|
|
The training kernel's trailing stop (around lines 756-784) computes `vol_scale` and `trend_scale` from ADX/CUSUM, then checks if the trade return retreated from peak.
|
|
|
|
- [ ] **Step 1: Extract vol_scale and trend_scale computation (keep inline)**
|
|
|
|
The ADX/CUSUM feature reads and vol/trend scale computation (lines 748-764) must stay inline because they read from kernel-specific data structures (`features[]`, `bar_idx`, `market_dim`). Only the DECISION logic should be shared.
|
|
|
|
- [ ] **Step 2: Replace the trailing stop decision logic with shared call**
|
|
|
|
After computing `vol_scale`, `trend_scale`, and `unrealized_trade_pnl`, replace the inline check (lines 769-784):
|
|
|
|
```c
|
|
/* Old inline logic (REMOVE):
|
|
float trail_distance = 0.005f * vol_scale * trend_scale;
|
|
if (prev_sign != 0 && hold_time >= (float)min_hold_bars && peak_equity > 1.0f) {
|
|
float peak_return = ...
|
|
if (peak_return > trail_distance) {
|
|
if (unrealized_trade_pnl < trail_floor) { ... force exit ... }
|
|
}
|
|
}
|
|
*/
|
|
|
|
/* New shared call: */
|
|
if (check_trailing_stop(hold_time, min_hold_bars, peak_equity, prev_equity,
|
|
unrealized_trade_pnl, 0.005f, vol_scale, trend_scale)) {
|
|
/* Force exit — trailing stop triggered */
|
|
position = 0.0f;
|
|
float exit_cost = compute_tx_cost(ps[0], raw_close, tx_cost_multiplier, 0.0f,
|
|
max_position, 0, -1.0f);
|
|
cash = ps[1] + ps[0] * raw_close - exit_cost;
|
|
is_flat = 1.0f;
|
|
exiting_trade = 1;
|
|
}
|
|
```
|
|
|
|
Note: The exit logic (force flat, compute exit cost, update cash) stays inline because it interacts with training-specific variables (`ps[]`, `is_flat`, `exiting_trade`). Only the DECISION (should we exit?) is shared.
|
|
|
|
- [ ] **Step 3: Verify build**
|
|
|
|
Run: `SQLX_OFFLINE=true cargo check -p ml`
|
|
|
|
---
|
|
|
|
### Task 3: Wire training kernel hold enforcement to `enforce_hold()`
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
|
|
|
|
This is the most delicate task. The training kernel's hold enforcement (lines 795-821) has EXTRA logic that `enforce_hold()` doesn't handle:
|
|
1. Action aliasing fix: overrides `action_idx` to match the held exposure
|
|
2. Sign recomputation after override
|
|
3. Sets `wants_exit = 0`, `wants_reversal = 0`
|
|
|
|
- [ ] **Step 1: Use `enforce_hold()` for the DECISION only**
|
|
|
|
Replace the hold violation detection:
|
|
```c
|
|
/* Old (lines 795-800):
|
|
int wants_exit = (prev_sign != 0 && curr_sign == 0);
|
|
int wants_reversal = (prev_sign != 0 && curr_sign != 0 && prev_sign != curr_sign);
|
|
int hold_violation = (hold_time > 0.0f
|
|
&& hold_time < (float)min_hold_bars
|
|
&& (wants_exit || wants_reversal)
|
|
&& bar_idx < total_bars - 1);
|
|
*/
|
|
|
|
/* New: use shared enforce_hold for the decision */
|
|
int is_last_bar = (bar_idx >= total_bars - 1) ? 1 : 0;
|
|
float enforced_position = enforce_hold(ps[0], position, hold_time, min_hold_bars, is_last_bar);
|
|
int hold_violation = (enforced_position != position);
|
|
|
|
/* Still need wants_exit and wants_reversal for downstream trade lifecycle */
|
|
int wants_exit = (prev_sign != 0 && curr_sign == 0);
|
|
int wants_reversal = (prev_sign != 0 && curr_sign != 0 && prev_sign != curr_sign);
|
|
```
|
|
|
|
- [ ] **Step 2: Keep the action aliasing fix as-is**
|
|
|
|
The block inside `if (hold_violation) { ... }` that fixes `action_idx` to match the held exposure STAYS. This is training-specific logic (action replay buffer aliasing) that the backtest doesn't need.
|
|
|
|
```c
|
|
if (hold_violation) {
|
|
position = enforced_position; /* use the shared result */
|
|
cash = ps[1];
|
|
is_flat = (fabsf(position) < 0.001f) ? 1.0f : 0.0f;
|
|
|
|
/* Action aliasing fix: override stored action to match held exposure */
|
|
float prev_exposure_frac = ps[0] / (max_position > 0.0f ? max_position : 1.0f);
|
|
int held_exposure = (int)roundf((prev_exposure_frac + 1.0f) * 0.5f * (float)(b0_size - 1));
|
|
/* ... rest of aliasing logic stays ... */
|
|
|
|
curr_sign = prev_sign;
|
|
wants_exit = 0;
|
|
wants_reversal = 0;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Verify build and test**
|
|
|
|
Run: `SQLX_OFFLINE=true cargo check -p ml`
|
|
|
|
Then run smoke test:
|
|
```bash
|
|
cargo clean -p ml --profile release-test && FOXHUNT_TEST_DATA=test_data/futures-baseline SQLX_OFFLINE=true cargo test -p ml --lib --profile release-test -- smoke_tests::training_stability::test_gpu_collector_auto_initializes --ignored --nocapture
|
|
```
|
|
|
|
Expected: Sharpe > 0, trades > 0, no SIGSEGV.
|
|
|
|
---
|
|
|
|
## Validation
|
|
|
|
After all 3 tasks, run the full suite:
|
|
|
|
```bash
|
|
# Unit tests
|
|
SQLX_OFFLINE=true cargo test -p ml --lib -- hyperopt::adapters::dqn::tests --nocapture
|
|
|
|
# Smoke test (training path)
|
|
FOXHUNT_TEST_DATA=test_data/futures-baseline SQLX_OFFLINE=true cargo test -p ml --lib --profile release-test -- smoke_tests::training_stability::test_gpu_collector_auto_initializes --ignored --nocapture
|
|
|
|
# Hyperopt integration (backtest path)
|
|
FOXHUNT_TEST_DATA=test_data/futures-baseline SQLX_OFFLINE=true cargo test -p ml --lib --profile release-test -- hyperopt::campaign::tests::test_local_hyperopt --ignored --nocapture
|
|
```
|
|
|
|
Success criteria:
|
|
- All unit tests pass
|
|
- Smoke test: Sharpe > 0, no SIGSEGV
|
|
- Hyperopt: 2 trials complete, max_dd < 30%, finite metrics
|
|
- `grep -c "execute_trade\|enforce_hold\|check_trailing_stop\|check_capital_floor\|decode_exposure_index\|compute_target_position\|decode_order_type\|compute_tx_cost\|apply_margin_cap\|update_hold_time" crates/ml/src/cuda_pipeline/experience_kernels.cu` shows ALL shared functions called
|
|
- Zero inline duplicates of shared logic remain
|