Commit Graph

34 Commits

Author SHA1 Message Date
jgrusewski
93c77b91b7 refactor(reward): delete 8 behavioral shaping terms in one sweep
Mass deletion of the "v7 gem" reward terms identified in the Phase 1
inventory as category errors — each one rewarded an outcome-adjacent
behavior instead of encoding the underlying physics, and each
empirically hurt validation metrics more than it helped training
stability.

Deleted from experience_kernels.cu and all plumbing (Rust configs,
launch args, hyperopt logs, TOML entries):

  * order_credit_weight        - reward redundant with compute_tx_cost
                                 (order_type_idx already differentiates
                                 fills by order type)
  * risk_efficiency_weight     - reward double-counted drawdown penalty
                                 asymmetrically (only on winners)
  * urgency_credit_weight      - reward was vol-normalized unrealized P&L,
                                 pure rename of core return
  * commitment_lambda          - triple-counted churn + tx_cost
  * w_dsr                      - kernel wrote DSR EMA but no longer added
                                 to reward (dead); removed the EMA
                                 bookkeeping too
  * dsr_eta                    - kernel arg for the deleted DSR EMA
  * position_entropy_weight    - rewarded action-bucket diversity
                                 regardless of outcome; histogram buffer
                                 + zero-init removed too
  * exit_timing_weight         - already inactive (used raw_next future
                                 price, comment-deleted earlier)
  * ofi_reward_weight          - dead plumbing; OFI already passed as
                                 feature through state[OFI_START..]
  * opportunity_cost_scale     - penalized flat when Q-gap wide;
                                 redundant with Q-values themselves

Kernel arg count: experience_env_step_batch shrank from ~55 to ~45 args.
Rust-side config surface reduced correspondingly.

Results on E1 smoke test (20-epoch):
  BEFORE any Phase 2 work:
    Val Sharpe -120 to -150, MaxDD 10-15%, Sharpe_raw -0.39
  AFTER reward_noise + Kelly (both envs) + urgency + this sweep:
    Val Sharpe      -17 to -22       (7× better)
    Val MaxDD       0.27%            (40× better)
    Val Sharpe_raw  ~-0.09           (4× better)
    Training Sharpe_raw  ~0          (stabilized from ±20 swings)
    Final q_gap     0.1712           (highest yet, collapse mechanism fine)

The extreme train-Sharpe swings (+17 one epoch, -13 next) were not
learning dynamics — they were shaping-term noise. Core reward (P&L +
drawdown + churn + holding + tx_cost + Kelly physics cap) gives training
metrics that actually reflect what the model does.

Inventory doc (docs/superpowers/specs/2026-04-21-phase1-reward-inventory.md)
extended with a "better-form taxonomy" section: every deleted gem has
a correct layer it belongs to (physics, feature, diagnostic, gradient-
level regularization — not reward). Kelly cap and Q-target smoothing
are already relocated; others are scheduled per the taxonomy's P1/P2/P3
priority list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:40:36 +02:00
jgrusewski
71ae90768d refactor(reward): Kelly sizing from behavioral reward to health-coupled physics cap
Phase 2 second relocation: the kelly_sizing_weight reward penalty
(experience_kernels.cu:1727-1746 — penalized deviation from Kelly-optimal
sizing) is deleted. Kelly is now a physics constraint in trade_physics.cuh:
the environment refuses to let the agent over-lever, not the reward
scoring the agent for matching a formula.

New helper in trade_physics.cuh (shared device function, reusable by
the forthcoming unified env kernel):

  kelly_position_cap(win_count, loss_count, sum_wins, sum_losses,
                     max_position, safety_multiplier)

Applied in experience_kernels.cu between margin cap and execute_trade,
with health-coupled safety multiplier:

  safety = 0.5 + 0.5 × health
  - health=1 (healthy): full Kelly — trust the learned policy
  - health=0 (collapsing): half Kelly — constrain when decisions less
    reliable

Cold-start warmup (critical — otherwise balanced priors yield kelly_f=0
until real trades accumulate, starving Q-learning):

  maturity = min(1.0, total_trades / 10)
  effective_kelly = maturity × kelly_f + (1 - maturity) × 0.5

Early on (0 trades): cap dominated by 50% floor.
As real trades accumulate (10+): pure data-driven Kelly.

Validation env (backtest_env_kernel.cu) does NOT yet get the Kelly cap —
that requires extending its portfolio state or adding a separate
kelly_stats buffer, which naturally belongs in the Phase 3 unified env
kernel refactor. The current asymmetry is a KNOWN temporary — training
is constrained, validation is not — and will be resolved when both
kernels share the same env_step() device function.

Also completes removal of kelly_sizing_weight from all plumbing:
- experience_kernels.cu: kernel arg deleted
- gpu_experience_collector.rs: launch arg, config field, default
- training_loop.rs: hyperparam propagation
- config.rs: field, default, intensity clamp (with tombstone)
- hyperopt/adapters/dqn.rs: log reference
- config/training/*.toml (6 entries across 4 files): orphan configs
  (none were wired to a profile parser field)

Verification:
- cargo check -p ml --lib clean
- E1 smoke test passes: final epoch q_gap=0.1109, health=0.51 (warmup
  floor of 0.5 gives early exploration enough room; floor of 0.25
  was too tight and failed at q_gap=0.0496)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:07:07 +02:00
jgrusewski
4bbf6180d1 refactor(reward): relocate reward_noise_scale to health-coupled Q-target smoothing
Phase 2 kick-off from the env-unification design. First relocation: the
reward_noise_scale field that perturbed training rewards is deleted, and
its regularization effect moves to the correct layer — Q-target label
smoothing in c51_loss_kernel.cu — now health-coupled rather than fixed.

Before:
- reward += pseudo_noise × max(|reward| × 0.05, 0.01)   (in env reward path)
- Q-target label smoothing = fixed LABEL_SMOOTHING_EPS = 0.01

After:
- Reward untouched by noise. Core reward = actual outcome + aligned penalties.
- Q-target label smoothing eps_eff = 0.02 × (1 − health) read from ISV[12]
  - health=1 (healthy): eps_eff=0, sharp targets preserved
  - health=0.5: eps_eff=0.01, matches old fixed behavior at mid-health
  - health=0 (collapsing): eps_eff=0.02, maximum regularization prevents
    overcommitment to the collapsed distribution

Why health-coupled:
Same insight as the distillation SAXPY fix — every fixed kernel scalar is
a temporal-coupling candidate when we have the ISV pinned buffer available.
Regularization strength should scale INVERSELY with network health: it's
most needed exactly when things are falling apart.

Files touched:
- c51_loss_kernel.cu: LABEL_SMOOTHING_EPS const replaced with
  LABEL_SMOOTHING_BASE + in-kernel health read from isv_signals[12]
- experience_kernels.cu: deleted reward noise block + kernel arg
- gpu_experience_collector.rs: dropped launch .arg + config field + default
- training_loop.rs: dropped hyperparam propagation
- config.rs: deleted field + intensity clamp + default (with tombstone)
- hyperopt/adapters/dqn.rs: dropped log reference
- config/training/*.toml (4 files): dropped orphan reward_noise_scale
  entries (none were being parsed — the profile parser had no field)

Verification:
- `cargo check -p ml --lib` clean
- E1 smoke test passes: final q_gap=0.1190, health=0.52 (health-coupled
  smoothing at ~mid-health matches old fixed behavior, collapse-prevention
  mechanism intact)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:43:24 +02:00
jgrusewski
108bb63fce feat: cost-driven hold timing — replace min_hold_bars with learned cost signals
Removed: enforce_hold(), min_hold_bars from config/kernels/backtest.
Added: holding_cost_rate (inventory penalty), churn_threshold_bars +
churn_penalty_scale (graduated flip penalty) to reward in
experience_env_step and backtest kernels.

The model learns optimal hold timing from cost signals:
- Per-trade tx cost prevents churning (existing)
- Inventory penalty makes large positions expensive to hold
- Churn penalty graduates cost for rapid flips
- Temporal attention learns when holding cost > expected profit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 01:04:11 +02:00
jgrusewski
eb7139c436 feat: adaptive C51 v_range — Q-values 0.008→0.525 (65× improvement)
Fundamental fix: C51 atom spacing (dz=0.6) was larger than the Q-value
range (0.02), making the distributional loss unable to resolve rewards.
The network was blind to its own learning signal.

Adaptive v_range via device buffer (same pattern as tau_buf):
- v_range_buf[2] on GPU, all C51/MSE/CQL/expected_q kernels read via
  pointer (graph captures stable address, reads value at replay time)
- Cold start: v_range=±1.0 → dz=2/51=0.039 → 25× atom resolution
- Monotonic expansion based on Q-stats every 50 training steps
- Never shrinks (avoids invalidating Bellman targets in replay buffer)

Kernel changes (5 files):
- c51_loss_kernel.cu, mse_loss_kernel.cu, mse_grad_kernel.cu,
  cql_grad_kernel.cu, compute_expected_q: scalar v_min/v_max → pointer

Also fixed:
- PopArt warmup 100→1 (start normalizing from step 1)
- PopArt variance floor 0.0001→1e-8 (allow sigma < 0.01)
- Backtest evaluator: allocate own v_range_buf for compute_expected_q
- num_atoms 51→52 in all TOMLs (TF32 4-element alignment)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 07:58:02 +02:00
jgrusewski
a0aab3baa5 feat: add c51_alpha_max to cap MSE→C51 blend and prevent gradient starvation
H100 baseline (20 epochs) showed gradient collapse at epoch 10: C51
cross-entropy converges its distributional fit before the policy converges,
leaving zero gradient signal. The collapse happened 5 epochs after C51
reached alpha=1.0 (pure C51, zero MSE).

Fix: cap the C51 alpha ramp at c51_alpha_max (default 0.5) so MSE always
contributes (1 - alpha_max) of the primary gradient. MSE loss measures
Q-error directly and only goes to zero when Q-values are correct, not
just when the distribution shape is right.

- c51_alpha_max added to DQNHyperparameters (default 0.5)
- Added to PSO search space as 15th dimension (range [0.3, 0.9])
- Added to TOML profiles: smoketest, production, hyperopt
- Training loop caps alpha ramp at alpha_max instead of 1.0
- All 907 ml tests + 300 ml-core tests + 6 smoke tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:42:41 +02:00
jgrusewski
30f46c04c1 fix(critical): mean-reduce gradients + fix ExposureLevel 4-branch mismatch
Three root causes found and fixed:

1. SUM-reduced gradients without 1/N: all CUDA loss gradient kernels
   (C51, MSE, IQN backward, CQL) accumulated per-sample gradients as
   raw SUM. At batch=16384 (H100) the raw norm was 282x larger than
   batch=58, causing gradient clipping to destroy signal-to-noise ratio
   and collapse training at epoch 2-3. Now all kernels multiply by
   1/batch_size, making gradient scale batch-invariant.

2. ExposureLevel::target_exposure() used a flat 9-level scale that did
   not match the 4-branch dir*mag encoding. The Rust backtest evaluator
   computed wrong position sizes (e.g. 4x oversize for Short+Small).
   Now uses dir x mag formula. Also fixed is_buy/is_sell/is_hold and
   from_trading_action for 4-branch semantics.

3. Rust epsilon-greedy only explored 5/9 exposure combos (0..5 instead
   of dir*3+mag), ignored the magnitude branch on greedy, and used wrong
   indices for order/urgency (get(1)/get(2) instead of get(2)/get(3)).

LR recalibrated: old gradient_clip_norm was accidentally a batch-size-
dependent LR reducer (~60x at batch=58, ~16000x at batch=16384). With
mean-reduced gradients the clip rarely fires, so LR is now the sole
training speed control. Smoketest 1e-4 -> 2e-6, production 1e-4 -> 1e-5,
hyperopt range [1e-5,3e-4] -> [1e-7,1e-4].

Diagnostics: FOXHUNT_GRAD_DIAG=1 enables per-stage gradient norm logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:18:36 +02:00
jgrusewski
0d1e01a0c7 fix: all DQN configs — IBKR costs (0.18 bps) + min_hold_bars=10 for volume bars
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:05:59 +02:00
jgrusewski
4889b6e968 feat(4branch): config foundation — b0=3 direction, b1=3 magnitude, b2=3 order, b3=3 urgency
Delete exposure_aux_weight, exposure_aux_warmup_epochs.
b0_size 9→3 (direction branch). b3_size=3 added (urgency).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:18:50 +02:00
jgrusewski
af60ddf837 fix: 10x stronger aux optimizer (LR 0.001→0.01, weight 0.01→0.1)
With separate optimizer, no gradient competition — safe to increase.
Smoke test shows within-group differentiation starting (S100≠S25≠L50).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:13:58 +02:00
jgrusewski
58ff8db406 fix: aux GEMM gradient explosion — scale 0.5→0.01 + clip after backward
The exposure aux backward GEMM amplifies gradients through the
hidden activation matrix (d_logits × h_b0), producing grad_norm=5000+.
With max_grad_norm=10, this throttles ALL gradients by 500x (lr/500).

Fix: reduce aux_weight 0.5→0.01 (50x) + clip_grad_buf_inplace after
aux GEMM to cap combined gradient at max_grad_norm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:03:54 +02:00
jgrusewski
891f4d8715 perf: validation subsampling (4x faster) + batch_size 16384 (halves training steps) 2026-04-08 01:55:10 +02:00
jgrusewski
1a93b71c69 feat(v8): wire 5 dead CUDA kernels + config fixes
Kernels now wired with Rust load + launch methods:
- popart_normalize_kernel: running mean/var + in-place normalization
- exposure_pretrain_step: supervised direction pre-training with backward GEMM
- td_lambda_kernel: exponentially-weighted lambda-returns
- compute_difficulty_scores: per-bar curriculum difficulty
- hindsight_relabel_kernel: optimal exit relabeling

Config fixes:
- batch_size hyperopt range: [64, 512] → [512, 8192] (H100 capacity)
- micro_reward_scale: 0.001 → 0.01 (10x stronger bootstrap)
- micro_reward hyperopt range: [0, 0.005] → [0, 0.05]

900 tests pass, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:26:09 +02:00
jgrusewski
d3f7423691 config(v8): add micro_reward, td_lambda, hindsight params to all TOML profiles 2026-04-07 23:42:36 +02:00
jgrusewski
d39786df72 feat(reward-v7.1): wire exposure aux gradient with backward GEMM + schedule fixes + dead code cleanup
- Exposure aux kernel -> f32 scratch -> bf16 cast -> backward_fc_layer GEMM
- Weight + bias gradients accumulated into grad_buf (pure GPU, zero CPU sync)
- CEA warmup: linear decay 1.0->base over 25% of epochs
- OFI epoch gate: disabled for first 5 epochs
- Exposure aux decay: base -> 10% over warmup_epochs
- Removed dead v6 fields: loss_aversion, beta_penalty, regret_blend, trade_clustering_penalty
  from ExperienceCollectorConfig (kernel no longer reads them)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:21:51 +02:00
jgrusewski
ad083e1c20 feat(reward-v7): implement all gems & pearls
Layers 6-9 + label smoothing + adaptive CEA warmup:
- Urgency branch attribution (fill price improvement)
- Exit timing quality (market reversal after exit)
- OFI-weighted reward confidence (amplify informed trades)
- Kelly-optimal position sizing signal
- Reward label smoothing (deterministic jitter)
- Adaptive CEA warmup (1.0 for first 3 epochs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:08:09 +02:00
jgrusewski
f05a4c5d44 config: enable position entropy (0.02) + add hyperopt search range
Anti-Flat-collapse: rewards exposure diversity via per-episode
position visit histogram entropy bonus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:02:47 +02:00
jgrusewski
704e609809 config(reward-v7): add cea_weight, order_credit_weight, risk_efficiency_weight to all TOML profiles 2026-04-07 20:27:59 +02:00
jgrusewski
bacaf2765a feat: leverage-based position cap (replaces hardcoded max_position)
max_position_absolute is now computed from max_leverage:
  max_position = floor(capital * max_leverage / (price * multiplier))

- Added max_leverage config field (default: 5.0)
- compute_max_position() derives position from leverage + median price
- Hyperopt risk_intensity scales max_leverage (not position directly)
- Updated all TOML configs: dqn-production, dqn-smoketest, dqn-localdev
- Hyperopt search space: max_leverage = [2.0, 10.0] (replaces [1.0, 4.0] contracts)
- GpuBacktestConfig wired with max_leverage for consistent eval

With $35K capital, ES at $5K, multiplier=50:
  5× leverage → floor(35000*5/250000) = 0.7 → 1 contract (safe)
  Old default 2.0 contracts → 14× leverage (dangerous)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:24:20 +02:00
jgrusewski
ae99567362 feat(generalization): CQL alpha 0.1→1.0, gradient budget 15%→25%
CQL was effectively disabled (0.1 alpha × 0.15 budget = 1.5% of gradient).
Now: alpha=1.0 × 0.25 budget = 25% of gradient enforces conservatism.
C51 reduced from 70% to 60% to accommodate.

CQL penalizes Q-values for actions not in the data — directly prevents
the model from being "confident but wrong" on OOS state-action pairs.

Hyperopt search range updated: [0.0, 1.0] → [0.5, 5.0].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:40:30 +02:00
jgrusewski
a400288ae0 feat: comprehensive TOML profiles + config consistency test
Task 7: Update all three DQN TOML profiles with every configurable parameter.
- dqn-smoketest: add [distributional], [advanced], [exploration] (noisy_sigma,
  entropy_coefficient, count_bonus), [risk] (max_position, loss_aversion),
  fix learning_rate 0.0003 -> 0.00003, add reward_scale + gamma
- dqn-production: add reward_scale, exploration params (noisy/entropy/count_bonus),
  advanced params (n_steps/tau/c51_warmup/her/iqn_lambda/spectral_norm),
  remove hardcoded v_min/v_max (now computed), remove duplicate tau from [training]
- dqn-hyperopt: add search space bounds for spectral_norm_sigma_max,
  c51_warmup_epochs, her_ratio

Profile system additions (training_profile.rs):
- TrainingSection: add reward_scale (recomputes v_min/v_max on apply)
- ExplorationSection: add noisy_sigma_init, entropy_coefficient,
  count_bonus_coefficient, q_gap_threshold
- AdvancedSection: add n_steps, tau, c51_warmup_epochs, her_ratio,
  iqn_lambda, spectral_norm_sigma_max, gradient_clip_norm
- RiskSection: add max_position (alias for max_position_absolute), loss_aversion
- RewardSection: add reward_scale
- SearchSpaceSection: add spectral_norm_sigma_max, c51_warmup_epochs, her_ratio
- apply_to: gamma change now recomputes v_min/v_max automatically

Task 8: Config consistency integration tests (5 tests):
- test_config_consistency_across_structs: v_range computed not hardcoded,
  fill simulation bounds, q_clip symmetry
- test_toml_profile_applies_all_fields: smoketest profile applies every
  new section field correctly
- test_production_profile_applies_all_sections: production profile end-to-end
- test_hyperopt_search_space_has_new_bounds: new search space fields parse
- test_reward_scale_recomputes_v_range: gamma override triggers v_range recomputation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:27:45 +01:00
jgrusewski
e8f54c37c1 feat: expose experience collector params in TOML training profiles
Add 7 new fields to DQNHyperparameters (fill_ioc_fill_prob,
fill_limit_fill_min, fill_limit_fill_max, fill_spread_cost_frac,
fill_spread_capture_frac, q_clip_min, q_clip_max) and wire them
through training_profile.rs into ExperienceCollectorConfig construction
in training_loop.rs. Previously these 7 values were hardcoded at the
construction site; now they flow from TOML [experience.fill_simulation]
and [risk] sections. Default values match the prior hardcoded constants
so existing behavior is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:05:01 +01:00
jgrusewski
04d8802c94 refactor: remove 8 always-on use_ booleans — features are mandatory
Remove use_double_dqn, use_dueling, use_per, use_branching,
use_distributional, use_noisy_nets, use_huber_loss, and use_cql
from DQNConfig, DQNHyperparameters, and DqnParams structs.

These features are always enabled (Rainbow DQN standard). The boolean
flags were dead code — every constructor set them to true, and the
only code paths that set them to false were in tests that disabled
features for simplicity. With the fields removed, the features are
unconditionally active, eliminating ~490 lines of dead configuration.

Key changes:
- Struct field declarations removed from 3 core config structs
- Conditional branches (if use_X { ... } else { ... }) simplified:
  dueling/branching/PER network creation is now unconditional
- Checkpoint metadata hardcodes "true" for backward compatibility
- Hyperopt search space index 11 (use_branching) fixed at 1.0
- TOML/YAML config files cleaned of removed fields
- Tests that toggled these flags updated or rewritten

45 files changed, -487 net lines. Zero new test failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:45:54 +01:00
jgrusewski
66dfcf599f feat: 9-exposure branch_sizes + num_actions defaults across workspace
- gpu_experience_collector: branch_sizes [5,3,3]→[9,3,3]
- gpu_iqn_head: branch_0_size 5→9
- All DQN configs: num_actions 5→9
- Production TOML: branch_0_size=9
- Removed use_branching from TOMLs (always enabled)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:24:55 +01:00
jgrusewski
e8a3104159 feat: DSR warm-up, num_atoms=101, clear stale cache on H100
- DSR warm-up: skip first 50 steps when EMA has insufficient history
- Phase Fast num_atoms: 51 → 101 (H100 can afford finer resolution,
  1.19 per atom vs 2.35 — critical for distinguishing Q-values)
- Argo template: clear stale feature cache before hyperopt (ensures
  fresh computation with VPIN/trades enrichment)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:32:48 +01:00
jgrusewski
0b37ff77b0 fix: reward v2 + dynamic C51 support — root cause of Q-value collapse
ROOT CAUSE: 5 interlocking bugs made learning impossible:
1. DSR denominator floor 1e-12 produced values in millions → drowned all signal
2. Global [-1,+1] clamp destroyed Bellman equation signal (can't distinguish
   catastrophic loss from mild loss)
3. v_range=20 exactly equals V_max for gamma=0.95 → Bellman target pins at
   ceiling → Q-values saturate → Q-gap collapses to 0.0000
4. num_atoms=11 over 40-unit range = 4.0 per atom (C51 paper min is 51)
5. 6/7 reward components were penalties → mean_reward=-0.311 regardless of action

FIXES:
- DSR denominator floor: 1e-12 → 0.01 (prevents million-scale spikes)
- Each component individually clamped BEFORE weighting (DSR to [-1,+1],
  z-score to [-3,+3], drawdown to [0,1], time decay to [0,0.3])
- Removed global [-1,+1] clamp (no longer needed with bounded components)
- profit_take_bonus: 0.1 → 0.01 (was 100x too large, caused reward hacking)
- Removed confidence scaling (positive feedback loop destabilized learning)
- Removed regime scaling (non-stationary reward confused the model)
- Dynamic v_range from gamma: v_range = 2.5/(1-gamma)*1.2 (always covers Q range)
- num_atoms minimum: 11 → 51 (C51 paper standard)
- gamma default: 0.99 → 0.95

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 00:39:39 +01:00
jgrusewski
95d9fdb034 config: set q_gap_threshold=0.1 as default — force trade selectivity
Q-gap was 0.0 (disabled) meaning the model traded on every bar regardless
of conviction. With 0.1, the model must have Q(best) - Q(flat) > 0.1
before entering a position. Local test showed 21% fewer trades.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:45:41 +01:00
jgrusewski
5cb400a73b feat: Q-gap conviction filter + remove dead use_branching kernel arg
Add q_gap_threshold to action selection kernel: when greedy Q(best) - Q(flat)
< threshold, default to flat. Teaches model to trade only with conviction.
39D search space (was 38D). Default 0.0 (disabled), hyperopt range [0.0, 0.5].

Remove use_branching parameter from experience_action_select — GPU pipeline
always uses branching DQN. Flat mode was dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:05:46 +01:00
jgrusewski
81a7ce2d43 config: tighten dd_threshold [0.005, 0.03] for HFT — 1% default
6% drawdown tolerance too generous for HFT. Tightened search range
to 0.5%-3%, default 1%. Forces aggressive loss cutting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:06:13 +01:00
jgrusewski
af940671bc feat: reward config pipeline — DQNHyperparameters + TOML profiles
Add 7 composite reward fields to DQNHyperparameters: w_dsr, w_pnl,
w_dd, w_idle, dd_threshold, loss_aversion, time_decay_rate.

Add RewardSection to training_profile.rs with Option<f64> fields and
apply_to() mapping. Add [reward] section to all 3 DQN TOML profiles
(production, smoketest, hyperopt) with identical defaults.

Remove hold_reward from ExperienceSection (replaced by w_idle).
Add 7 reward search bounds to SearchSpaceSection and bound() match.
Add 7 reward phase_fast defaults to PhaseFastSection.

hold_penalty kept as deprecated field for hyperopt adapter compat
(Task 4 will clean it up).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:50:23 +01:00
jgrusewski
43998a330a feat: two-phase hyperopt + backtest evaluator VRAM leak fix
Two-phase hyperopt splits 31D PSO search into sequential phases:
- Phase 1 (--phase fast, default): fix architecture to small network
  (hidden_dim=128, num_atoms=11), search learning dynamics (~15D).
- Phase 2 (--phase full): fix dynamics from Phase 1 JSON, search
  architecture (~5D). Halves dimensionality per phase → better convergence.
- Phase 1 output includes best_continuous_vector for Phase 2 consumption.

GpuBacktestEvaluator Drop impl: sync forked stream, destroy CUDA graph
and cuBLAS handles before CudaSlice buffers drop. Fixes 261MB/trial
VRAM leak on H100 hyperopt.

ml-core clippy fixes: hex literal, remove dead check_err drain,
unnecessary safety comment, unused OnceLock import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:32:37 +01:00
jgrusewski
e97c30b50a perf: cap hyperopt hidden_dim=256, num_atoms=51 — 4x faster trials
Production defaults are sufficient for hyperopt exploration. Larger networks
can be tested in a separate phase with the best hyperparams found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:00:26 +01:00
jgrusewski
3c8e177932 feat: HyperoptProfile with TOML search space bounds
Add SearchSpaceSection, PsoSection, HyperoptProfile structs to
training_profile.rs. All 31 PSO search bounds now configurable in
config/training/dqn-hyperopt.toml — no code changes needed to
adjust search ranges.

HyperoptProfile::bound("field", default) returns the TOML value
or falls back to the hardcoded default. Adapter wiring is next step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:33:55 +01:00
jgrusewski
a75a98bd0d feat: TOML training profile system — config-driven hyperparameters
Training Profile Loader:
- 3-tier resolution: $FOXHUNT_TRAINING_PROFILE > filesystem > embedded defaults
- DqnTrainingProfile with 10 sections, all Option<T> for sparse profiles
- apply_to() applies only Some fields, preserving struct defaults
- 11 unit tests, all passing

TOML Profiles (config/training/):
- dqn-production.toml: full Rainbow DQN (40+ params)
- dqn-smoketest.toml: CI fast path (sparse, 8 overrides)
- dqn-hyperopt.toml: PSO search space ranges + fixed flags
- ppo-production.toml, ppo-smoketest.toml
- supervised-production.toml, supervised-smoketest.toml
- walk-forward.toml: window sizes

CLI Integration:
- train_baseline_rl: --training-profile (default: dqn-production)
- train_baseline_supervised: --training-profile (default: supervised-production)
- Merge priority: CLI args > TOML profile > GPU profile > struct defaults

Smoke Tests:
- smoke_params() now loads dqn-smoketest.toml instead of hardcoding
- Production features set as manual overrides (testing flags, not config)

Infrastructure:
- K8s job-template.yaml: TRAINING_PROFILE env var + --training-profile arg
- Delete old config/ml/training.toml (replaced, zero callers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:35:41 +01:00