# Config Unification Plan — Single Source of Truth

> **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:** Eliminate all hardcoded config values and `use_` boolean bloat. Every training parameter flows from TOML → DQNHyperparameters → downstream configs. No Default impl should diverge from the TOML-driven path.

**Architecture:** Single config flow: `TOML file → DqnTrainingProfile::load() → DQNHyperparameters → GpuDqnTrainConfig + GpuExperienceCollectorConfig + DQNConfig`. Default impls match conservative() exactly. All `use_` booleans that are always true are removed — the features are always on.

**Tech Stack:** Rust config structs, TOML deserialization, training_profile.rs.

---

## Problem Statement

Three config structs have independent `Default` impls with conflicting values:

| Parameter | DQNConfig | GpuDqnTrainConfig | ExperienceCollectorConfig |
|-----------|-----------|-------------------|--------------------------|
| state_dim | 48 | 72 | — |
| v_min/v_max | -25/25 | -2/2 | -2/2 |
| learning_rate | 1e-4 | 3e-4 | — |
| weight_decay | 1e-4 | 1e-5 | — |
| batch_size | 64 | 256 | — |
| num_atoms | 51 | 51 | 1 |
| use_noisy_nets | true | — | false |
| use_distributional | true | — | false |

Plus 26 ExperienceCollectorConfig parameters are hardcoded with no TOML exposure.

## Design Principles

1. **TOML is the source of truth** — every tunable value has a TOML field
2. **conservative() reads TOML** — no hardcoded literals in the function
3. **Default impls match conservative()** — tests behave like production
4. **No `use_` booleans for always-on features** — if it's always true, remove the flag
5. **One config flow** — TOML → DQNHyperparameters → everything else

---

## Task 1: Clean up `use_` booleans (config.rs + dqn.rs)

**Files:**
- Modify: `crates/ml/src/trainers/dqn/config.rs` — DQNHyperparameters
- Modify: `crates/ml-dqn/src/dqn.rs` — DQNConfig

**Always-on features (remove the `use_` field, hardwire to true internally):**
- `use_double_dqn` — always true since Wave 6.4
- `use_dueling` — always true (Rainbow DQN standard)
- `use_per` — always true (memory says "PER always enabled, non-PER paths are dead code")
- `use_branching` — always true (9×3×3 action space)
- `use_distributional` — always true (C51 is the primary loss)
- `use_noisy_nets` — always true (replaces epsilon-greedy)
- `use_huber_loss` — always true (more robust than MSE)
- `use_cql` — always true (activated this session)

For each:
- [ ] **Step 1:** Remove the field from DQNHyperparameters
- [ ] **Step 2:** Remove the field from DQNConfig
- [ ] **Step 3:** In constructors that read these fields, replace `if self.use_X { ... }` with just `{ ... }`
- [ ] **Step 4:** In training_profile.rs, remove the TOML field if it exists (or ignore it)
- [ ] **Step 5:** Search for ALL references with `grep -rn "use_double_dqn\|use_dueling\|use_per\b\|use_branching\|use_distributional\|use_noisy_nets\|use_huber_loss\|use_cql\b"` and update each site
- [ ] **Step 6:** Build + test

**Keep as legitimate config flags:**
- `use_cvar_action_selection` — experimental, toggleable
- `enable_kelly_sizing` — can be disabled for ablation
- `enable_circuit_breaker` — safety feature, must be toggleable
- `enable_action_masking` — experimental
- `enable_regime_qnetwork` — experimental
- `enable_early_stopping` — must be toggleable

- [ ] **Step 7:** Commit: "refactor: remove always-on use_ booleans — 8 features are mandatory"

---

## Task 2: Unify Default impls to match conservative()

**Files:**
- Modify: `crates/ml-dqn/src/dqn.rs` — DQNConfig::default()
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` — GpuDqnTrainConfig::default()
- Modify: `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` — GpuExperienceCollectorConfig default

Every `Default` impl must produce values identical to what `conservative()` → constructor flow would produce. This ensures tests behave like production.

- [ ] **Step 1:** In DQNConfig::default():
  - `v_min: -50.0, v_max: 50.0` (match conservative/reward v6)
  - `learning_rate: 3e-5` (match conservative)
  - `weight_decay: 1e-4` (match conservative)
  - `batch_size: 64` (match smoketest, conservative)
  - `num_atoms: 51`
  - `state_dim: 48` (match actual feature dim with padding)
  - `num_actions: 9` (match 9-action exposure space)

- [ ] **Step 2:** In GpuDqnTrainConfig::default():
  - `state_dim: 72` → `state_dim: 48` (or remove default entirely — always set by constructor)
  - `v_min: -50.0, v_max: 50.0`
  - `lr: 3e-5` → match conservative
  - `weight_decay: 1e-4`
  - `batch_size: 64`
  - `branch_0_size: 9` (correct — 9 exposure levels)

- [ ] **Step 3:** In GpuExperienceCollectorConfig default:
  - `use_noisy_nets: true` (was false — mismatch)
  - `use_distributional: true` (was false — mismatch)
  - `num_atoms: 51` (was 1 — mismatch)
  - `v_min: -50.0, v_max: 50.0`
  - `q_clip_min: -200.0, q_clip_max: 200.0` (match reward v6 scale)

- [ ] **Step 4:** Build + all tests pass
- [ ] **Step 5:** Commit: "fix: unify Default impls to match conservative() — no more config mismatches"

---

## Task 3: Expose ExperienceCollectorConfig params in TOML

**Files:**
- Modify: `crates/ml/src/training_profile.rs` — add ExperienceSection
- Modify: `config/training/dqn-smoketest.toml` — add [experience] section
- Create: `config/training/dqn-production.toml` — add [experience] section (if not exists)
- Modify: `crates/ml/src/trainers/dqn/trainer/training_loop.rs` — read from profile

26 parameters need TOML exposure. Group them:

```toml
[experience]
gpu_n_episodes = 32              # episodes per experience collection round
gpu_timesteps_per_episode = 100  # bars per episode
initial_capital = 100000.0       # starting equity

[experience.fill_simulation]
ioc_fill_prob = 0.85
limit_fill_min = 0.30
limit_fill_max = 0.80
spread_cost_frac = 0.50
spread_capture_frac = 0.50

[experience.barriers]
profit_mult = 1.02
loss_mult = 0.98
max_bars = 500.0
scale = 0.5

[risk]
max_position = 2.0
loss_aversion = 1.5
q_clip_min = -200.0
q_clip_max = 200.0
```

- [ ] **Step 1:** Add `ExperienceSection`, `FillSimulationSection`, `BarrierSection`, `RiskSection` to training_profile.rs
- [ ] **Step 2:** Implement `apply_to()` for each section
- [ ] **Step 3:** Update dqn-smoketest.toml with [experience] section
- [ ] **Step 4:** In training_loop.rs, pass TOML values to ExperienceCollectorConfig instead of hardcoded defaults
- [ ] **Step 5:** Build + test
- [ ] **Step 6:** Commit: "feat: expose 26 experience collector params in TOML training profiles"

---

## Task 4: Wire v_range computation from reward scale

**Files:**
- Modify: `crates/ml/src/trainers/dqn/config.rs` — compute v_min/v_max from reward_scale + gamma
- Modify: `crates/ml/src/hyperopt/adapters/dqn.rs` — same computation in from_continuous()

v_min/v_max should NEVER be hardcoded. They should be computed:
```rust
let reward_scale = 10.0; // reward v6 REWARD_SCALE
let v_range = (reward_scale / (1.0 - gamma) * 1.2).clamp(20.0, 300.0);
let v_min = -v_range;
let v_max = v_range;
```

- [ ] **Step 1:** Add `reward_scale: f64` to DQNHyperparameters (default 10.0)
- [ ] **Step 2:** In conservative(), compute v_min/v_max from reward_scale + gamma
- [ ] **Step 3:** Remove hardcoded v_min/v_max from conservative()
- [ ] **Step 4:** In hyperopt adapter from_continuous(), use same formula
- [ ] **Step 5:** Add reward_scale to TOML and hyperopt search space
- [ ] **Step 6:** Build + test
- [ ] **Step 7:** Commit: "feat: v_range computed from reward_scale + gamma — no more hardcoded C51 bounds"

---

## Task 5: Consolidate DQNConfig → derive from DQNHyperparameters

**Files:**
- Modify: `crates/ml-dqn/src/dqn.rs` — DQNConfig construction
- Modify: `crates/ml/src/trainers/dqn/trainer/constructor.rs` — DQNConfig from hyperparams

DQNConfig should NOT have its own defaults. It should be constructed FROM DQNHyperparameters:

```rust
impl From<&DQNHyperparameters> for DQNConfig {
    fn from(hp: &DQNHyperparameters) -> Self {
        Self {
            state_dim: hp.state_dim(),
            num_actions: hp.num_actions(),
            learning_rate: hp.learning_rate,
            gamma: hp.gamma,
            batch_size: hp.batch_size,
            v_min: hp.v_min(),
            v_max: hp.v_max(),
            num_atoms: hp.num_atoms,
            // ... all fields derived
        }
    }
}
```

- [ ] **Step 1:** Add `From<&DQNHyperparameters> for DQNConfig`
- [ ] **Step 2:** Use this conversion in constructor.rs instead of field-by-field copy
- [ ] **Step 3:** Remove DQNConfig::default() (or make it delegate to From<conservative()>)
- [ ] **Step 4:** Build + test
- [ ] **Step 5:** Commit: "refactor: DQNConfig derived from DQNHyperparameters — single source of truth"

---

## Task 6: Consolidate GpuDqnTrainConfig → derive from DQNHyperparameters

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
- Modify: `crates/ml/src/trainers/dqn/fused_training.rs`

Same pattern: GpuDqnTrainConfig should be constructed FROM DQNHyperparameters, not hardcoded:

```rust
impl GpuDqnTrainConfig {
    pub fn from_hyperparams(hp: &DQNHyperparameters, dqn_config: &DQNConfig) -> Self {
        Self {
            state_dim: dqn_config.state_dim,
            num_atoms: hp.num_atoms,
            v_min: hp.v_min() as f32,
            v_max: hp.v_max() as f32,
            lr: hp.learning_rate as f32,
            weight_decay: hp.weight_decay as f32,
            // ... all derived
        }
    }
}
```

- [ ] **Step 1:** Add `from_hyperparams()` constructor
- [ ] **Step 2:** Use it in fused_training.rs instead of field-by-field construction
- [ ] **Step 3:** Remove or deprecate GpuDqnTrainConfig::default()
- [ ] **Step 4:** Build + test
- [ ] **Step 5:** Commit: "refactor: GpuDqnTrainConfig derived from hyperparams — no independent defaults"

---

## Task 7: Update TOML files with all parameters

**Files:**
- Modify: `config/training/dqn-smoketest.toml`
- Modify: `config/training/dqn-hyperopt.toml`
- Create or modify: `config/training/dqn-production.toml`

Each TOML file should have ALL sections:
```toml
[training]
epochs = 50
batch_size = 64
learning_rate = 0.00003
gamma = 0.95
weight_decay = 0.0001
reward_scale = 10.0

[distributional]
num_atoms = 51
# v_min/v_max computed from reward_scale + gamma

[exploration]
epsilon_start = 1.0
epsilon_end = 0.02
epsilon_decay = 0.95
noisy_sigma_init = 0.5
entropy_coefficient = 0.001
count_bonus_coefficient = 0.1
q_gap_threshold = 0.05

[experience]
gpu_n_episodes = 32
gpu_timesteps_per_episode = 100
initial_capital = 100000.0

[risk]
max_position = 2.0
loss_aversion = 1.5
kelly_fractional = 0.5
kelly_max_fraction = 0.25

[advanced]
n_steps = 3
tau = 0.005
c51_warmup_epochs = 5
her_ratio = 0.2
cql_alpha = 0.1
iqn_lambda = 0.25
spectral_norm_sigma_max = 3.0
gradient_clip_norm = 10.0

[early_stopping]
enabled = false
patience = 10
```

- [ ] **Step 1:** Create comprehensive dqn-smoketest.toml
- [ ] **Step 2:** Create comprehensive dqn-production.toml
- [ ] **Step 3:** Update dqn-hyperopt.toml with new sections
- [ ] **Step 4:** Verify all TOML fields are read by training_profile.rs
- [ ] **Step 5:** Build + test
- [ ] **Step 6:** Commit: "feat: comprehensive TOML training profiles — every parameter configurable"

---

## Task 8: Integration test — config consistency

**Files:**
- Create: `crates/ml/src/trainers/dqn/smoke_tests/config_consistency.rs`

Write a test that verifies all three config structs produce identical values when constructed through the proper flow:

```rust
#[test]
fn test_config_consistency() {
    let hp = DQNHyperparameters::conservative();
    let dqn_config = DQNConfig::from(&hp);
    let gpu_config = GpuDqnTrainConfig::from_hyperparams(&hp, &dqn_config);

    // All must agree
    assert_eq!(dqn_config.num_atoms as usize, hp.num_atoms);
    assert!((dqn_config.v_min - hp.v_min()).abs() < 1e-6);
    assert!((gpu_config.v_min as f64 - hp.v_min()).abs() < 0.01);
    assert!((gpu_config.lr as f64 - hp.learning_rate).abs() < 1e-8);
    // ... all shared fields
}
```

- [ ] **Step 1:** Write config consistency test
- [ ] **Step 2:** Test passes
- [ ] **Step 3:** Commit: "test: config consistency — all 3 structs agree on shared parameters"

---

## Execution Order

```
Task 1 (remove use_ booleans) → Task 2 (unify defaults) → Task 3 (TOML exposure)
    → Task 4 (v_range computation) → Task 5 (DQNConfig from HP)
    → Task 6 (GpuConfig from HP) → Task 7 (TOML files) → Task 8 (consistency test)
```

Each task produces a working, testable state. Total: ~8 commits.
