//! Wide format backtest simulation engine
//!
//! This module implements the main backtest simulation loop that matches
//! Finlab's backtest_core Cython implementation.
//!
//! # Weight Modes
//!
//! Supports two input modes:
//! 1. **Boolean signals** - Converted to equal weights (like Finlab with bool positions)
//! 2. **Float weights** - Custom weights, normalized to sum=1 (like Finlab with float positions)

// Import from refactored modules
use crate::config::BacktestConfig;
use crate::mae_mfe::calculate_mae_mfe_at_exit;
use crate::portfolio::PortfolioState;
use crate::position::{Position, PositionSnapshot};
use crate::stops::{detect_stops, detect_stops_finlab, detect_touched_exit};
use crate::tracker::{WideBacktestResult, NoopIndexTracker, IndexTracker, TradeTracker};
use crate::weights::{normalize_weights_finlab, IntoWeights};
use crate::{is_valid_price, FLOAT_EPSILON};

// ============================================================================
// Unified Simulation Engine
// ============================================================================

/// Optional OHLC prices for touched_exit mode
struct OhlcPrices<'a> {
    open: &'a [Vec<f64>],
    high: &'a [Vec<f64>],
    low: &'a [Vec<f64>],
}

/// Core simulation loop with generic trade tracking
///
/// This is the unified internal implementation that both `run_backtest`
/// and `run_backtest_with_trades` use. The `TradeTracker` trait allows
/// for zero-cost abstraction when trade tracking is not needed.
fn simulate_backtest<T: TradeTracker<Key = usize, Date = usize>>(
    close_prices: &[Vec<f64>],
    trade_prices: &[Vec<f64>],
    weights: &[Vec<f64>],
    rebalance_indices: &[usize],
    config: &BacktestConfig,
    tracker: &mut T,
    ohlc: Option<OhlcPrices>,
) -> Vec<f64> {
    if config.finlab_mode {
        simulate_backtest_finlab(close_prices, trade_prices, weights, rebalance_indices, config, tracker, ohlc)
    } else {
        simulate_backtest_standard(close_prices, trade_prices, weights, rebalance_indices, config, tracker)
    }
}

/// Finlab mode backtest simulation
///
/// Matches Finlab's backtest_core.pyx behavior exactly:
/// - T+1 execution model
/// - Cumulative return tracking (cr *= r)
/// - touched_exit intraday stop detection
fn simulate_backtest_finlab<T: TradeTracker<Key = usize, Date = usize>>(
    close_prices: &[Vec<f64>],
    trade_prices: &[Vec<f64>],
    weights: &[Vec<f64>],
    rebalance_indices: &[usize],
    config: &BacktestConfig,
    tracker: &mut T,
    ohlc: Option<OhlcPrices>,
) -> Vec<f64> {
    if close_prices.is_empty() {
        return vec![];
    }

    let n_times = close_prices.len();
    let n_assets = close_prices[0].len();

    let mut portfolio = PortfolioState::new();
    let mut weight_idx = 0;
    let mut prev_prices = close_prices[0].clone();
    let mut creturn = Vec::with_capacity(n_times);
    let mut stopped_stocks: Vec<bool> = vec![false; n_assets];
    let mut pending_weights: Option<Vec<f64>> = None;
    let mut pending_signal_index: Option<usize> = None;
    let mut pending_stop_exits: Vec<usize> = Vec::new();
    let mut active_weights: Vec<f64> = vec![0.0; n_assets];

    for t in 0..n_times {
        if t > 0 {
            // Step 1: Update max_prices (cr *= r, maxcr update)
            portfolio.update_max_prices(&close_prices[t]);

            // Touched exit mode: detect and execute intraday stops
            if config.touched_exit {
                if let Some(ref ohlc_data) = ohlc {
                    let touched_exits = detect_touched_exit(
                        &portfolio.positions,
                        &ohlc_data.open[t],
                        &ohlc_data.high[t],
                        &ohlc_data.low[t],
                        &close_prices[t],
                        &prev_prices,
                        config,
                    );

                    for touched in &touched_exits {
                        if let Some(pos) = portfolio.positions.remove(&touched.stock_id) {
                            let exit_value = pos.last_market_value * touched.exit_ratio;
                            let sell_value = exit_value - exit_value.abs() * (config.fee_ratio + config.tax_ratio);
                            portfolio.cash += sell_value;

                            if config.stop_trading_next_period && touched.stock_id < stopped_stocks.len() {
                                stopped_stocks[touched.stock_id] = true;
                            }
                            if touched.stock_id < active_weights.len() {
                                active_weights[touched.stock_id] = 0.0;
                            }

                            let exit_price = trade_prices[t].get(touched.stock_id).copied().unwrap_or(1.0);
                            // Wide format: no factor concept, use 1.0
                            tracker.close_trade(&touched.stock_id, t, Some(t), exit_price, 1.0, config.fee_ratio, config.tax_ratio);
                        }
                    }
                }
            }

            portfolio.update_previous_prices(&close_prices[t]);

            // Detect stops for T+1 execution
            let mut today_stops = if config.touched_exit {
                Vec::new()
            } else {
                detect_stops_finlab(&portfolio.positions, &close_prices[t], config)
            };

            // Step 2: Execute pending stop exits
            if !pending_stop_exits.is_empty() {
                let exits_to_process: Vec<usize> = pending_stop_exits
                    .iter()
                    .filter(|&&stock_id| {
                        if let Some(ref weights) = pending_weights {
                            let has_nonzero_weight = stock_id < weights.len() && weights[stock_id].abs() > FLOAT_EPSILON;
                            config.stop_trading_next_period || !has_nonzero_weight
                        } else {
                            true
                        }
                    })
                    .copied()
                    .collect();

                for stock_id in exits_to_process {
                    if let Some(pos) = portfolio.positions.remove(&stock_id) {
                        let market_value = pos.last_market_value;
                        let sell_value = market_value - market_value.abs() * (config.fee_ratio + config.tax_ratio);
                        portfolio.cash += sell_value;

                        if config.stop_trading_next_period && stock_id < stopped_stocks.len() {
                            stopped_stocks[stock_id] = true;
                        }
                        if stock_id < active_weights.len() {
                            active_weights[stock_id] = 0.0;
                        }

                        let exit_price = trade_prices[t].get(stock_id).copied().unwrap_or(1.0);
                        // Wide format: no factor concept, use 1.0
                        tracker.close_trade(&stock_id, t, None, exit_price, 1.0, config.fee_ratio, config.tax_ratio);
                        today_stops.retain(|&x| x != stock_id);
                    }
                }
                pending_stop_exits.clear();
            }

            pending_stop_exits.extend(today_stops);

            // Step 3: Execute rebalance
            if let Some(mut target_weights) = pending_weights.take() {
                let signal_index = pending_signal_index.take().unwrap_or(t - 1);

                if config.stop_trading_next_period {
                    let original_sum: f64 = target_weights.iter().map(|w| w.abs()).sum();
                    for (i, stopped) in stopped_stocks.iter().enumerate() {
                        if *stopped && i < target_weights.len() {
                            target_weights[i] = 0.0;
                        }
                    }
                    let remaining_sum: f64 = target_weights.iter().map(|w| w.abs()).sum();
                    if remaining_sum > 0.0 && remaining_sum < original_sum {
                        let scale_factor = original_sum / remaining_sum;
                        for w in target_weights.iter_mut() {
                            *w *= scale_factor;
                        }
                    }
                }

                // Close all trades during rebalance
                for stock_id in portfolio.positions.keys().copied().collect::<Vec<_>>() {
                    let exit_price = trade_prices[t].get(stock_id).copied().unwrap_or(1.0);
                    // Wide format: no factor concept, use 1.0
                    tracker.close_trade(&stock_id, t, Some(signal_index), exit_price, 1.0, config.fee_ratio, config.tax_ratio);
                }

                execute_finlab_rebalance(&mut portfolio, &target_weights, &close_prices[t], config);
                active_weights = target_weights.clone();

                for (stock_id, &target_weight) in target_weights.iter().enumerate() {
                    if target_weight != 0.0 && portfolio.positions.contains_key(&stock_id) {
                        let entry_price = trade_prices[t].get(stock_id).copied().unwrap_or(1.0);
                        // Wide format: no factor concept, use 1.0
                        tracker.open_trade(stock_id, t, signal_index, entry_price, target_weight, 1.0);
                    }
                }

                stopped_stocks = vec![false; n_assets];
            }

            update_entry_prices_after_nan(&mut portfolio, &close_prices[t], &prev_prices);
        }

        // Check for rebalance signal
        if rebalance_indices.contains(&t) && weight_idx < weights.len() {
            let target_weights = normalize_weights_finlab(&weights[weight_idx], &stopped_stocks, config.position_limit);
            pending_weights = Some(target_weights);
            pending_signal_index = Some(t);
            weight_idx += 1;
        }

        creturn.push(portfolio.balance_finlab(&close_prices[t]));
        prev_prices = close_prices[t].clone();
    }

    // Add pending entries for last day signals
    if let Some(weights) = pending_weights {
        let signal_index = pending_signal_index.unwrap_or(n_times.saturating_sub(1));
        for (stock_id, &weight) in weights.iter().enumerate() {
            if weight > FLOAT_EPSILON && !portfolio.positions.contains_key(&stock_id) {
                tracker.add_pending_entry(stock_id, signal_index, weight);
            }
        }
    }

    creturn
}

/// Standard mode backtest simulation
///
/// Simpler execution model without Finlab-specific behaviors:
/// - T+1 execution model
/// - Direct value tracking (no cumulative return)
fn simulate_backtest_standard<T: TradeTracker<Key = usize, Date = usize>>(
    close_prices: &[Vec<f64>],
    trade_prices: &[Vec<f64>],
    weights: &[Vec<f64>],
    rebalance_indices: &[usize],
    config: &BacktestConfig,
    tracker: &mut T,
) -> Vec<f64> {
    if close_prices.is_empty() {
        return vec![];
    }

    let n_times = close_prices.len();
    let n_assets = close_prices[0].len();

    let mut portfolio = PortfolioState::new();
    let mut weight_idx = 0;
    let mut prev_prices = close_prices[0].clone();
    let mut creturn = Vec::with_capacity(n_times);
    let mut stopped_stocks: Vec<bool> = vec![false; n_assets];
    let mut pending_weights: Option<Vec<f64>> = None;
    let mut pending_signal_index: Option<usize> = None;
    let mut pending_stop_exits: Vec<usize> = Vec::new();

    for t in 0..n_times {
        if t > 0 {
            if let Some(target_weights) = pending_weights.take() {
                let signal_index = pending_signal_index.take().unwrap_or(t - 1);

                // Close trades for exiting positions
                for (&stock_id, _) in portfolio.positions.iter() {
                    if stock_id < target_weights.len() && target_weights[stock_id] == 0.0 {
                        let exit_price = trade_prices[t].get(stock_id).copied().unwrap_or(1.0);
                        // Wide format: no factor concept, use 1.0
                        tracker.close_trade(&stock_id, t, Some(signal_index), exit_price, 1.0, config.fee_ratio, config.tax_ratio);
                    }
                }

                execute_t1_rebalance(&mut portfolio, &target_weights, &prev_prices, &close_prices[t], config);

                // Open trades for new positions
                for (stock_id, &target_weight) in target_weights.iter().enumerate() {
                    if target_weight != 0.0 && portfolio.positions.contains_key(&stock_id) && !tracker.has_open_trade(&stock_id) {
                        let entry_price = trade_prices[t].get(stock_id).copied().unwrap_or(1.0);
                        // Wide format: no factor concept, use 1.0
                        tracker.open_trade(stock_id, t, signal_index, entry_price, target_weight, 1.0);
                    }
                }

                stopped_stocks = vec![false; n_assets];
            } else {
                update_position_values(&mut portfolio, &close_prices[t], &prev_prices);
            }

            // Execute pending stop exits
            if !pending_stop_exits.is_empty() {
                for &stock_id in &pending_stop_exits {
                    if let Some(pos) = portfolio.positions.remove(&stock_id) {
                        let sell_value = pos.value * (1.0 - config.fee_ratio - config.tax_ratio);
                        portfolio.cash += sell_value;

                        if config.stop_trading_next_period && stock_id < stopped_stocks.len() {
                            stopped_stocks[stock_id] = true;
                        }

                        let exit_price = trade_prices[t].get(stock_id).copied().unwrap_or(1.0);
                        // Wide format: no factor concept, use 1.0
                        tracker.close_trade(&stock_id, t, None, exit_price, 1.0, config.fee_ratio, config.tax_ratio);
                    }
                }
                pending_stop_exits.clear();
            }

            // Detect stops for T+1 execution
            let new_stops = detect_stops(&portfolio.positions, &close_prices[t], config);
            pending_stop_exits.extend(new_stops);
        }

        // Check for rebalance signal
        if rebalance_indices.contains(&t) && weight_idx < weights.len() {
            let target_weights = normalize_weights_finlab(&weights[weight_idx], &stopped_stocks, config.position_limit);
            pending_weights = Some(target_weights);
            pending_signal_index = Some(t);
            weight_idx += 1;
        }

        creturn.push(portfolio.balance());
        prev_prices = close_prices[t].clone();
    }

    // Add pending entries for last day signals
    if let Some(weights) = pending_weights {
        let signal_index = pending_signal_index.unwrap_or(n_times.saturating_sub(1));
        for (stock_id, &weight) in weights.iter().enumerate() {
            if weight > FLOAT_EPSILON && !portfolio.positions.contains_key(&stock_id) {
                tracker.add_pending_entry(stock_id, signal_index, weight);
            }
        }
    }

    creturn
}

/// Run backtest simulation
///
/// # Arguments
/// * `prices` - 2D array of prices [n_times x n_assets]
/// * `signals` - 2D array of signals [n_rebalance_times x n_assets]
///   - `Vec<Vec<bool>>`: Boolean signals, converted to equal weights (fully invested)
///   - `Vec<Vec<f64>>`: Float weights, normalized using Finlab rules (allows partial investment)
/// * `rebalance_indices` - Indices in price array where rebalancing occurs
/// * `config` - Backtest configuration
///
/// # Returns
/// Vector of cumulative returns at each time step
///
/// # Execution Model (T+1, Finlab-compatible)
///
/// This function uses T+1 execution to match Finlab's behavior:
/// - Signal on Day T
/// - Trade executes at Day T+1's close price
/// - Entry fee applied on Day T+1
/// - First price return for new entries starts Day T+2
/// - Existing positions experience Day T→T+1 return on Day T+1
///
/// # Examples
///
/// ```ignore
/// // Using boolean signals (equal weight, fully invested)
/// let signals: Vec<Vec<bool>> = vec![vec![true, false, true]];
/// let result = run_backtest(&prices, &signals, &indices, &config);
///
/// // Using float weights (custom allocation)
/// let weights: Vec<Vec<f64>> = vec![vec![0.5, 0.0, 0.3]];
/// let result = run_backtest(&prices, &weights, &indices, &config);
/// ```
pub fn run_backtest<S: IntoWeights>(
    prices: &[Vec<f64>],
    signals: &[S],
    rebalance_indices: &[usize],
    config: &BacktestConfig,
) -> Vec<f64> {
    // Convert signals to weights (empty stopped_stocks is fine now)
    let weights: Vec<Vec<f64>> = signals
        .iter()
        .map(|s| s.into_weights(&[], config.position_limit))
        .collect();

    // Use NoopIndexTracker for zero-overhead simulation
    let mut tracker = NoopIndexTracker::new();
    simulate_backtest(
        prices,
        prices,
        &weights,
        rebalance_indices,
        config,
        &mut tracker,
        None, // No OHLC data for simple backtest
    )
}

/// Update position values based on price changes
fn update_position_values(
    portfolio: &mut PortfolioState,
    current_prices: &[f64],
    prev_prices: &[f64],
) {
    for (&stock_id, pos) in portfolio.positions.iter_mut() {
        if stock_id >= current_prices.len() || stock_id >= prev_prices.len() {
            continue;
        }

        let prev_price = prev_prices[stock_id];
        let curr_price = current_prices[stock_id];

        if prev_price > 0.0 && curr_price > 0.0 {
            // Update value based on price change
            let return_pct = (curr_price - prev_price) / prev_price;
            pos.value *= 1.0 + return_pct;

            // Update max price for trailing stop
            if curr_price > pos.max_price {
                pos.max_price = curr_price;
            }
        }
    }
}

/// Update entry prices for positions that are recovering from NaN (Finlab mode)
///
/// Two cases are handled:
/// 1. When a stock's previous price was NaN but current price is valid
/// 2. When a position was entered with NaN price (entry_price = 0) and current price is valid
///
/// In both cases, we update entry_price to current price so the position value stays constant.
/// This matches Finlab's behavior: NaN price means 0% return for that stock.
fn update_entry_prices_after_nan(
    portfolio: &mut PortfolioState,
    current_prices: &[f64],
    _prev_prices: &[f64],
) {
    for (&stock_id, pos) in portfolio.positions.iter_mut() {
        if stock_id >= current_prices.len() {
            continue;
        }

        let curr_price = current_prices[stock_id];
        let curr_is_valid = is_valid_price(curr_price);

        if !curr_is_valid {
            continue;
        }

        // Case 1: Position was entered with NaN price (entry_price = 0)
        // Set entry_price to current price so market_value = cost_basis * curr / curr = cost_basis
        if pos.entry_price <= 0.0 {
            pos.entry_price = curr_price;
            pos.stop_entry_price = curr_price;
            pos.max_price = curr_price;
            continue;
        }

        // Case 2: Previous day's price was NaN but current is valid
        // DO NOT update entry_price here!
        // Finlab behavior: previous_price is NOT updated during NaN days
        // So when price recovers, the return includes the "hidden" price change during NaN
        // Our model: balance = pos.value * close_price / entry_price
        // If entry_price stays at pre-NaN value, the calculation correctly includes the return
        //
        // Example:
        // Day 1: price=100, entry_price=100, balance = 1.0 * 100/100 = 1.0
        // Day 2: price=NaN, uses last_market_value = 1.0
        // Day 3: price=120, balance = 1.0 * 120/100 = 1.2 (correctly captures 20% return)
        //
        // If we updated entry_price to 120 on Day 3, balance would be 1.0 * 120/120 = 1.0 (WRONG!)
    }
}

/// Execute rebalance in Finlab mode
///
/// Finlab uses proportional fee allocation:
/// 1. Calculate total rebalance fee cost (sell fee + buy fee)
/// 2. Spread this cost proportionally across all target positions
/// 3. Each position gets: market_value * target_weight - proportional_fee
///
/// This ensures all positions have exactly equal weight after rebalance.
fn execute_finlab_rebalance(
    portfolio: &mut PortfolioState,
    target_weights: &[f64],
    prices: &[f64],
    config: &BacktestConfig,
) {
    // Finlab uses the DIFFERENCE method for fee calculation:
    // - Continuing positions: keep value, pay fee only on SOLD portion
    // - New positions: pay entry fee on full amount
    // - This results in unequal cost_basis and potentially negative cash

    // Step 1: Update all positions to market value using last_market_value
    // Finlab: pos[sid] is updated daily via pos *= r, and we use this cumulative value
    // last_market_value has already been updated in update_max_prices() before this function
    //
    // Note: stop_entry_price handling is done in Step 4 when rebuilding positions,
    // because Step 4 clears and rebuilds all positions. The logic there handles
    // both retain_cost_when_rebalance=True and False cases.
    for (stock_id, pos) in portfolio.positions.iter_mut() {
        if *stock_id < prices.len() {
            let close_price = prices[*stock_id];
            // Use last_market_value for consistency (Finlab: pos[sid] after pos *= r)
            pos.value = pos.last_market_value;
            pos.entry_price = close_price;
        }
    }

    // Step 2: Calculate current market value (balance)
    let balance = portfolio.total_cost_basis();

    // Step 3: Calculate ratio for weight scaling
    let total_target_weight: f64 = target_weights.iter().map(|w| w.abs()).sum();
    if total_target_weight == 0.0 || balance <= 0.0 {
        // Exit all positions
        let all_positions: Vec<usize> = portfolio.positions.keys().copied().collect();
        for stock_id in all_positions {
            if let Some(pos) = portfolio.positions.remove(&stock_id) {
                let sell_value = pos.value - pos.value.abs() * (config.fee_ratio + config.tax_ratio);
                portfolio.cash += sell_value;
            }
        }
        return;
    }

    let ratio = balance / total_target_weight.max(1.0);

    // Step 4: Process each stock using Finlab's set_position logic
    // Store old positions for reference (single snapshot instead of 7 HashMaps)
    let old_snapshots: std::collections::HashMap<usize, PositionSnapshot> = portfolio
        .positions
        .iter()
        .map(|(&k, v)| (k, PositionSnapshot::from(v)))
        .collect();

    // Clear positions, keep track of initial cash, rebuild using Finlab's method
    portfolio.positions.clear();
    // Start with the initial cash from the portfolio (usually 0 for positions,
    // but could be non-zero at the very start or after exits)
    let mut cash = portfolio.cash;

    for (stock_id, &target_weight) in target_weights.iter().enumerate() {
        if stock_id >= prices.len() {
            continue;
        }

        let price = prices[stock_id];
        let price_valid = is_valid_price(price);

        // Target position value (scaled by ratio)
        let target_value = target_weight * ratio;
        let snapshot = old_snapshots.get(&stock_id);
        let current_value = snapshot.map(|s| s.cost_basis).unwrap_or(0.0);

        // Handle NaN price case:
        // Finlab enters positions even when price is NaN!
        // The position value is set, and balance uses the value directly when price is NaN.
        if !price_valid {
            // If target is 0 and we have an old position, sell it using last market value
            if target_weight.abs() < FLOAT_EPSILON {
                if let Some(snap) = snapshot {
                    if snap.market_value.abs() > FLOAT_EPSILON {
                        let sell_fee = snap.market_value.abs() * (config.fee_ratio + config.tax_ratio);
                        cash += snap.market_value - sell_fee;
                    }
                }
                continue;
            }

            // Finlab behavior: Enter/modify position even with NaN price
            // Fees are calculated based on monetary amount difference, not price
            // - entry_price = 0 signals NaN entry, balance_finlab will use last_market_value
            // - When price becomes valid, update_entry_prices_after_nan will set entry_price
            if target_value.abs() > FLOAT_EPSILON {
                // Same fee logic as valid price case
                let amount = target_value - current_value;
                let is_buy = amount > 0.0;
                let is_entry = (target_value >= 0.0 && amount > 0.0) || (target_value <= 0.0 && amount < 0.0);
                let cost = if is_entry {
                    amount.abs() * config.fee_ratio
                } else {
                    amount.abs() * (config.fee_ratio + config.tax_ratio)
                };

                let new_value = if is_buy {
                    cash -= amount;
                    current_value + amount - cost
                } else {
                    let sell_amount = amount.abs();
                    cash += sell_amount - cost;
                    current_value - sell_amount
                };

                portfolio.positions.insert(
                    stock_id,
                    Position {
                        value: new_value,
                        entry_price: 0.0, // Signal that price was NaN at entry
                        stop_entry_price: 0.0,
                        max_price: 0.0,
                        last_market_value: new_value, // Use position value as market value
                        cr: 1.0,
                        maxcr: 1.0,
                        previous_price: 0.0,
                    },
                );
            }
            continue;
        }

        // Calculate trade amount (difference method)
        let amount = target_value - current_value;

        if target_value.abs() < FLOAT_EPSILON {
            // Exit position completely
            if current_value.abs() > FLOAT_EPSILON {
                // Sell all: cash gets net of exit fee
                let sell_fee = current_value.abs() * (config.fee_ratio + config.tax_ratio);
                cash += current_value - sell_fee;
            }
            continue;
        }

        let is_buy = amount > 0.0;
        // is_entry: buying into a long position OR selling into a short position
        let is_entry = (target_value >= 0.0 && amount > 0.0) || (target_value <= 0.0 && amount < 0.0);
        let cost = if is_entry {
            amount.abs() * config.fee_ratio
        } else {
            amount.abs() * (config.fee_ratio + config.tax_ratio)
        };

        let new_position_value;
        if is_buy {
            // Buying: cash decreases by gross amount, position increases by net amount
            cash -= amount;
            new_position_value = current_value + amount - cost;
        } else {
            // Selling: cash increases by net amount, position decreases by gross amount
            let sell_amount = amount.abs();
            cash += sell_amount - cost;
            new_position_value = current_value - sell_amount;
        }

        if new_position_value.abs() > FLOAT_EPSILON {
            // Determine stop_entry_price, cr, maxcr based on retain_cost_when_rebalance
            // Finlab logic (lines 468-478 of restored_backtest_core.pyx):
            // - retain_cost=False: cr.fill(1); maxcr.fill(1); for ALL stocks
            // - retain_cost=True: only reset for NEW positions or DIRECTION CHANGE
            let old_value = snapshot.map(|s| s.cost_basis).unwrap_or(0.0);
            let is_continuing = old_value.abs() > FLOAT_EPSILON && old_value * target_weight > 0.0;

            let (stop_entry, max_price_val, cr_val, maxcr_val, prev_price) =
                if config.retain_cost_when_rebalance && is_continuing {
                    // Preserve old stop tracking for continuing same-direction positions
                    let snap = snapshot.unwrap(); // Safe: is_continuing implies snapshot exists
                    (snap.stop_entry_price, snap.max_price, snap.cr, snap.maxcr, snap.previous_price)
                } else {
                    // New position or direction change or retain_cost=False: reset all
                    (price, price, 1.0, 1.0, price)
                };

            portfolio.positions.insert(
                stock_id,
                Position {
                    value: new_position_value,
                    entry_price: price,
                    stop_entry_price: stop_entry,
                    max_price: max_price_val,
                    last_market_value: new_position_value, // Initialize to cost_basis
                    cr: cr_val,
                    maxcr: maxcr_val,
                    previous_price: prev_price,
                },
            );
        }
    }

    // Step 5: Handle old positions that are OUTSIDE target_weights array
    // (positions within target_weights range are already handled in step 4)
    for (&stock_id, snapshot) in old_snapshots.iter() {
        if stock_id >= target_weights.len() && snapshot.cost_basis.abs() > FLOAT_EPSILON {
            // This position is outside target_weights and should be sold
            let sell_fee = snapshot.cost_basis.abs() * (config.fee_ratio + config.tax_ratio);
            cash += snapshot.cost_basis - sell_fee;
        }
    }

    // Step 6: Store the cash (may be negative in Finlab model)
    portfolio.cash = cash;
}

/// Execute T+1 rebalance with Finlab-compatible sequence
///
/// T+1 execution model (matches Finlab):
/// - Signal on Day T
/// - Execute at Day T+1's close (current_prices)
/// - Existing positions experience Day T→T+1 return
/// - New entries are made at Day T+1's close (no return on entry day, just fee)
/// - First price return for new positions starts Day T+2
fn execute_t1_rebalance(
    portfolio: &mut PortfolioState,
    target_weights: &[f64],
    prev_prices: &[f64],
    current_prices: &[f64],
    config: &BacktestConfig,
) {
    // Step 1: Update existing positions to experience Day T→T+1 return
    update_position_values(portfolio, current_prices, prev_prices);

    // Step 2: Rebalance at current_prices (Day T+1's close)
    // This matches Finlab's behavior where entries happen at execution day's close
    rebalance_to_target_weights(portfolio, target_weights, current_prices, config);
}

/// Rebalance portfolio to target weights at specified prices
///
/// This function:
/// 1. Sells positions that should be closed (target weight = 0)
/// 2. Adjusts existing positions to match target weights
/// 3. Enters new positions
fn rebalance_to_target_weights(
    portfolio: &mut PortfolioState,
    target_weights: &[f64],
    prices: &[f64],
    config: &BacktestConfig,
) {
    // If retain_cost_when_rebalance is false (default), reset all entry prices
    // This matches Finlab's behavior where cr.fill(1) resets all cumulative returns
    if !config.retain_cost_when_rebalance {
        for (stock_id, pos) in portfolio.positions.iter_mut() {
            if *stock_id < prices.len() {
                pos.entry_price = prices[*stock_id];
                pos.max_price = prices[*stock_id];
                pos.cr = 1.0; // Reset cr (Finlab: cr.fill(1))
                pos.maxcr = 1.0; // Reset maxcr (Finlab: maxcr.fill(1))
                pos.previous_price = prices[*stock_id]; // Reset for daily r calculation
            }
        }
    }

    // First, fully exit positions that should be closed
    let positions_to_close: Vec<usize> = portfolio
        .positions
        .keys()
        .filter(|&&id| id < target_weights.len() && target_weights[id] == 0.0)
        .copied()
        .collect();

    for stock_id in positions_to_close {
        if let Some(pos) = portfolio.positions.remove(&stock_id) {
            let sell_value = pos.value * (1.0 - config.fee_ratio - config.tax_ratio);
            portfolio.cash += sell_value;
        }
    }

    // Also close any positions not in target_weights array
    let extra_positions: Vec<usize> = portfolio
        .positions
        .keys()
        .filter(|&&id| id >= target_weights.len())
        .copied()
        .collect();

    for stock_id in extra_positions {
        if let Some(pos) = portfolio.positions.remove(&stock_id) {
            let sell_value = pos.value * (1.0 - config.fee_ratio - config.tax_ratio);
            portfolio.cash += sell_value;
        }
    }

    // Calculate target allocation based on current portfolio value
    // NOTE: DO NOT normalize weights! If total_weight < 1.0, it means partial allocation
    let total_target_weight: f64 = target_weights.iter().sum();
    if total_target_weight == 0.0 {
        return;
    }

    let total_value = portfolio.balance();

    // First pass: sell positions that need to be reduced or closed
    for (stock_id, &target_weight) in target_weights.iter().enumerate() {
        // Use weight directly, not normalized
        let target_value = total_value * target_weight;
        let current_value = portfolio
            .positions
            .get(&stock_id)
            .map(|p| p.value)
            .unwrap_or(0.0);

        let diff = target_value - current_value;
        if diff < 0.0 {
            let sell_amount = -diff;
            if let Some(pos) = portfolio.positions.get_mut(&stock_id) {
                if pos.value >= sell_amount - FLOAT_EPSILON {
                    let sell_value = sell_amount * (1.0 - config.fee_ratio - config.tax_ratio);
                    pos.value -= sell_amount;
                    portfolio.cash += sell_value;

                    // Remove position if value is near zero
                    if pos.value < FLOAT_EPSILON {
                        portfolio.positions.remove(&stock_id);
                    }
                }
            }
        }
    }

    // Second pass: buy positions that need to be increased
    for (stock_id, &target_weight) in target_weights.iter().enumerate() {
        if target_weight == 0.0 {
            continue;
        }

        // Use weight directly, not normalized
        // target_value represents allocation amount (like Finlab)
        let target_allocation = total_value * target_weight;
        let current_value = portfolio
            .positions
            .get(&stock_id)
            .map(|p| p.value)
            .unwrap_or(0.0);

        // How much more allocation is needed
        // Note: We need to figure out how much MORE to spend to reach target position
        // If current_value < target_allocation * (1 - fee), we need to buy more
        let target_position = target_allocation * (1.0 - config.fee_ratio);
        let diff = target_position - current_value;

        if diff > FLOAT_EPSILON {
            // Finlab-style fee calculation:
            // - Spend `amount` from cash
            // - Position value = amount * (1 - fee_ratio)
            // So to get position_diff, we need to spend position_diff / (1 - fee_ratio)
            let spend_needed = diff / (1.0 - config.fee_ratio);
            let actual_spend = spend_needed.min(portfolio.cash);

            if actual_spend > FLOAT_EPSILON {
                // Position value after fee deduction (Finlab style)
                let position_value = actual_spend * (1.0 - config.fee_ratio);
                portfolio.cash -= actual_spend;

                let entry = portfolio.positions.entry(stock_id).or_insert(Position {
                    value: 0.0,
                    entry_price: prices[stock_id],
                    stop_entry_price: prices[stock_id],
                    max_price: prices[stock_id],
                    last_market_value: 0.0,
                    cr: 1.0,
                    maxcr: 1.0,
                    previous_price: prices[stock_id],
                });
                // Update entry price only for new positions
                if entry.value < FLOAT_EPSILON {
                    entry.entry_price = prices[stock_id];
                    entry.stop_entry_price = prices[stock_id];
                    entry.max_price = prices[stock_id];
                    entry.cr = 1.0;
                    entry.maxcr = 1.0;
                    entry.previous_price = prices[stock_id];
                }
                entry.value += position_value;
                entry.last_market_value = entry.value;
            }
        }
    }
}

/// Price data for backtest simulation
///
/// Contains close and trade prices (required), with optional high/low for touched_exit.
#[derive(Debug, Clone)]
pub struct PriceData<'a> {
    /// Close prices (adjusted) for return calculation [n_times x n_assets]
    pub close: &'a [Vec<f64>],
    /// Trade prices (original) for trade records [n_times x n_assets]
    pub trade: &'a [Vec<f64>],
    /// Open prices for touched_exit (optional)
    pub open: Option<&'a [Vec<f64>]>,
    /// High prices for touched_exit (optional)
    pub high: Option<&'a [Vec<f64>]>,
    /// Low prices for touched_exit (optional)
    pub low: Option<&'a [Vec<f64>]>,
}

impl<'a> PriceData<'a> {
    /// Create with close and trade prices only
    pub fn new(close: &'a [Vec<f64>], trade: &'a [Vec<f64>]) -> Self {
        Self {
            close,
            trade,
            open: None,
            high: None,
            low: None,
        }
    }

    /// Create with full OHLC data for touched_exit
    pub fn with_ohlc(
        close: &'a [Vec<f64>],
        trade: &'a [Vec<f64>],
        open: &'a [Vec<f64>],
        high: &'a [Vec<f64>],
        low: &'a [Vec<f64>],
    ) -> Self {
        Self {
            close,
            trade,
            open: Some(open),
            high: Some(high),
            low: Some(low),
        }
    }
}

/// Run backtest with trades tracking
///
/// This function returns both cumulative returns and trade records.
/// It uses:
/// - `prices.close`: Adjusted prices for return calculation (creturn)
/// - `prices.trade`: Original prices for trade records (entry/exit prices)
/// - `prices.high/low`: Optional, for touched_exit support (not yet implemented)
///
/// The trade records match Finlab's trades DataFrame format, using
/// original prices for entry/exit to match real trading execution.
///
/// # Arguments
/// * `prices` - PriceData containing close/trade prices (and optional high/low)
/// * `signals` - 2D array of bool signals or float weights [n_rebalance_times x n_assets]
/// * `rebalance_indices` - Indices in price array where rebalancing occurs
/// * `config` - Backtest configuration
///
/// # Returns
/// WideBacktestResult containing creturn and trades list
pub fn run_backtest_with_trades<S: IntoWeights>(
    prices: &PriceData,
    signals: &[S],
    rebalance_indices: &[usize],
    config: &BacktestConfig,
) -> WideBacktestResult {
    // Convert signals to weights
    let weights: Vec<Vec<f64>> = signals
        .iter()
        .map(|s| s.into_weights(&[], config.position_limit))
        .collect();

    // Build OHLC data if available (for touched_exit mode)
    let ohlc = match (prices.open, prices.high, prices.low) {
        (Some(open), Some(high), Some(low)) => Some(OhlcPrices { open, high, low }),
        _ => None,
    };

    // Use IndexTracker for full trade tracking
    let mut tracker = IndexTracker::new();
    let creturn = simulate_backtest(
        prices.close,
        prices.trade,
        &weights,
        rebalance_indices,
        config,
        &mut tracker,
        ohlc,
    );

    // Finalize trades (close any remaining open positions)
    let mut trades = tracker.finalize(config.fee_ratio, config.tax_ratio);

    // Calculate MAE/MFE for completed trades
    let n_times = prices.close.len();
    let n_assets = if n_times > 0 { prices.close[0].len() } else { 0 };

    for trade in trades.iter_mut() {
        if let (Some(entry_idx), Some(exit_idx)) = (trade.entry_index, trade.exit_index) {
            let stock_id = trade.stock_id;
            if stock_id < n_assets {
                // Extract per-stock price series
                let close_series: Vec<f64> = prices.close.iter().map(|row| row[stock_id]).collect();
                let trade_series: Vec<f64> = prices.trade.iter().map(|row| row[stock_id]).collect();

                // Calculate MAE/MFE at exit
                // For Wide format, position weight > 0 means long
                let is_long = trade.position_weight > 0.0;
                let metrics = calculate_mae_mfe_at_exit(
                    &close_series,
                    &trade_series,
                    entry_idx,
                    exit_idx,
                    is_long,
                    true,  // has_entry_transaction
                    true,  // has_exit_transaction
                    config.fee_ratio,
                    config.tax_ratio,
                );

                trade.mae = Some(metrics.mae);
                trade.gmfe = Some(metrics.gmfe);
                trade.bmfe = Some(metrics.bmfe);
                trade.mdd = Some(metrics.mdd);
                trade.pdays = Some(metrics.pdays);
            }
        }
    }

    WideBacktestResult { creturn, trades }
}

// Dead code removed - the following functions were replaced by simulate_backtest:
// - run_backtest_with_trades_internal
// - close_trades_for_rebalance
// - open_trades_for_rebalance
// - check_stops_with_trade_tracking

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_simple_backtest() {
        // 3 days, 2 stocks
        let prices = vec![
            vec![100.0, 200.0],  // Day 0
            vec![102.0, 198.0],  // Day 1: Stock 0 +2%, Stock 1 -1%
            vec![105.0, 200.0],  // Day 2: Stock 0 +2.9%, Stock 1 +1%
        ];

        // Hold both stocks from day 0
        let signals = vec![
            vec![true, true],
        ];

        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.001425,
            tax_ratio: 0.003,
            ..Default::default()
        };
        let creturn = run_backtest(&prices, &signals, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 3);
        // T+1 mode: Day 0 signal not yet executed = 1.0
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON, "Day 0 should be 1.0, got {}", creturn[0]);
        // Day 1: Executed with entry fee, price movement (+2% - 1%) / 2 = +0.5%, minus fees
        // Result could be above or below 1.0 depending on fee impact
        assert!(creturn[1] > 0.9 && creturn[1] < 1.1, "Day 1 should be reasonable, got {}", creturn[1]);
        assert!(creturn[2] > 0.0);
    }

    #[test]
    fn test_no_positions() {
        let prices = vec![
            vec![100.0, 200.0],
            vec![102.0, 198.0],
        ];

        let signals = vec![
            vec![false, false],
        ];

        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.001425,
            tax_ratio: 0.003,
            ..Default::default()
        };
        let creturn = run_backtest(&prices, &signals, &rebalance_indices, &config);

        // Should stay at 1.0 with no positions
        assert_eq!(creturn.len(), 2);
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON);
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON);
    }

    #[test]
    fn test_rebalancing() {
        // 5 days, 2 stocks
        let prices = vec![
            vec![100.0, 100.0],  // Day 0
            vec![100.0, 100.0],  // Day 1 (rebalance)
            vec![110.0, 90.0],   // Day 2
            vec![110.0, 90.0],   // Day 3 (rebalance)
            vec![120.0, 80.0],   // Day 4
        ];

        // First period: hold both, second period: only stock 0
        let signals = vec![
            vec![true, true],   // Day 0
            vec![true, false],  // Day 3
        ];

        let rebalance_indices = vec![0, 3];

        let config = BacktestConfig {
            fee_ratio: 0.001425,
            tax_ratio: 0.003,
            ..Default::default()
        };
        let creturn = run_backtest(&prices, &signals, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 5);
        // Check that rebalancing occurred
        assert!(creturn[4] > 0.0);
    }

    #[test]
    fn test_stop_loss() {
        let prices = vec![
            vec![100.0],
            vec![95.0],   // -5%
            vec![89.0],   // -11% from entry
            vec![85.0],
        ];

        let signals = vec![vec![true]];
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.001425,
            tax_ratio: 0.003,
            stop_loss: 0.10,  // 10% stop loss
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &signals, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 4);
        // After stop loss triggers, portfolio should stay flat
        // (position was exited on day 2 when loss exceeded 10%)
    }

    #[test]
    fn test_finlab_mode_stop_exit_uses_market_value() {
        // Verify that stop exit in finlab mode uses market value for fee calculation
        // Bug fixed: was using cost_basis instead of market value
        let prices = vec![
            vec![100.0],  // Day 0: signal
            vec![100.0],  // Day 1: entry at 100
            vec![125.0],  // Day 2: +25%, triggers 20% take profit
            vec![130.0],  // Day 3: execute exit (T+1)
            vec![140.0],  // Day 4: should be flat (already exited)
        ];

        let signals = vec![vec![true]];
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.01,  // 1% fee
            tax_ratio: 0.0,
            take_profit: 0.20,
            finlab_mode: true,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &signals, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 5);

        // After exit, portfolio should be flat
        assert!(
            (creturn[4] - creturn[3]).abs() < FLOAT_EPSILON,
            "Portfolio should be flat after stop exit"
        );

        // Final value should reflect profit from 100 -> 130 (exit price) minus fees
        // If bug existed, would be ~0.99 (only cost_basis returned)
        assert!(
            creturn[4] > 1.2,
            "Final value {} should reflect profit (>1.2)",
            creturn[4]
        );
    }

    // Tests for run_backtest with weights

    #[test]
    fn test_backtest_with_weights_basic() {
        // Same as boolean signals test, but using float weights
        let prices = vec![
            vec![100.0, 200.0],  // Day 0
            vec![102.0, 198.0],  // Day 1
            vec![105.0, 200.0],  // Day 2
        ];

        // Equal weights (0.5, 0.5) should match boolean (true, true)
        let weights = vec![vec![0.5, 0.5]];
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.001425,
            tax_ratio: 0.003,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &weights, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 3);
        // T+1 mode: Day 0 signal not yet executed = 1.0
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON, "Day 0 should be 1.0, got {}", creturn[0]);
        // Day 1: Executed with entry fee, price change could offset
        assert!(creturn[1] > 0.9 && creturn[1] < 1.1, "Day 1 should be reasonable, got {}", creturn[1]);
        assert!(creturn[2] > 0.0);
    }

    #[test]
    fn test_backtest_with_weights_unequal() {
        // 70% stock 0, 30% stock 1
        // Finlab-compatible T+1: Signal Day 0 → Execute Day 1 at Day 1's close → Return on Day 2
        let prices = vec![
            vec![100.0, 100.0],  // Day 0: Signal day
            vec![100.0, 100.0],  // Day 1: Entry day (at Day 1's close), no return yet
            vec![110.0, 100.0],  // Day 2: Stock 0 +10%, Stock 1 flat
        ];

        let weights = vec![vec![0.7, 0.3]];
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &weights, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 3);
        // Day 0: Signal not yet executed = 1.0
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON, "Day 0 should be 1.0, got {}", creturn[0]);
        // Day 1: Entry at Day 1's close, no return (same prices) = 1.0
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON, "Day 1 should be 1.0, got {}", creturn[1]);
        // Day 2: Return = 0.7 * 10% + 0.3 * 0% = 7%
        let expected_day2 = 1.0 + 0.07;
        assert!((creturn[2] - expected_day2).abs() < 0.001,
            "Expected {}, got {}", expected_day2, creturn[2]);
    }

    #[test]
    fn test_backtest_with_weights_overweight() {
        // Weights sum to 1.5, should be normalized
        // Finlab-compatible T+1: Signal Day 0 → Execute Day 1 → Return on Day 2
        let prices = vec![
            vec![100.0, 100.0],  // Day 0: Signal day
            vec![100.0, 100.0],  // Day 1: Entry day
            vec![110.0, 100.0],  // Day 2: Stock 0 +10%
        ];

        let weights = vec![vec![1.0, 0.5]]; // sum = 1.5
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &weights, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 3);
        // Day 0 and Day 1: No return yet
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON);
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON);
        // Day 2: Normalized: 1.0/1.5 = 0.667, 0.5/1.5 = 0.333
        // Return: 0.667 * 10% + 0.333 * 0% = 6.67%
        let expected_day2 = 1.0 + (1.0 / 1.5) * 0.10;
        assert!((creturn[2] - expected_day2).abs() < 0.001,
            "Expected {}, got {}", expected_day2, creturn[2]);
    }

    #[test]
    fn test_backtest_with_weights_underweight() {
        // Weights sum to 0.5, should NOT be normalized (partial allocation)
        // Finlab-compatible T+1: Signal Day 0 → Execute Day 1 → Return on Day 2
        let prices = vec![
            vec![100.0, 100.0],  // Day 0: Signal day
            vec![100.0, 100.0],  // Day 1: Entry day
            vec![110.0, 110.0],  // Day 2: Both +10%
        ];

        let weights = vec![vec![0.25, 0.25]]; // sum = 0.5
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &weights, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 3);
        // Day 0 and Day 1: No return yet
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON);
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON);
        // Day 2: Only 50% invested, so return is 0.5 * 10% = 5%
        let expected_day2 = 1.0 + 0.50 * 0.10;
        assert!((creturn[2] - expected_day2).abs() < 0.001,
            "Expected {}, got {}", expected_day2, creturn[2]);
    }

    #[test]
    fn test_backtest_with_weights_position_limit() {
        // Weight of 0.8 should be clipped to position_limit of 0.4
        // Finlab-compatible T+1: Signal Day 0 → Execute Day 1 → Return on Day 2
        let prices = vec![
            vec![100.0, 100.0],  // Day 0: Signal day
            vec![100.0, 100.0],  // Day 1: Entry day
            vec![110.0, 100.0],  // Day 2: Stock 0 +10%
        ];

        let weights = vec![vec![0.8, 0.2]]; // Stock 0 = 0.8, will be clipped to 0.4
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            position_limit: 0.4,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &weights, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 3);
        // Day 0 and Day 1: No return yet
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON);
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON);
        // Day 2: Normalized: 0.8 / 1.0 = 0.8, clipped to 0.4
        //                   0.2 / 1.0 = 0.2, stays
        // Return: 0.4 * 10% + 0.2 * 0% = 4%
        let expected_day2 = 1.0 + 0.04;
        assert!((creturn[2] - expected_day2).abs() < 0.001,
            "Expected {}, got {}", expected_day2, creturn[2]);
    }

    #[test]
    fn test_backtest_with_weights_matches_signals() {
        // run_backtest([0.5, 0.5]) should match run_backtest([true, true])
        let prices = vec![
            vec![100.0, 100.0],
            vec![110.0, 90.0],   // +10%, -10%
            vec![115.0, 85.0],
        ];

        let signals = vec![vec![true, true]];
        let weights = vec![vec![0.5, 0.5]];
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.001425,
            tax_ratio: 0.003,
            ..Default::default()
        };

        let creturn_signals = run_backtest(&prices, &signals, &rebalance_indices, &config);
        let creturn_weights = run_backtest(&prices, &weights, &rebalance_indices, &config);

        assert_eq!(creturn_signals.len(), creturn_weights.len());
        for (cs, cw) in creturn_signals.iter().zip(creturn_weights.iter()) {
            assert!((cs - cw).abs() < FLOAT_EPSILON,
                "Signal result {} != Weight result {}", cs, cw);
        }
    }

    #[test]
    fn test_backtest_with_weights_empty() {
        let prices: Vec<Vec<f64>> = vec![];
        let weights: Vec<Vec<f64>> = vec![];
        let rebalance_indices: Vec<usize> = vec![];

        let config = BacktestConfig::default();
        let creturn = run_backtest(&prices, &weights, &rebalance_indices, &config);

        assert!(creturn.is_empty());
    }

    #[test]
    fn test_backtest_with_weights_all_zero() {
        let prices = vec![
            vec![100.0, 100.0],
            vec![110.0, 110.0],
        ];

        let weights = vec![vec![0.0, 0.0]];
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &weights, &rebalance_indices, &config);

        // With zero weights, should stay at 1.0
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON);
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON);
    }

    // T+1 Execution Mode Tests (default and only mode)
    //
    // All backtests use T+1 execution (Finlab-compatible):
    // - Signal on Day T
    // - Execute at Day T+1's close price
    // - First price return starts Day T+2

    #[test]
    fn test_t1_execution_basic() {
        // Finlab-compatible T+1 execution:
        // Signal Day 0 → Execute Day 1 at Day 1's close → First return on Day 2
        let prices = vec![
            vec![100.0],  // Day 0: signal day
            vec![100.0],  // Day 1: entry day (at Day 1's close)
            vec![105.0],  // Day 2: first return +5%
            vec![110.0],  // Day 3: second return +4.76%
        ];

        let signals = vec![vec![true]];
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &signals, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 4);
        // Day 0: Signal given but not yet executed = 1.0
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON, "Day 0 should be 1.0, got {}", creturn[0]);
        // Day 1: Entry at Day 1's close = 1.0 (no return yet)
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON, "Day 1 should be 1.0, got {}", creturn[1]);
        // Day 2: First return from 100 to 105 = +5%
        let expected_day2 = 1.0 * (105.0 / 100.0);
        assert!((creturn[2] - expected_day2).abs() < FLOAT_EPSILON,
            "Day 2: Expected {}, got {}", expected_day2, creturn[2]);
        // Day 3: From 105 to 110 = +4.76% additional
        let expected_day3 = expected_day2 * (110.0 / 105.0);
        assert!((creturn[3] - expected_day3).abs() < 0.001,
            "Day 3: Expected {}, got {}", expected_day3, creturn[3]);
    }

    #[test]
    fn test_t1_execution_with_fees() {
        // Test that entry fee is applied on T+1 (but entry price is Day T's close)
        let prices = vec![
            vec![100.0],  // Day 0: signal day, entry price
            vec![100.0],  // Day 1: execute with fee, flat price
            vec![100.0],  // Day 2: flat price
        ];

        let signals = vec![vec![true]];
        let rebalance_indices = vec![0];

        let fee_ratio = 0.001425;

        let config = BacktestConfig {
            fee_ratio,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &signals, &rebalance_indices, &config);

        // Day 0: No trade yet = 1.0
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON, "Day 0 should be 1.0, got {}", creturn[0]);

        // Day 1: Entry fee applied. Finlab-style: value = 1 * (1 - fee_ratio)
        let expected_day1 = 1.0 * (1.0 - fee_ratio);
        assert!((creturn[1] - expected_day1).abs() < 1e-6,
            "Day 1: Expected {}, got {}", expected_day1, creturn[1]);
    }

    #[test]
    fn test_t1_execution_weights() {
        // Finlab-compatible T+1 mode with custom weights:
        // Signal Day 0 → Execute Day 1 → First return on Day 2
        let prices = vec![
            vec![100.0, 100.0],  // Day 0: signal day
            vec![100.0, 100.0],  // Day 1: entry day
            vec![110.0, 100.0],  // Day 2: stock 0 +10%
            vec![120.0, 100.0],  // Day 3: stock 0 +9.1% more
        ];

        let weights = vec![vec![0.5, 0.5]];  // Equal weight
        let rebalance_indices = vec![0];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &weights, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 4);
        // Day 0 and Day 1: Signal and entry, no return yet = 1.0
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON);
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON);
        // Day 2: stock 0 rose 10%, 50% weight = +5%
        let expected_day2 = 1.0 + 0.5 * 0.10;
        assert!((creturn[2] - expected_day2).abs() < 0.001,
            "Day 2: Expected {}, got {}", expected_day2, creturn[2]);
    }

    #[test]
    fn test_t1_multiple_rebalances() {
        // Finlab-compatible T+1 mode with multiple rebalance points:
        // Signal 0 on Day 0 → Execute Day 1 at Day 1's close → Return on Day 2
        // Signal 1 on Day 1 → Execute Day 2 at Day 2's close → Return on Day 3
        let prices = vec![
            vec![100.0, 100.0],  // Day 0: signal 1 (stock 0)
            vec![100.0, 100.0],  // Day 1: execute signal 1, signal 2 (switch to stock 1)
            vec![110.0, 100.0],  // Day 2: stock 0 +10%, execute signal 2
            vec![110.0, 100.0],  // Day 3: stock 0 sold, stock 1 bought
            vec![110.0, 110.0],  // Day 4: stock 1 +10%
        ];

        // Signal 1: stock 0 only
        // Signal 2: stock 1 only (switch from stock 0 to stock 1)
        let signals = vec![
            vec![true, false],  // Day 0
            vec![false, true],  // Day 1
        ];
        let rebalance_indices = vec![0, 1];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let creturn = run_backtest(&prices, &signals, &rebalance_indices, &config);

        assert_eq!(creturn.len(), 5);
        // Day 0: Signal, not executed = 1.0
        assert!((creturn[0] - 1.0).abs() < FLOAT_EPSILON, "Day 0 should be 1.0, got {}", creturn[0]);
        // Day 1: Entry at Day 1's close, no return yet = 1.0
        assert!((creturn[1] - 1.0).abs() < FLOAT_EPSILON, "Day 1 should be 1.0, got {}", creturn[1]);
        // Day 2: stock 0 +10%, then switch signal executes at Day 2's close
        assert!((creturn[2] - 1.10).abs() < 0.001, "Day 2: Expected 1.10, got {}", creturn[2]);
        // Day 3: Stock 0 sold at Day 2's close (1.10), stock 1 bought at Day 3's close
        // Value should be ~1.10 (no return on switch day)
        assert!((creturn[3] - 1.10).abs() < 0.01, "Day 3 should be ~1.10, got {}", creturn[3]);
        // Day 4: In stock 1, gains from 100 to 110 = +10% on 1.10 = 1.21
        assert!((creturn[4] - 1.21).abs() < 0.01, "Day 4: Expected ~1.21, got {}", creturn[4]);
    }

    #[test]
    fn test_mae_mfe_calculation() {
        // Test MAE/MFE metrics calculation in trades
        // Stock price goes: 100 -> 95 -> 105 -> 110
        // Entry at 100 (T+1), experiences 5% drawdown then 10% profit
        let prices = vec![
            vec![100.0],  // Day 0: signal
            vec![100.0],  // Day 1: entry at close
            vec![95.0],   // Day 2: -5% from entry (MAE point)
            vec![105.0],  // Day 3: +5%
            vec![110.0],  // Day 4: exit at +10% (via rebalance)
        ];

        let signals = vec![
            vec![true],   // Day 0: enter
            vec![false],  // Day 3: exit signal (executed Day 4)
        ];
        let rebalance_indices = vec![0, 3];

        let config = BacktestConfig {
            fee_ratio: 0.0,
            tax_ratio: 0.0,
            ..Default::default()
        };

        let price_data = PriceData {
            close: &prices,
            trade: &prices,
            open: None,
            high: None,
            low: None,
        };

        let result = run_backtest_with_trades(&price_data, &signals, &rebalance_indices, &config);

        // Should have 1 completed trade
        let completed: Vec<_> = result.trades.iter()
            .filter(|t| t.entry_index.is_some() && t.exit_index.is_some())
            .collect();
        assert_eq!(completed.len(), 1, "Should have 1 completed trade");

        let trade = &completed[0];

        // Verify MAE/MFE are calculated
        assert!(trade.mae.is_some(), "MAE should be calculated");
        assert!(trade.gmfe.is_some(), "GMFE should be calculated");
        assert!(trade.mdd.is_some(), "MDD should be calculated");
        assert!(trade.pdays.is_some(), "pdays should be calculated");

        // MAE should be negative (the -5% drop)
        let mae = trade.mae.unwrap();
        assert!(mae < 0.0, "MAE should be negative, got {}", mae);
        assert!((mae - (-0.05)).abs() < 0.01, "MAE should be around -0.05, got {}", mae);

        // GMFE should be positive (the +10% peak)
        let gmfe = trade.gmfe.unwrap();
        assert!(gmfe > 0.0, "GMFE should be positive, got {}", gmfe);
        assert!(gmfe > 0.05, "GMFE should be at least 0.05, got {}", gmfe);
    }
}
