//! Test suite for DQNTrainer PortfolioTracker initialization (Bug #2 fix) //! //! This test validates that PortfolioTracker is correctly initialized in DQNTrainer //! and properly configured with starting capital and spread parameters. //! //! Bug #2 Context: Portfolio features were hardcoded as empty vector [0.0, 0.0, 0.0] //! at ml/src/trainers/dqn.rs:1528. PortfolioTracker module exists (9/9 tests passing) //! but was NOT USED by DQNTrainer. //! //! Test Strategy (TDD): //! 1. Write tests first (expect failures) //! 2. Implement PortfolioTracker field in DQNTrainer struct //! 3. Initialize in constructor with $100k cash and 1bp spread //! 4. Watch tests pass use anyhow::Result; use ml::trainers::dqn::{DQNHyperparameters, DQNTrainer}; /// Test 1: Verify PortfolioTracker is initialized with correct cash ($100,000) /// /// Expected behavior: /// - DQNTrainer should have a portfolio_tracker field /// - Initial capital should be $100,000 /// - Portfolio value should equal cash when no positions are open #[tokio::test] async fn test_portfolio_tracker_initialized_with_correct_cash() -> Result<()> { // Create DQN trainer with conservative hyperparameters let hyperparams = DQNHyperparameters::conservative(); let trainer = DQNTrainer::new(hyperparams)?; // Verify portfolio_tracker is initialized with $100,000 let features = trainer.portfolio_tracker.get_portfolio_features(100.0); assert_eq!( features[0], 100_000.0, "Portfolio value should be $100,000 at initialization" ); Ok(()) } /// Test 2: Verify spread is set correctly (0.0001 = 1 basis point) /// /// Expected behavior: /// - PortfolioTracker should be initialized with 1 basis point spread /// - Spread should be 0.0001 (as a fraction) #[tokio::test] async fn test_portfolio_tracker_spread_initialization() -> Result<()> { // Create DQN trainer let hyperparams = DQNHyperparameters::conservative(); let trainer = DQNTrainer::new(hyperparams)?; // Verify spread is 1 basis point (0.0001) let features = trainer.portfolio_tracker.get_portfolio_features(100.0); assert_eq!( features[2], 0.0001, "Spread should be 1 basis point (0.0001)" ); Ok(()) } /// Test 3: Verify portfolio features are extracted correctly /// /// Expected behavior: /// - get_portfolio_features() should return [value, position, spread] /// - Initial state: [100_000.0, 0.0, 0.0001] #[tokio::test] async fn test_portfolio_features_extraction() -> Result<()> { // Create DQN trainer let hyperparams = DQNHyperparameters::conservative(); let trainer = DQNTrainer::new(hyperparams)?; // Verify all portfolio features are correctly initialized let features = trainer.portfolio_tracker.get_portfolio_features(100.0); assert_eq!( features[0], 100_000.0, "Portfolio value should be $100k (cash, no positions)" ); assert_eq!(features[1], 0.0, "Position size should be 0 (flat)"); assert_eq!(features[2], 0.0001, "Spread should be 1 basis point"); Ok(()) } /// Test 4: Verify PortfolioTracker survives across training loop initialization /// /// This test ensures that portfolio_tracker is properly initialized before /// the training loop begins, preventing the Bug #2 scenario where portfolio /// features were always [0.0, 0.0, 0.0]. #[tokio::test] async fn test_portfolio_tracker_persists_through_initialization() -> Result<()> { // Create DQN trainer let hyperparams = DQNHyperparameters::conservative(); let trainer = DQNTrainer::new(hyperparams)?; // Verify PortfolioTracker is initialized (not the bug scenario of [0, 0, 0]) let features = trainer.portfolio_tracker.get_portfolio_features(100.0); // Bug #2 would have resulted in [0.0, 0.0, 0.0] // Verify we get the correct initial values instead assert_ne!( features[0], 0.0, "Portfolio value should NOT be 0 (Bug #2 scenario)" ); assert_eq!(features[0], 100_000.0, "Portfolio value should be $100k"); Ok(()) } /// Test 5: Verify PortfolioTracker can be reset for new epochs /// /// Expected behavior: /// - After execute_action(), portfolio state changes /// - After reset(), portfolio state returns to initial values #[tokio::test] async fn test_portfolio_tracker_reset_capability() -> Result<()> { use ml::dqn::agent::TradingAction; // Create DQN trainer let hyperparams = DQNHyperparameters::conservative(); let mut trainer = DQNTrainer::new(hyperparams)?; // Execute a buy action to change portfolio state trainer .portfolio_tracker .execute_action(TradingAction::Buy, 100.0, 10.0); // Verify portfolio state changed let features_after_trade = trainer.portfolio_tracker.get_portfolio_features(100.0); assert_eq!( features_after_trade[1], 10.0, "Position size should be 10.0 after buy" ); // Reset portfolio for new epoch trainer.portfolio_tracker.reset(); // Verify portfolio state returned to initial values let features_after_reset = trainer.portfolio_tracker.get_portfolio_features(100.0); assert_eq!( features_after_reset[0], 100_000.0, "Portfolio value should be $100k after reset" ); assert_eq!( features_after_reset[1], 0.0, "Position size should be 0 after reset" ); assert_eq!( features_after_reset[2], 0.0001, "Spread should remain 1 basis point after reset" ); Ok(()) }