Files
foxhunt/WAVE3_A4_MULTIOBJECTIVE_TESTS_REPORT.md
jgrusewski 17d94e654c feat(dqn): Wave 10 - Architectural improvements and bug fixes
Wave 10 Summary:
- A1-A4: Architecture upgrades (4x network, LeakyReLU, Xavier init, diagnostics)
- A5-A6: Integration testing and production validation
- A7: Research hyperopt vs manual tuning (manual recommended)
- A8-A12: HOLD penalty tuning and critical bug fixes

Architecture Changes:
- Network expansion: [128,64,32] → [256,128,64] (2.5x parameters)
- LeakyReLU activation (alpha=0.01) to prevent dead neurons
- Xavier/Glorot initialization for better gradient flow
- Real-time diagnostic monitoring (Q-values, dead neurons, gradients)

Critical Bugs Fixed:
- Bug #1: HOLD penalty not wired to reward calculation
- Bug #2: Zero price error in calculate_hold_reward (velocity-based fix)
- Huber loss default enabled (Wave 9)
- Shape mismatch fix (Wave 8)

Test Results:
- Integration tests: 149/152 passing (98%)
- New tests: 40+ tests added across 15 files
- Xavier init: 5/5 tests passing
- HOLD penalty wiring: 4/4 tests passing
- Zero price fix: 4/4 tests passing

Known Issues:
- HOLD bias persists at ~100% despite penalties
- Gradient collapse: 217 instances per training run (norm=0.0)
- Reversed penalty effect: Higher penalties → worse Q-spread
- Root cause: Gradient clipping bottleneck (max_norm=10.0 vs penalty signal)

Phase 1 Trials (all completed without crashes):
- Penalty 0.5: Q-spread 250 pts, HOLD 100%
- Penalty 1.0: Q-spread 251 pts, HOLD 100%
- Penalty 2.0: Q-spread 255 pts, HOLD 100% (+ Q-value explosion)

Next Steps: Architectural investigation via parallel agent debugging

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 00:38:23 +01:00

17 KiB

Wave 3 - Agent A4: Multi-Objective Function Unit Tests

Date: 2025-11-05 Status: COMPLETE - 15 tests created, all failing as expected (TDD) Duration: 40 minutes Agent: Wave 3 Agent A4


Executive Summary

Created comprehensive unit tests for the DQN hyperopt multi-objective function BEFORE implementation (Test-Driven Development). All 15 tests (including 3 edge case tests) compile successfully and fail with expected panic messages, ready for implementation.

Key Achievement: Established test-driven development baseline for multi-objective optimization that will prevent Trial #31-style failures (Q-value collapse, action degeneration, early stopping).


Deliverables

1. Test File Created

File: ml/tests/dqn_hyperopt_multiobjective_test.rs

  • Lines: 579 lines
  • Tests: 15 comprehensive tests (12 core + 3 edge cases)
  • Status: Compiles (1 warning - expected dead code)
  • Execution: All 15 tests fail with expected panic messages

2. Test Structure

DQN Hyperopt Multi-Objective Tests (15 tests)
├── Component Tests (6 tests)
│   ├── test_reward_component_normalization()
│   ├── test_diversity_penalty_balanced_actions()
│   ├── test_diversity_penalty_extreme_hold()
│   ├── test_stability_penalty_good_qvalues()
│   ├── test_stability_penalty_gradient_explosion()
│   └── test_completion_penalty_early_stop()
├── Integration Tests (3 tests)
│   ├── test_multiobjective_optimal_trial()
│   ├── test_multiobjective_broken_trial()
│   └── test_weight_sensitivity()
├── Hard Constraint Tests (3 tests)
│   ├── test_hard_constraint_q_value_floor()
│   ├── test_hard_constraint_training_loss_ceiling()
│   └── test_hard_constraint_minimum_epochs()
└── Edge Case Tests (3 tests)
    ├── test_edge_case_zero_entropy()           [100% single action]
    ├── test_edge_case_q_value_boundaries()     [Boundaries 0.5, 10.0]
    └── test_edge_case_gradient_norm_threshold() [Threshold 10.0]

Test Details

Component Tests (6 tests)

1. test_reward_component_normalization()

Purpose: Verify reward component is correctly negated for minimization

Test Cases:

  • avg_episode_reward = 0.001 → reward_component = -0.001
  • avg_episode_reward = 0.005 → reward_component = -0.005 (better)

Expected Behavior: Higher rewards result in more negative components (better for optimizer minimization).

2. test_diversity_penalty_balanced_actions()

Purpose: Verify balanced action distribution (33/33/33) results in low penalty

Test Case: action_distribution = [0.33, 0.33, 0.34]

Expected: Penalty < 0.05 (entropy ≈ max_entropy = ln(3) ≈ 1.099)

3. test_diversity_penalty_extreme_hold()

Purpose: Verify extreme HOLD bias (5/5/90) results in high penalty

Test Case: action_distribution = [0.05, 0.05, 0.90]

Expected: Penalty > 0.5 (low entropy, poor diversity score)

4. test_stability_penalty_good_qvalues()

Purpose: Verify Q-values in healthy range [0.5, 10.0] result in zero penalty

Test Cases:

  • avg_q_value = 5.0 (healthy)
  • avg_gradient_norm = 2.0 (below 10.0 threshold)

Expected: Penalty < 0.1

5. test_stability_penalty_gradient_explosion()

Purpose: Verify gradient explosion (grad_norm > 10) results in high penalty

Test Case: avg_gradient_norm = 150.0

Expected: Penalty > 5.0 (catastrophic gradient explosion)

6. test_completion_penalty_early_stop()

Purpose: Verify early stopping results in penalty

Test Case: epochs_completed = 15, target_epochs = 50 (30% completion)

Expected: Penalty > 0.5 (70% of epochs missing)


Integration Tests (3 tests)

7. test_multiobjective_optimal_trial()

Purpose: Verify "perfect" trial gets low objective score

Test Case: Healthy metrics (balanced actions, good Q-values, full epochs)

Expected Calculation:

reward_component = -0.001
diversity_penalty = ~0.0
stability_penalty = ~0.0
completion_penalty = ~0.0

objective = 0.8 * (-0.001) + 0.1 * 0.0 + 0.05 * 0.0 + 0.05 * 0.0
          = -0.0008

Expected: objective < -0.0005 (negative = good for minimization)

8. test_multiobjective_broken_trial()

Purpose: Verify broken trial (99% HOLD) gets high objective score

Test Case: HOLD bias metrics (90% HOLD, low reward)

Expected Calculation:

reward_component = -0.0003
diversity_penalty = ~0.6 (high due to HOLD bias)
stability_penalty = ~0.0
completion_penalty = ~0.0

objective = 0.8 * (-0.0003) + 0.1 * 0.6 + 0.05 * 0.0 + 0.05 * 0.0
          = -0.00024 + 0.06
          = +0.05976

Expected: objective > 0.03 (positive = bad for minimization)

9. test_weight_sensitivity()

Purpose: Verify weight changes affect trial ranking correctly

Test Cases:

  • Good trial (balanced actions, good reward)
  • HOLD-biased trial (90% HOLD, low reward)

Expected: good_objective < hold_objective (good trial ranks better)


Hard Constraint Tests (3 tests)

10. test_hard_constraint_q_value_floor()

Purpose: Verify Q-value < -10.0 triggers trial rejection

Test Case: avg_q_value = -681.92 (Trial #31 collapse)

Expected: objective = 1e6 (penalty)

11. test_hard_constraint_training_loss_ceiling()

Purpose: Verify training loss > 1000.0 triggers trial rejection

Test Case: train_loss = 5000.0 (numerical explosion)

Expected: objective = 1e6 (penalty)

12. test_hard_constraint_minimum_epochs()

Purpose: Verify epochs_completed < 20 triggers trial rejection

Test Case: epochs_completed = 15 (below minimum)

Expected: objective = 1e6 (penalty)


Test Helpers

Data Builders (5 functions)

  1. create_healthy_metrics(): Balanced actions, good Q-values, full epochs

    DQNMetrics {
        train_loss: 0.1,
        val_loss: 0.15,
        avg_q_value: 5.0,
        final_epsilon: 0.01,
        epochs_completed: 50,
        avg_episode_reward: 0.001,
        action_distribution: [0.33, 0.33, 0.34],
        avg_gradient_norm: 2.0,
    }
    
  2. create_q_collapse_metrics(): Q-value collapse, degenerate actions

    avg_q_value: -681.92,
    action_distribution: [0.994, 0.003, 0.003],  // 99.4% BUY
    epochs_completed: 10,
    
  3. create_high_loss_metrics(): Numerical explosion

    train_loss: 5000.0,
    avg_gradient_norm: 150.0,
    
  4. create_early_stop_metrics(): Early stopping

    epochs_completed: 15,
    final_epsilon: 0.50,  // High (not enough decay)
    
  5. create_hold_bias_metrics(): Extreme HOLD bias

    action_distribution: [0.05, 0.05, 0.90],  // 90% HOLD
    avg_episode_reward: 0.0003,  // Low (agent not acting)
    

Component Functions (NOT IMPLEMENTED)

1. calculate_reward_component(metrics: &DQNMetrics) -> f64

Formula: -metrics.avg_episode_reward

Purpose: Negate reward for minimization objective

Tests: test_reward_component_normalization


2. calculate_diversity_penalty(metrics: &DQNMetrics) -> f64

Formula:

entropy = -Σ(p_i * ln(p_i)) for i in [BUY, SELL, HOLD]
max_entropy = ln(3)  1.099
diversity_score = entropy / max_entropy
penalty = 1.0 - diversity_score

Purpose: Penalize degenerate action distributions (99% single action)

Tests:

  • test_diversity_penalty_balanced_actions (33/33/33 → penalty < 0.05)
  • test_diversity_penalty_extreme_hold (5/5/90 → penalty > 0.5)

3. calculate_stability_penalty(metrics: &DQNMetrics) -> f64

Formula:

// Q-value penalty
q_penalty = if Q < 0.5 { |0.5 - Q| }
            else if Q > 10.0 { (Q - 10.0) * 0.1 }
            else { 0.0 }

// Gradient penalty
grad_penalty = if grad_norm > 10.0 { (grad_norm - 10.0) * 0.5 }
               else { 0.0 }

stability_penalty = q_penalty + grad_penalty

Purpose: Penalize Q-value collapse and gradient explosions

Tests:

  • test_stability_penalty_good_qvalues (Q=5.0, grad=2.0 → penalty < 0.1)
  • test_stability_penalty_gradient_explosion (grad=150.0 → penalty > 5.0)

4. calculate_completion_penalty(metrics: &DQNMetrics, target_epochs: usize) -> f64

Formula: (target_epochs - epochs_completed) / target_epochs

Purpose: Penalize early stopping (insufficient training)

Tests: test_completion_penalty_early_stop (15/50 epochs → penalty > 0.5)


5. violates_hard_constraints(metrics: &DQNMetrics) -> bool

Constraints:

  1. Q-value floor: avg_q_value > -10.0
  2. Training loss ceiling: train_loss < 1000.0
  3. Minimum epochs: epochs_completed >= 20

Purpose: Trial rejection mechanism

Tests:

  • test_hard_constraint_q_value_floor
  • test_hard_constraint_training_loss_ceiling
  • test_hard_constraint_minimum_epochs

6. calculate_multiobjective(metrics: &DQNMetrics, target_epochs: usize) -> f64

Formula:

if violates_hard_constraints(metrics) {
    return 1e6;  // Penalty
}

let reward = calculate_reward_component(metrics);
let diversity = calculate_diversity_penalty(metrics);
let stability = calculate_stability_penalty(metrics);
let completion = calculate_completion_penalty(metrics, target_epochs);

0.8 * reward + 0.1 * diversity + 0.05 * stability + 0.05 * completion

Purpose: Combine all components into single objective value

Tests:

  • test_multiobjective_optimal_trial (good → objective < -0.0005)
  • test_multiobjective_broken_trial (HOLD bias → objective > 0.03)
  • test_weight_sensitivity (good ranks better than broken)
  • All 3 hard constraint tests

Edge Case Tests (3 tests)

13. test_edge_case_zero_entropy()

Purpose: Verify 100% single action (zero entropy) results in maximum penalty

Test Case: action_distribution = [1.0, 0.0, 0.0] (100% BUY)

Formula:

entropy = -(1.0 * ln(1.0) + 0.0 * ln(0.0) + 0.0 * ln(0.0))
        = -(0 + 0 + 0)  [ln(0) treated as 0 in entropy calculation]
        = 0
diversity_score = 0 / 1.099 = 0
penalty = 1.0 - 0 = 1.0 (maximum)

Expected: penalty > 0.95

14. test_edge_case_q_value_boundaries()

Purpose: Verify Q-value penalty behavior at exact boundaries (0.5 and 10.0)

Test Cases:

  1. Q-value = 0.5 (lower boundary) → penalty < 0.01
  2. Q-value = 10.0 (upper boundary) → penalty < 0.01
  3. Q-value = 0.49 (below boundary) → penalty > 0.0
  4. Q-value = 10.01 (above boundary) → penalty > 0.0

Purpose: Ensure boundary conditions are handled correctly (≤ vs <, ≥ vs >)

15. test_edge_case_gradient_norm_threshold()

Purpose: Verify gradient norm penalty behavior at exact threshold (10.0)

Test Cases:

  1. grad_norm = 10.0 (at threshold) → penalty < 0.1
  2. grad_norm = 9.99 (below threshold) → penalty < 0.1
  3. grad_norm = 10.01 (above threshold) → penalty > 0.0

Purpose: Ensure threshold behavior is consistent (gradient clipping max_norm=10.0)


DQNMetrics Extensions Required

Current Structure (ml/src/hyperopt/adapters/dqn.rs)

pub struct DQNMetrics {
    pub train_loss: f64,
    pub val_loss: f64,
    pub avg_q_value: f64,
    pub final_epsilon: f64,
    pub epochs_completed: usize,
    pub avg_episode_reward: f64,
}

Proposed Extensions (2 new fields)

pub struct DQNMetrics {
    pub train_loss: f64,
    pub val_loss: f64,
    pub avg_q_value: f64,
    pub final_epsilon: f64,
    pub epochs_completed: usize,
    pub avg_episode_reward: f64,
    pub action_distribution: [f64; 3],  // ✅ NEW: [BUY%, SELL%, HOLD%]
    pub avg_gradient_norm: f64,         // ✅ NEW: Average gradient norm
}

Implementation Tasks:

  1. Add fields to DQNMetrics struct
  2. Track actions in TrainingMonitor (ml/src/trainers/dqn.rs)
  3. Track gradient norms during training
  4. Populate fields in train_with_params() (ml/src/hyperopt/adapters/dqn.rs)

See DQN_HYPEROPT_OBJECTIVE_ANALYSIS.md Section 6.1 for details.


Test Execution Results

Compilation

$ cargo test -p ml --test dqn_hyperopt_multiobjective_test --no-run
   Compiling ml v1.0.0 (/home/jgrusewski/Work/foxhunt/ml)
warning: multiple fields are never read
  --> ml/tests/dqn_hyperopt_multiobjective_test.rs:43:9
   = note: `#[warn(dead_code)]` on by default

warning: `ml` (test "dqn_hyperopt_multiobjective_test") generated 1 warning
    Finished `test` profile [unoptimized] target(s) in 0.59s
  Executable tests/dqn_hyperopt_multiobjective_test.rs (target/debug/deps/dqn_hyperopt_multiobjective_test-39b5064c47028623)

Status: Compiles successfully (1 warning expected - dead code will be used after implementation)

Test Execution

$ cargo test -p ml --test dqn_hyperopt_multiobjective_test
    Finished `test` profile [unoptimized] target(s) in 0.39s
     Running tests/dqn_hyperopt_multiobjective_test.rs

running 15 tests
test test_completion_penalty_early_stop ... FAILED
test test_diversity_penalty_balanced_actions ... FAILED
test test_diversity_penalty_extreme_hold ... FAILED
test test_edge_case_gradient_norm_threshold ... FAILED
test test_edge_case_q_value_boundaries ... FAILED
test test_edge_case_zero_entropy ... FAILED
test test_hard_constraint_minimum_epochs ... FAILED
test test_hard_constraint_q_value_floor ... FAILED
test test_hard_constraint_training_loss_ceiling ... FAILED
test test_multiobjective_broken_trial ... FAILED
test test_multiobjective_optimal_trial ... FAILED
test test_reward_component_normalization ... FAILED
test test_stability_penalty_good_qvalues ... FAILED
test test_stability_penalty_gradient_explosion ... FAILED
test test_weight_sensitivity ... FAILED

test result: FAILED. 0 passed; 15 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s

Status: All 15 tests fail with expected panic messages:

  • calculate_reward_component() not implemented yet
  • calculate_diversity_penalty() not implemented yet
  • calculate_stability_penalty() not implemented yet
  • calculate_completion_penalty() not implemented yet
  • violates_hard_constraints() not implemented yet
  • calculate_multiobjective() not implemented yet

Expected Benefits After Implementation

1. Prevent Trial #31-Style Failures

Current Problem (Single-Objective):

  • Trial #31: avg_reward = +0.000733 (BEST), but Q-value = -681.92 (CATASTROPHIC)
  • Optimizer selected Trial #31 because it only checked episode reward

After Implementation (Multi-Objective):

Trial #31:
  Hard Constraint Check: Q-value = -681.92 < -10.0  VIOLATION
  Objective: 1e6 (penalty)
  Result: REJECTED

2. Ensure Action Diversity

Current Problem:

  • Trial #31: 99.4% BUY, 0.3% SELL, 0.3% HOLD (degenerate)

After Implementation:

Trial #31:
  Action Distribution: [0.994, 0.003, 0.003]
  Diversity Penalty: ~0.9 (very high)
  Objective: +0.08 (bad due to high penalty)
  Result: Ranked low

3. Reward Stable Training

Current Problem:

  • Trials with early stopping (epoch 10/50) not penalized

After Implementation:

Early Stop Trial:
  Epochs: 10/50 = 20% completion
  Completion Penalty: 0.8 (80% missing)
  Objective: +0.04 (bad due to high penalty)
  Result: Ranked low

Next Steps

Immediate (Priority 1)

  1. Extend DQNMetrics (1-2 hours):

    • Add action_distribution: [f64; 3]
    • Add avg_gradient_norm: f64
    • Update train_with_params() to populate fields
  2. Implement Component Functions (2-3 hours):

    • calculate_reward_component()
    • calculate_diversity_penalty()
    • calculate_stability_penalty()
    • calculate_completion_penalty()
    • violates_hard_constraints()
    • calculate_multiobjective()
  3. Run Tests (5 minutes):

    cargo test -p ml --test dqn_hyperopt_multiobjective_test
    
    • Expected: All 12 tests pass

Follow-Up (Priority 2)

  1. Integrate into Hyperopt (1 hour):

    • Replace extract_objective() in ml/src/hyperopt/adapters/dqn.rs
    • Use calculate_multiobjective() as objective function
  2. Re-run Hyperopt (3-4 hours):

    • Deploy to Runpod with new objective
    • 100 trials, 50 epochs/trial
    • Compare results to Trial #31 baseline
  3. Validate Results (1 hour):

    • Verify no Q-value collapses (< -10.0)
    • Verify balanced action distributions (>5% per action)
    • Verify sufficient training (≥20 epochs)
    • Document in DQN_HYPEROPT_RESULTS_V2.md

Summary

Deliverables

  • Test file created: ml/tests/dqn_hyperopt_multiobjective_test.rs (579 lines)
  • 15 tests defined (6 component, 3 integration, 3 hard constraint, 3 edge case)
  • 5 test helper functions (data builders)
  • 6 component functions stubbed (ready for implementation)
  • All tests compile successfully
  • All tests fail with expected panic messages (TDD baseline)

Test Coverage

Component Tests Coverage
Reward Component 1 Normalization
Diversity Penalty 2 + 1 edge Balanced, Extreme HOLD, Zero entropy
Stability Penalty 2 + 2 edge Good Q-values, Gradient explosion, Q boundaries, Grad threshold
Completion Penalty 1 Early stop
Hard Constraints 3 Q-value floor, Loss ceiling, Min epochs
Integration 3 Optimal trial, Broken trial, Weight sensitivity

Success Criteria

  • All test names defined
  • All assertions defined
  • Tests compile successfully
  • Tests fail with expected messages
  • Ready for implementation (TDD)

Status: COMPLETE - Test-driven development baseline established for multi-objective optimization.


End of Report