#![allow(
    clippy::assertions_on_constants,
    clippy::assertions_on_result_states,
    clippy::clone_on_copy,
    clippy::decimal_literal_representation,
    clippy::doc_markdown,
    clippy::empty_line_after_doc_comments,
    clippy::field_reassign_with_default,
    clippy::get_unwrap,
    clippy::identity_op,
    clippy::inconsistent_digit_grouping,
    clippy::indexing_slicing,
    clippy::integer_division,
    clippy::len_zero,
    clippy::let_underscore_must_use,
    clippy::manual_div_ceil,
    clippy::manual_let_else,
    clippy::manual_range_contains,
    clippy::modulo_arithmetic,
    clippy::needless_range_loop,
    clippy::non_ascii_literal,
    clippy::redundant_clone,
    clippy::shadow_reuse,
    clippy::shadow_same,
    clippy::shadow_unrelated,
    clippy::single_match_else,
    clippy::str_to_string,
    clippy::string_slice,
    clippy::tests_outside_test_module,
    clippy::too_many_lines,
    clippy::unnecessary_wraps,
    clippy::unseparated_literal_suffix,
    clippy::use_debug,
    clippy::useless_vec,
    clippy::wildcard_enum_match_arm,
    clippy::else_if_without_else,
    clippy::expect_used,
    clippy::missing_const_for_fn,
    clippy::similar_names,
    clippy::type_complexity,
    clippy::collapsible_else_if,
    clippy::doc_lazy_continuation,
    clippy::items_after_test_module,
    clippy::map_clone,
    clippy::multiple_unsafe_ops_per_block,
    clippy::unwrap_or_default,
    clippy::assign_op_pattern,
    clippy::needless_borrow,
    clippy::println_empty_string,
    clippy::unnecessary_cast,
    clippy::used_underscore_binding,
    clippy::create_dir,
    clippy::implicit_saturating_sub,
    clippy::exit,
    clippy::expect_fun_call,
    clippy::too_many_arguments,
    clippy::unnecessary_map_or,
    clippy::unwrap_used,
    dead_code,
    unused_imports,
    unused_variables,
    clippy::cloned_ref_to_slice_refs,
    clippy::neg_multiply,
    clippy::while_let_loop,
    clippy::bool_assert_comparison,
    clippy::excessive_precision,
    clippy::trivially_copy_pass_by_ref,
    clippy::op_ref,
    clippy::redundant_closure,
    clippy::unnecessary_lazy_evaluations,
    clippy::if_then_some_else_none,
    clippy::unnecessary_to_owned,
    clippy::single_component_path_imports,
)]
//! Portfolio Value Normalization Tests (Wave 16S-V12, Agent 17 - TDD)
//!
//! Test suite defining EXPECTED normalization behavior BEFORE implementation.
//! These tests will FAIL initially (TDD approach) and guide the implementation.
//!
//! # Problem Context
//! Q-value explosion (2463-2694) caused by absolute dollar values ($100K) in portfolio features.
//! Need to normalize to 1.0 ± 0.01 scale to prevent Q-value instability.
//!
//! # Current Implementation (Bug #17)
//! ```rust
//! pub fn get_portfolio_features(&self, current_price: f32) -> [f32; 3] {
//!     let portfolio_value = self.get_portfolio_value(current_price);
//!     [
//!         portfolio_value,    // [0] ABSOLUTE DOLLARS (e.g., 100,000.0) ❌
//!         self.position_size, // [1] Raw position
//!         self.avg_spread,    // [2] Spread
//!     ]
//! }
//! ```
//!
//! # Expected Implementation (After Fix)
//! ```rust
//! pub fn get_portfolio_features(&self, current_price: f32) -> [f32; 3] {
//!     let portfolio_value = self.get_portfolio_value(current_price);
//!     let normalized_value = portfolio_value / self.initial_capital; // RATIO ✅
//!     [
//!         normalized_value,   // [0] RATIO (e.g., 1.0 = initial capital, 1.02 = 2% gain)
//!         self.position_size, // [1] Unchanged
//!         self.avg_spread,    // [2] Unchanged
//!     ]
//! }
//! ```
//!
//! # Test Coverage (8 tests)
//! 1. `test_portfolio_features_normalized_to_ratio` - Verifies features[0] is ratio not absolute
//! 2. `test_initial_capital_normalization_divisor` - Tests scale-invariance across capital levels
//! 3. `test_reward_scale_correct` - Validates reward magnitude suitable for Q-learning
//! 4. `test_large_portfolio_changes_bounded` - Extreme gains/losses stay bounded
//! 5. `test_bankruptcy_scenario_graceful` - Portfolio value = $0 doesn't crash
//! 6. `test_position_feature_unchanged` - Normalization only affects features[0]
//! 7. `test_multiple_trades_accumulate` - Gains/losses compound correctly
//! 8. `test_normalization_consistency_across_prices` - Price-independent normalization

#![allow(unused_crate_dependencies)]

use ml::dqn::action_space::{ExposureLevel, FactoredAction, OrderType, Urgency};
use ml::dqn::portfolio_tracker::PortfolioTracker;
use tracing::info;

// ============================================================================
// Helper Functions - Create FactoredActions for Testing
// ============================================================================

/// Create a BUY action (Long100 + Market + Normal)
fn create_buy_action() -> FactoredAction {
    FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal)
}

/// Create a SELL action (Short100 + Market + Normal)
fn create_sell_action() -> FactoredAction {
    FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal)
}

/// Create a FLAT action (closes position)
fn create_flat_action() -> FactoredAction {
    FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal)
}

// ============================================================================
// Test 1: Portfolio Features Normalized to Ratio (6 assertions)
// ============================================================================

#[test]
fn test_portfolio_features_normalized_to_ratio() {
    // Setup: $100K initial capital
    let initial_capital = 100_000.0;
    let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    let price = 5000.0;

    // Get initial features
    let features = tracker.get_portfolio_features(price);
    info!(?features, "Initial features");

    // Verify features[0] is RATIO not ABSOLUTE
    assert!(
        features[0] >= 0.95 && features[0] <= 1.05,
        "Portfolio value should be normalized ratio ~1.0, got {}. \
         Expected: 1.0 ± 0.05 (within 5% of initial capital). \
         If this is absolute dollars (e.g., 100000.0), normalization is missing.",
        features[0]
    );

    assert!(
        features[0] < 200.0,
        "Portfolio value should NOT be absolute dollars (would be ~100000), got {}. \
         This indicates features[0] is not normalized by initial_capital.",
        features[0]
    );

    // After profitable trade
    let buy_action = create_buy_action();
    tracker.execute_action(buy_action, price, 2.0); // Buy 2 contracts at $5000

    let price_after_gain = 5100.0; // +2% price increase
    let features_after = tracker.get_portfolio_features(price_after_gain);
    info!(?features_after, "Features after +2% gain");

    assert!(
        features_after[0] > 1.0,
        "Profitable trade should increase ratio above 1.0, got {}. \
         Expected: >1.0 (gained value). \
         If features_after[0] ≈ features[0], normalization may be missing.",
        features_after[0]
    );

    assert!(
        features_after[0] < 1.10,
        "2% gain should be ~1.02 ratio, not >1.10, got {}. \
         Expected: 1.00-1.05 (2% gain + small transaction cost). \
         If features_after[0] >> 1.10, normalization may be incorrect.",
        features_after[0]
    );

    // After loss
    let sell_action = create_sell_action();
    tracker.execute_action(sell_action, price_after_gain, 2.0); // Close position

    let price_after_loss = 5050.0; // Slightly lower
    tracker.execute_action(create_buy_action(), price_after_loss, 2.0); // Re-enter
    let price_loss = 4950.0; // -2% from 5050

    let features_loss = tracker.get_portfolio_features(price_loss);
    info!(?features_loss, "Features after -2% loss");

    assert!(
        features_loss[0] < features_after[0],
        "Loss should decrease ratio, got {} vs previous {}. \
         Expected: Lower value after loss.",
        features_loss[0],
        features_after[0]
    );

    assert!(
        features_loss[0] > 0.0,
        "Even with loss, ratio should be positive, got {}. \
         Expected: >0.0 (portfolio still has value).",
        features_loss[0]
    );
}

// ============================================================================
// Test 2: Initial Capital Normalization Divisor (4 assertions)
// ============================================================================

#[test]
fn test_initial_capital_normalization_divisor() {
    // Test with different initial capitals
    let tracker_100k = PortfolioTracker::new(100_000.0, 0.0001, 0.0);
    let tracker_50k = PortfolioTracker::new(50_000.0, 0.0001, 0.0);
    let price = 5000.0;

    let features_100k = tracker_100k.get_portfolio_features(price);
    let features_50k = tracker_50k.get_portfolio_features(price);

    info!(?features_100k, "100K capital features");
    info!(?features_50k, "50K capital features");

    // Both should start at 1.0 regardless of initial capital
    assert!(
        (features_100k[0] - 1.0).abs() < 0.001,
        "100K capital should normalize to 1.0, got {}. \
         Expected: 1.0 (initial state = 1.0 × initial_capital). \
         If not ~1.0, normalization divisor is incorrect.",
        features_100k[0]
    );

    assert!(
        (features_50k[0] - 1.0).abs() < 0.001,
        "50K capital should normalize to 1.0, got {}. \
         Expected: 1.0 (initial state = 1.0 × initial_capital). \
         If not ~1.0, normalization is not scale-invariant.",
        features_50k[0]
    );

    // Scale-invariant: Same % gain = same ratio change
    let mut tracker_100k_mut = PortfolioTracker::new(100_000.0, 0.0001, 0.0);
    let mut tracker_50k_mut = PortfolioTracker::new(50_000.0, 0.0001, 0.0);

    // 100K: Buy 2 contracts at $5000 (2% of capital)
    tracker_100k_mut.execute_action(create_buy_action(), price, 2.0);
    let price_gain = 5100.0; // +2% gain
    let after_100k = tracker_100k_mut.get_portfolio_features(price_gain);

    // 50K: Buy 1 contract at $5000 (2% of capital)
    tracker_50k_mut.execute_action(create_buy_action(), price, 1.0);
    let after_50k = tracker_50k_mut.get_portfolio_features(price_gain);

    info!(?after_100k, "After +2% gain (100K)");
    info!(?after_50k, "After +2% gain (50K)");

    // Both should have similar ratio increase (2% gain)
    assert!(
        (after_100k[0] - after_50k[0]).abs() < 0.01,
        "Same % gain should produce same ratio regardless of initial capital. \
         Got: 100K={}, 50K={}, diff={}. \
         Expected: diff < 0.01 (scale-invariant normalization). \
         If diff > 0.01, normalization may not use initial_capital.",
        after_100k[0],
        after_50k[0],
        (after_100k[0] - after_50k[0]).abs()
    );
}

// ============================================================================
// Test 3: Reward Scale Correct (5 assertions)
// ============================================================================

#[test]
fn test_reward_scale_correct() {
    // Verify normalization produces correct reward scale for Q-learning
    let initial_capital = 100_000.0;
    let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    let price = 5000.0;

    let features_before = tracker.get_portfolio_features(price);
    info!(?features_before, "Features before trade");

    // Buy 2 contracts
    tracker.execute_action(create_buy_action(), price, 2.0);

    let price_gain = 5050.0; // +1% price move
    let features_after = tracker.get_portfolio_features(price_gain);
    info!(?features_after, "Features after +1% gain");

    let pnl_delta = features_after[0] - features_before[0];
    info!(pnl_delta, "P&L delta (ratio change)");

    // 1% price move on 2 contracts = ~0.01 ratio change
    // Note: 2 contracts * $5000 = $10K position = 10% of $100K capital
    // 1% price move on 10% position = 0.1% portfolio change = 0.001 ratio
    // But our position is 2 contracts, so expect ~0.002 ratio change
    assert!(
        pnl_delta > 0.001 && pnl_delta < 0.025,
        "1% gain on 2 contracts should produce 0.001-0.025 ratio change, got {}. \
         Expected: Small ratio change (~0.002). \
         If pnl_delta > 1.0, normalization is missing. \
         If pnl_delta < 0.001, position size may be incorrect.",
        pnl_delta
    );

    assert!(
        pnl_delta < 1.0,
        "Ratio change should NOT be absolute dollars (would be ~1000), got {}. \
         This indicates features[0] is not normalized.",
        pnl_delta
    );

    // Verify this is suitable for Q-learning
    assert!(
        pnl_delta.abs() < 0.10,
        "Reward magnitude should be <0.10 for stable Q-learning, got {}. \
         Expected: Small ratio changes (<0.10). \
         If pnl_delta >> 0.10, Q-values will explode.",
        pnl_delta
    );

    // Verify reward is positive
    assert!(
        pnl_delta > 0.0,
        "Profitable trade should produce positive reward, got {}. \
         Expected: pnl_delta > 0.",
        pnl_delta
    );

    // Verify magnitude is reasonable
    assert!(
        pnl_delta > 0.0005,
        "Reward should be detectable (>0.0005), got {}. \
         Expected: Meaningful signal for Q-learning. \
         If pnl_delta < 0.0005, position may be too small.",
        pnl_delta
    );
}

// ============================================================================
// Test 4: Large Portfolio Changes Bounded (6 assertions)
// ============================================================================

#[test]
fn test_large_portfolio_changes_bounded() {
    // Test extreme scenarios stay bounded
    let initial_capital = 100_000.0;

    // Extreme gain: +50%
    let mut tracker_gain = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    let entry_price = 5000.0;
    tracker_gain.execute_action(create_buy_action(), entry_price, 2.0);

    let gain_price = 7500.0; // +50% price increase
    let features_after_gain = tracker_gain.get_portfolio_features(gain_price);
    info!(?features_after_gain, "Features after +50% gain");

    assert!(
        features_after_gain[0] > 1.0,
        "Large gain should be >1.0, got {}. \
         Expected: Ratio > 1.0 (gained value).",
        features_after_gain[0]
    );

    assert!(
        features_after_gain[0] < 2.0,
        "50% gain should be ~1.5 ratio, not >2.0, got {}. \
         Expected: 1.0-1.6 (50% gain on partial position). \
         If features_after_gain[0] > 2.0, normalization may be incorrect.",
        features_after_gain[0]
    );

    // Extreme loss: -50%
    let mut tracker_loss = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    tracker_loss.execute_action(create_buy_action(), entry_price, 2.0);

    let loss_price = 2500.0; // -50% price decrease
    let features_after_loss = tracker_loss.get_portfolio_features(loss_price);
    info!(?features_after_loss, "Features after -50% loss");

    assert!(
        features_after_loss[0] < 1.0,
        "Large loss should be <1.0, got {}. \
         Expected: Ratio < 1.0 (lost value).",
        features_after_loss[0]
    );

    assert!(
        features_after_loss[0] > 0.0,
        "Even 50% loss should be >0.0 ratio, got {}. \
         Expected: Positive ratio (portfolio still has value). \
         If features_after_loss[0] < 0.0, calculation is incorrect.",
        features_after_loss[0]
    );

    assert!(
        features_after_loss[0] > 0.5,
        "50% loss on 2 contracts should be ~0.9 ratio (small position), got {}. \
         Expected: >0.5 (small position relative to capital). \
         If features_after_loss[0] < 0.5, position may be too large.",
        features_after_loss[0]
    );

    // Verify gains and losses are symmetric
    let gain_ratio_delta = features_after_gain[0] - 1.0;
    let loss_ratio_delta = 1.0 - features_after_loss[0];

    info!(gain_ratio_delta, "Gain ratio delta");
    info!(loss_ratio_delta, "Loss ratio delta");

    // Symmetric check (gains and losses should be roughly equal magnitude)
    assert!(
        (gain_ratio_delta - loss_ratio_delta).abs() < 0.2,
        "Symmetric 50% price moves should produce symmetric ratio changes. \
         Gain delta: {}, Loss delta: {}, diff: {}. \
         Expected: Similar magnitudes (within 0.2). \
         If diff > 0.2, normalization may be asymmetric.",
        gain_ratio_delta,
        loss_ratio_delta,
        (gain_ratio_delta - loss_ratio_delta).abs()
    );
}

// ============================================================================
// Test 5: Bankruptcy Scenario Graceful (3 assertions)
// ============================================================================

#[test]
fn test_bankruptcy_scenario_graceful() {
    // Test bankruptcy (portfolio value = $0) doesn't crash
    let initial_capital = 100_000.0;
    let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    let entry_price = 5000.0;

    tracker.execute_action(create_buy_action(), entry_price, 2.0);

    // Extreme loss causing bankruptcy (price → 0)
    let bankrupt_price = 0.0;
    let features_bankrupt = tracker.get_portfolio_features(bankrupt_price);
    info!(?features_bankrupt, "Features at bankruptcy (price=0)");

    assert!(
        features_bankrupt[0] >= 0.0,
        "Bankrupt should be 0.0 ratio, not negative, got {}. \
         Expected: >= 0.0 (portfolio value can't be negative). \
         If features_bankrupt[0] < 0.0, calculation is incorrect.",
        features_bankrupt[0]
    );

    assert!(
        features_bankrupt[0] < 0.1,
        "Bankrupt should be near 0.0, got {}. \
         Expected: < 0.1 (minimal value remaining). \
         If features_bankrupt[0] > 0.1, normalization may be incorrect.",
        features_bankrupt[0]
    );

    assert!(
        !features_bankrupt[0].is_nan(),
        "Bankrupt should not be NaN. \
         Expected: Valid number (0.0 or near-zero). \
         NaN indicates division by zero or invalid calculation."
    );
}

// ============================================================================
// Test 6: Position Feature Unchanged (2 assertions)
// ============================================================================

#[test]
fn test_position_feature_unchanged() {
    // Verify normalization only affects features[0] (value), not features[1] (position)
    let initial_capital = 100_000.0;
    let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    let price = 5000.0;

    tracker.execute_action(create_buy_action(), price, 2.0);

    let price_gain = 5100.0;
    let features = tracker.get_portfolio_features(price_gain);
    info!(?features, "Features after buy");

    assert_eq!(
        features[1], 2.0,
        "Position feature should be unchanged (absolute contracts), got {}. \
         Expected: 2.0 (bought 2 contracts). \
         If features[1] != 2.0, position tracking is broken.",
        features[1]
    );

    assert_eq!(
        features[2], 0.0001,
        "Spread feature should be unchanged, got {}. \
         Expected: 0.0001 (default spread). \
         If features[2] != 0.0001, spread is incorrect.",
        features[2]
    );
}

// ============================================================================
// Test 7: Multiple Trades Accumulate (8 assertions)
// ============================================================================

#[test]
fn test_multiple_trades_accumulate() {
    // Test that gains/losses accumulate correctly in normalized space
    let initial_capital = 100_000.0;
    let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    let start_price = 5000.0;

    let start = tracker.get_portfolio_features(start_price)[0];
    info!(start, "Starting ratio");

    assert!(
        (start - 1.0).abs() < 0.001,
        "Starting ratio should be ~1.0, got {}. \
         Expected: 1.0 (initial state). \
         If start != 1.0, initialization is broken.",
        start
    );

    // Trade 1: +1% gain
    tracker.execute_action(create_buy_action(), start_price, 2.0);
    let price_after_trade1 = 5050.0; // +1%
    let after_trade1 = tracker.get_portfolio_features(price_after_trade1)[0];
    info!(after_trade1, "After trade 1 (+1%)");

    assert!(
        after_trade1 > start,
        "First trade gain should increase ratio, got {} vs start {}. \
         Expected: after_trade1 > start.",
        after_trade1,
        start
    );

    // Close position
    tracker.execute_action(create_flat_action(), price_after_trade1, 2.0);

    // Trade 2: +1% gain (on new base)
    tracker.execute_action(create_buy_action(), price_after_trade1, 2.0);
    let price_after_trade2 = 5100.0; // +1% from 5050
    let after_trade2 = tracker.get_portfolio_features(price_after_trade2)[0];
    info!(after_trade2, "After trade 2 (+1%)");

    assert!(
        after_trade2 > after_trade1,
        "Second trade should compound gains, got {} vs {}. \
         Expected: after_trade2 > after_trade1.",
        after_trade2,
        after_trade1
    );

    // Verify compounding
    let total_gain = after_trade2 - start;
    info!(total_gain, "Total gain ratio");

    assert!(
        total_gain > 0.01 && total_gain < 0.05,
        "Two +1% trades should compound to ~2% ratio gain, got {}. \
         Expected: 0.01-0.05 (2× ~1% gains + transaction costs). \
         If total_gain < 0.01, position may be too small. \
         If total_gain > 0.05, normalization may be incorrect.",
        total_gain
    );

    assert!(
        after_trade2 > 1.0,
        "Multiple profitable trades should keep ratio >1.0, got {}. \
         Expected: >1.0 (accumulated gains).",
        after_trade2
    );

    assert!(
        after_trade2 < 1.10,
        "Two +1% trades should be <1.10 ratio, got {}. \
         Expected: 1.01-1.05. \
         If after_trade2 > 1.10, normalization may be incorrect.",
        after_trade2
    );

    // Verify intermediate values are reasonable
    assert!(
        after_trade1 < 1.05,
        "Single +1% trade should be <1.05 ratio, got {}. \
         Expected: 1.00-1.03.",
        after_trade1
    );

    assert!(
        total_gain < 0.10,
        "Total gain should be suitable for Q-learning (<0.10), got {}. \
         Expected: Small ratio changes.",
        total_gain
    );
}

// ============================================================================
// Test 8: Normalization Consistency Across Prices (5 assertions)
// ============================================================================

#[test]
fn test_normalization_consistency_across_prices() {
    // Verify normalization is price-independent when no position is held
    let initial_capital = 100_000.0;
    let tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0);

    // Get features at different price levels
    let features_low = tracker.get_portfolio_features(100.0); // Low price
    let features_mid = tracker.get_portfolio_features(5000.0); // Mid price
    let features_high = tracker.get_portfolio_features(50000.0); // High price

    info!(?features_low, "Features at low price (100)");
    info!(?features_mid, "Features at mid price (5000)");
    info!(?features_high, "Features at high price (50000)");

    // All should be ~1.0 (no position, so price doesn't matter)
    assert!(
        (features_low[0] - 1.0).abs() < 0.001,
        "No position: low price should give ratio ~1.0, got {}. \
         Expected: 1.0 (price doesn't affect cash-only portfolio). \
         If != 1.0, normalization may depend on price incorrectly.",
        features_low[0]
    );

    assert!(
        (features_mid[0] - 1.0).abs() < 0.001,
        "No position: mid price should give ratio ~1.0, got {}. \
         Expected: 1.0. \
         If != 1.0, normalization is broken.",
        features_mid[0]
    );

    assert!(
        (features_high[0] - 1.0).abs() < 0.001,
        "No position: high price should give ratio ~1.0, got {}. \
         Expected: 1.0. \
         If != 1.0, normalization is broken.",
        features_high[0]
    );

    // Verify consistency across prices
    assert!(
        (features_low[0] - features_mid[0]).abs() < 0.001,
        "No position: features should be price-independent, got low={}, mid={}. \
         Expected: Same value (~1.0). \
         If different, price affects normalization incorrectly.",
        features_low[0],
        features_mid[0]
    );

    assert!(
        (features_mid[0] - features_high[0]).abs() < 0.001,
        "No position: features should be price-independent, got mid={}, high={}. \
         Expected: Same value (~1.0). \
         If different, price affects normalization incorrectly.",
        features_mid[0],
        features_high[0]
    );
}

// ============================================================================
// Additional Edge Cases
// ============================================================================

#[test]
fn test_normalization_with_short_position() {
    // Verify normalization works correctly for short positions
    let initial_capital = 100_000.0;
    let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    let entry_price = 5000.0;

    // Open short position
    tracker.execute_action(create_sell_action(), entry_price, 2.0);
    let features_short = tracker.get_portfolio_features(entry_price);
    info!(?features_short, "Features after short entry");

    assert!(
        features_short[0] >= 0.95 && features_short[0] <= 1.05,
        "Short entry should be ~1.0 ratio (no P&L yet), got {}. \
         Expected: 1.0 ± 0.05.",
        features_short[0]
    );

    assert_eq!(
        features_short[1], -2.0,
        "Position should be -2.0 (short 2 contracts), got {}.",
        features_short[1]
    );

    // Price decreases (profitable for short)
    let price_decrease = 4900.0; // -2%
    let features_gain = tracker.get_portfolio_features(price_decrease);
    info!(?features_gain, "Features after -2% price (short profit)");

    assert!(
        features_gain[0] > 1.0,
        "Short position should profit from price decrease, got {}. \
         Expected: >1.0.",
        features_gain[0]
    );

    // Price increases (loss for short)
    let price_increase = 5100.0; // +2%
    let features_loss = tracker.get_portfolio_features(price_increase);
    info!(?features_loss, "Features after +2% price (short loss)");

    assert!(
        features_loss[0] < 1.0,
        "Short position should lose from price increase, got {}. \
         Expected: <1.0.",
        features_loss[0]
    );
}

#[test]
fn test_normalization_after_multiple_reversals() {
    // Test normalization remains consistent after position reversals
    let initial_capital = 100_000.0;
    let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0);
    let price = 5000.0;

    // Long → Short → Long → Flat
    tracker.execute_action(create_buy_action(), price, 2.0);
    let after_long1 = tracker.get_portfolio_features(price)[0];

    tracker.execute_action(create_sell_action(), price, 2.0);
    let after_short = tracker.get_portfolio_features(price)[0];

    tracker.execute_action(create_buy_action(), price, 2.0);
    let after_long2 = tracker.get_portfolio_features(price)[0];

    tracker.execute_action(create_flat_action(), price, 2.0);
    let after_flat = tracker.get_portfolio_features(price)[0];

    info!(after_long1, after_short, after_long2, after_flat, "Reversal sequence ratios");

    // All should be close to 1.0 (no net price movement)
    assert!(
        (after_long1 - 1.0).abs() < 0.05,
        "After first long: ratio should be ~1.0, got {}.",
        after_long1
    );

    assert!(
        (after_short - 1.0).abs() < 0.10,
        "After reversal to short: ratio should be ~1.0 (minus transaction costs), got {}.",
        after_short
    );

    assert!(
        (after_long2 - 1.0).abs() < 0.15,
        "After second reversal to long: ratio should be ~1.0 (minus more transaction costs), got {}.",
        after_long2
    );

    assert!(
        (after_flat - 1.0).abs() < 0.20,
        "After closing: ratio should be ~1.0 (minus all transaction costs), got {}.",
        after_flat
    );

    // Should have lost money due to transaction costs
    assert!(
        after_flat < 1.0,
        "Multiple reversals should lose money due to transaction costs, got {}. \
         Expected: <1.0.",
        after_flat
    );
}
