CRITICAL FINDINGS from 3-trial validation: - 85,120 gradient clipping warnings (81.6% of logs) - REGRESSION - Rainbow features DISABLED: use_dueling=false, use_distributional=false, use_noisy_nets=false - Negative Q-values confirmed: HOLD -1000 to -3250 - Performance: Sharpe 0.29 (target 0.77) Changes: - Fixed N-Step compilation (7/7 tests passing) - Fixed Distributional compilation (6/6 tests passing) - Fixed Dueling CUDA errors (10/10 tests passing) - Added TDD validation for state_dim=225 - Total: 23/23 Wave 11 tests passing (100%) Issues requiring investigation: 1. Why are Dueling/Distributional/Noisy disabled in hyperopt? 2. Why gradient explosion despite previous fixes? 3. Test coverage gaps - unit tests pass but integration fails 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
223 lines
9.5 KiB
Rust
223 lines
9.5 KiB
Rust
//! PPO Portfolio Tracker Tests (TDD)
|
|
//!
|
|
//! These tests validate the PortfolioTracker integration for PPO.
|
|
//! Tests written FIRST (TDD Red Phase) before implementation.
|
|
|
|
use ml::ppo::portfolio_tracker::PortfolioTracker;
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_initialization() {
|
|
// Test initial state (cash, position, pnl)
|
|
let tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
assert_eq!(tracker.cash_balance(), 10_000.0, "Initial cash should equal initial capital");
|
|
assert_eq!(tracker.current_position(), 0.0, "Initial position should be 0 (flat)");
|
|
assert_eq!(tracker.realized_pnl(), 0.0, "Initial realized P&L should be 0");
|
|
assert_eq!(tracker.unrealized_pnl(100.0), 0.0, "Initial unrealized P&L should be 0");
|
|
assert_eq!(tracker.total_value(100.0), 10_000.0, "Initial portfolio value should equal cash");
|
|
}
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_buy_action() {
|
|
// Test BUY action updates position and cash
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
// Execute PPO-style action: action index 0 = BUY
|
|
tracker.execute_ppo_action(0, 100.0, 10.0);
|
|
|
|
assert_eq!(tracker.current_position(), 10.0, "Position should be 10 contracts after BUY");
|
|
assert_eq!(tracker.cash_balance(), 9_000.0, "Cash should decrease by position cost (10 * 100)");
|
|
assert_eq!(tracker.average_entry_price(), 100.0, "Entry price should be recorded");
|
|
}
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_sell_action() {
|
|
// Test SELL action updates position and cash
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
// Execute PPO-style action: action index 1 = SELL
|
|
tracker.execute_ppo_action(1, 100.0, 10.0);
|
|
|
|
assert_eq!(tracker.current_position(), -10.0, "Position should be -10 contracts after SELL (short)");
|
|
assert_eq!(tracker.cash_balance(), 11_000.0, "Cash should increase by short proceeds (10 * 100)");
|
|
assert_eq!(tracker.average_entry_price(), 100.0, "Entry price should be recorded");
|
|
}
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_cash_reserve() {
|
|
// Test cash reserve enforcement
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 20.0); // 20% cash reserve
|
|
|
|
// Try to buy with insufficient cash (reserve enforcement)
|
|
// At price 100, max affordable = (10000 - 2000) / 100 = 80 contracts
|
|
// Request 100 contracts -> should be reduced to 80
|
|
tracker.execute_ppo_action(0, 100.0, 100.0);
|
|
|
|
let cash = tracker.cash_balance();
|
|
let portfolio_value = tracker.total_value(100.0);
|
|
let reserve_required = portfolio_value * 0.20;
|
|
|
|
assert!(
|
|
cash >= reserve_required,
|
|
"Cash reserve should be enforced: cash=${:.2}, reserve=${:.2}",
|
|
cash, reserve_required
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_pnl_calculation() {
|
|
// Test P&L calculation accuracy
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
// Open long position: Buy 10 contracts at $100
|
|
tracker.execute_ppo_action(0, 100.0, 10.0);
|
|
assert_eq!(tracker.cash_balance(), 9_000.0, "Cash after buy");
|
|
|
|
// Price rises to $110 -> +$100 unrealized profit
|
|
let unrealized = tracker.unrealized_pnl(110.0);
|
|
assert_eq!(unrealized, 100.0, "Unrealized P&L should be +$100 (10 contracts * $10 gain)");
|
|
|
|
// Total portfolio value = cash + position_value
|
|
let total_value = tracker.total_value(110.0);
|
|
assert_eq!(total_value, 10_100.0, "Portfolio value = 9000 (cash) + 1100 (position value)");
|
|
|
|
// Close position: Sell 10 contracts at $110
|
|
tracker.execute_ppo_action(1, 110.0, 10.0);
|
|
|
|
// After closing, realized P&L should be +$100
|
|
assert_eq!(tracker.current_position(), 0.0, "Position should be closed");
|
|
assert_eq!(tracker.realized_pnl(), 100.0, "Realized P&L should be +$100");
|
|
}
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_hold_action() {
|
|
// Test HOLD action (no change)
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
let initial_cash = tracker.cash_balance();
|
|
let initial_position = tracker.current_position();
|
|
|
|
// Execute PPO-style action: action index 2 = HOLD
|
|
tracker.execute_ppo_action(2, 100.0, 10.0);
|
|
|
|
assert_eq!(tracker.cash_balance(), initial_cash, "Cash should not change after HOLD");
|
|
assert_eq!(tracker.current_position(), initial_position, "Position should not change after HOLD");
|
|
}
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_reset() {
|
|
// Test reset functionality
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
// Execute trades
|
|
tracker.execute_ppo_action(0, 100.0, 10.0);
|
|
|
|
// Reset
|
|
tracker.reset();
|
|
|
|
assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should reset to initial capital");
|
|
assert_eq!(tracker.current_position(), 0.0, "Position should reset to 0");
|
|
assert_eq!(tracker.average_entry_price(), 0.0, "Entry price should reset to 0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_short_position_pnl() {
|
|
// Test short position P&L calculation
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
// Open short position: Sell 10 contracts at $100
|
|
tracker.execute_ppo_action(1, 100.0, 10.0);
|
|
assert_eq!(tracker.current_position(), -10.0, "Position should be -10 (short)");
|
|
assert_eq!(tracker.cash_balance(), 11_000.0, "Cash after short sale");
|
|
|
|
// Price falls to $90 -> +$100 unrealized profit (profitable for short)
|
|
let unrealized = tracker.unrealized_pnl(90.0);
|
|
assert_eq!(unrealized, 100.0, "Unrealized P&L should be +$100 (10 contracts * $10 gain from price drop)");
|
|
|
|
// Total portfolio value = cash + position_value (position_value is negative for shorts)
|
|
let total_value = tracker.total_value(90.0);
|
|
assert_eq!(total_value, 10_100.0, "Portfolio value = 11000 (cash) - 900 (short liability)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_portfolio_tracker_get_portfolio_features() {
|
|
// Test portfolio features retrieval
|
|
let tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
let features = tracker.get_portfolio_features(100.0);
|
|
|
|
// Features: [normalized_value, normalized_position, spread]
|
|
assert_eq!(features[0], 1.0, "Normalized portfolio value should be 1.0 (initial capital)");
|
|
assert_eq!(features[1], 0.0, "Normalized position should be 0.0 (no position)");
|
|
assert_eq!(features[2], 0.0001, "Spread should be 0.0001");
|
|
}
|
|
|
|
// ========== WAVE 2 AGENT 4: POSITION REVERSAL TESTS (TDD RED PHASE) ==========
|
|
|
|
#[test]
|
|
fn test_position_reversal_long_to_short() {
|
|
// Test position reversal from long to short
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
// Step 1: Open long position: Buy 20 contracts at $100
|
|
tracker.execute_ppo_action(0, 100.0, 20.0);
|
|
assert_eq!(tracker.current_position(), 20.0, "Should have long position of 20 contracts");
|
|
assert_eq!(tracker.cash_balance(), 8_000.0, "Cash = 10000 - (20 * 100)");
|
|
|
|
// Step 2: Reversal - Sell 40 contracts at $110 (close 20 long + open 20 short)
|
|
// Phase 1: Close 20 long at $110 -> cash += 20 * 110 = +2200
|
|
// Phase 2: Open 20 short at $110 -> cash += 20 * 110 = +2200
|
|
tracker.execute_ppo_action(1, 110.0, 40.0);
|
|
|
|
// Expected: Position = -20 (short), Cash = 8000 + 2200 + 2200 = 12400
|
|
assert_eq!(tracker.current_position(), -20.0, "Should have short position of -20 contracts after reversal");
|
|
assert_eq!(tracker.cash_balance(), 12_400.0, "Cash = 8000 + 2200 (close long) + 2200 (open short)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_position_reversal_short_to_long() {
|
|
// Test position reversal from short to long
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
|
|
|
|
// Step 1: Open short position: Sell 20 contracts at $100
|
|
tracker.execute_ppo_action(1, 100.0, 20.0);
|
|
assert_eq!(tracker.current_position(), -20.0, "Should have short position of -20 contracts");
|
|
assert_eq!(tracker.cash_balance(), 12_000.0, "Cash = 10000 + (20 * 100)");
|
|
|
|
// Step 2: Reversal - Buy 40 contracts at $90 (close 20 short + open 20 long)
|
|
// Phase 1: Close 20 short at $90 -> cash -= 20 * 90 = -1800
|
|
// Phase 2: Open 20 long at $90 -> cash -= 20 * 90 = -1800
|
|
tracker.execute_ppo_action(0, 90.0, 40.0);
|
|
|
|
// Expected: Position = 20 (long), Cash = 12000 - 1800 - 1800 = 8400
|
|
assert_eq!(tracker.current_position(), 20.0, "Should have long position of 20 contracts after reversal");
|
|
assert_eq!(tracker.cash_balance(), 8_400.0, "Cash = 12000 - 1800 (close short) - 1800 (open long)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_partial_reversal_insufficient_cash() {
|
|
// Test partial reversal when insufficient cash for full flip
|
|
let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 20.0); // 20% cash reserve
|
|
|
|
// Step 1: Open long position: Buy 50 contracts at $100
|
|
// Max affordable = (10000 - 2000 reserve) / 100 = 80 contracts
|
|
tracker.execute_ppo_action(0, 100.0, 50.0);
|
|
assert_eq!(tracker.current_position(), 50.0, "Should have long position of 50 contracts");
|
|
|
|
// Step 2: Attempt reversal - Sell 100 contracts at $110 (close 50 long + open 50 short)
|
|
// Phase 1: Close 50 long at $110 -> cash += 50 * 110 = +5500
|
|
// Phase 2: Try to open 50 short at $110 -> may be partial due to cash reserve
|
|
tracker.execute_ppo_action(1, 110.0, 100.0);
|
|
|
|
// After reversal:
|
|
// - Position should be negative (short) but may be less than -50 due to cash constraint
|
|
// - Cash reserve should still be enforced
|
|
let final_position = tracker.current_position();
|
|
let final_cash = tracker.cash_balance();
|
|
let portfolio_value = tracker.total_value(110.0);
|
|
let reserve_required = portfolio_value * 0.20;
|
|
|
|
assert!(final_position < 0.0, "Should have short position after reversal (negative)");
|
|
assert!(final_cash >= reserve_required, "Cash reserve should be enforced: cash=${:.2}, reserve=${:.2}", final_cash, reserve_required);
|
|
}
|