"""
ML Backtesting Engine for model validation.

This module provides a comprehensive backtesting engine specifically designed
for validating machine learning models in trading contexts. It supports:

- Individual strategy testing
- Ensemble model testing
- Walk-forward validation
- Configurable transaction costs (0.5 pips forex, 0.1% crypto)
- Kelly criterion position sizing (max 2%)
- Complete trade history with entry/exit timestamps
"""

import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Any, Callable, Union, Protocol
from loguru import logger
from abc import ABC, abstractmethod
import json
from pathlib import Path

from .trade import Trade, TradeDirection, TradeStatus
from .position_manager import PositionManager, PositionSizingConfig


class Strategy(Protocol):
    """Protocol defining the interface for trading strategies."""

    def generate_signal(
        self,
        data: pd.DataFrame,
        index: int
    ) -> Optional[Dict[str, Any]]:
        """
        Generate a trading signal for the current bar.

        Args:
            data: Full OHLCV dataframe
            index: Current bar index

        Returns:
            Signal dictionary with keys:
            - direction: 'long' or 'short'
            - confidence: float 0-1
            - stop_loss: price level
            - take_profit: price level
            Or None if no signal
        """
        ...


@dataclass
class BacktestConfig:
    """
    Configuration for backtesting.

    Attributes:
        start_date: Start date for backtest
        end_date: End date for backtest
        initial_capital: Starting capital amount
        commission_type: Type of commission ('pips', 'percent', 'fixed')
        commission_value: Commission value based on type
        slippage_type: Type of slippage ('pips', 'percent', 'fixed')
        slippage_value: Slippage value based on type
        position_sizing: Position sizing method ('kelly', 'fixed', 'fixed_risk')
        max_position_pct: Maximum position as percentage of capital (default 2%)
        kelly_fraction: Fraction of Kelly to use (default 0.25)
        max_concurrent_positions: Maximum positions at once
        min_confidence: Minimum signal confidence to trade
        enable_shorting: Whether short positions are allowed
        asset_type: Type of asset ('forex', 'crypto', 'stock')
        timeframe: Trading timeframe (e.g., '1H', '4H', '1D')
        risk_free_rate: Annual risk-free rate for Sharpe calculation
    """

    start_date: Optional[datetime] = None
    end_date: Optional[datetime] = None
    initial_capital: float = 10000.0

    commission_type: str = "pips"
    commission_value: float = 0.5

    slippage_type: str = "pips"
    slippage_value: float = 0.2

    position_sizing: str = "kelly"
    max_position_pct: float = 0.02
    kelly_fraction: float = 0.25
    risk_per_trade: float = 0.02

    max_concurrent_positions: int = 3
    min_confidence: float = 0.55
    enable_shorting: bool = True

    asset_type: str = "forex"
    timeframe: str = "1H"
    risk_free_rate: float = 0.02

    walk_forward_splits: int = 5
    walk_forward_train_pct: float = 0.7
    walk_forward_gap: int = 0

    def get_commission(self, price: float, size: float) -> float:
        """Calculate commission for a trade."""
        if self.commission_type == "pips":
            pip_value = 0.0001 if self.asset_type == "forex" else 1.0
            return self.commission_value * pip_value * size
        elif self.commission_type == "percent":
            return price * size * (self.commission_value / 100)
        else:
            return self.commission_value

    def get_slippage(self, price: float) -> float:
        """Calculate slippage for a trade."""
        if self.slippage_type == "pips":
            pip_value = 0.0001 if self.asset_type == "forex" else 1.0
            return self.slippage_value * pip_value
        elif self.slippage_type == "percent":
            return price * (self.slippage_value / 100)
        else:
            return self.slippage_value

    @classmethod
    def for_forex(cls, **kwargs) -> "BacktestConfig":
        """Create config optimized for forex trading."""
        defaults = {
            "asset_type": "forex",
            "commission_type": "pips",
            "commission_value": 0.5,
            "slippage_type": "pips",
            "slippage_value": 0.2,
        }
        defaults.update(kwargs)
        return cls(**defaults)

    @classmethod
    def for_crypto(cls, **kwargs) -> "BacktestConfig":
        """Create config optimized for crypto trading."""
        defaults = {
            "asset_type": "crypto",
            "commission_type": "percent",
            "commission_value": 0.1,
            "slippage_type": "percent",
            "slippage_value": 0.05,
        }
        defaults.update(kwargs)
        return cls(**defaults)


@dataclass
class BacktestMetrics:
    """
    Comprehensive metrics for backtest results.

    Contains all standard trading metrics plus ML-specific metrics.
    """

    total_trades: int = 0
    winning_trades: int = 0
    losing_trades: int = 0
    win_rate: float = 0.0

    total_profit: float = 0.0
    total_profit_pct: float = 0.0
    gross_profit: float = 0.0
    gross_loss: float = 0.0

    profit_factor: float = 0.0
    avg_win: float = 0.0
    avg_loss: float = 0.0
    avg_trade: float = 0.0
    expectancy: float = 0.0

    best_trade: float = 0.0
    worst_trade: float = 0.0
    avg_trade_duration_minutes: float = 0.0

    max_drawdown: float = 0.0
    max_drawdown_pct: float = 0.0
    max_drawdown_duration_days: float = 0.0

    sharpe_ratio: float = 0.0
    sortino_ratio: float = 0.0
    calmar_ratio: float = 0.0

    max_consecutive_wins: int = 0
    max_consecutive_losses: int = 0

    long_trades: int = 0
    short_trades: int = 0
    long_win_rate: float = 0.0
    short_win_rate: float = 0.0

    total_commission: float = 0.0
    total_slippage: float = 0.0

    def to_dict(self) -> Dict[str, Any]:
        """Convert metrics to dictionary."""
        return {
            "total_trades": self.total_trades,
            "winning_trades": self.winning_trades,
            "losing_trades": self.losing_trades,
            "win_rate": self.win_rate,
            "total_profit": self.total_profit,
            "total_profit_pct": self.total_profit_pct,
            "gross_profit": self.gross_profit,
            "gross_loss": self.gross_loss,
            "profit_factor": self.profit_factor,
            "avg_win": self.avg_win,
            "avg_loss": self.avg_loss,
            "avg_trade": self.avg_trade,
            "expectancy": self.expectancy,
            "best_trade": self.best_trade,
            "worst_trade": self.worst_trade,
            "avg_trade_duration_minutes": self.avg_trade_duration_minutes,
            "max_drawdown": self.max_drawdown,
            "max_drawdown_pct": self.max_drawdown_pct,
            "max_drawdown_duration_days": self.max_drawdown_duration_days,
            "sharpe_ratio": self.sharpe_ratio,
            "sortino_ratio": self.sortino_ratio,
            "calmar_ratio": self.calmar_ratio,
            "max_consecutive_wins": self.max_consecutive_wins,
            "max_consecutive_losses": self.max_consecutive_losses,
            "long_trades": self.long_trades,
            "short_trades": self.short_trades,
            "long_win_rate": self.long_win_rate,
            "short_win_rate": self.short_win_rate,
            "total_commission": self.total_commission,
            "total_slippage": self.total_slippage
        }


@dataclass
class WalkForwardPeriod:
    """
    Represents a single walk-forward validation period.

    Attributes:
        period_id: Unique identifier for the period
        train_start: Start index of training period
        train_end: End index of training period
        test_start: Start index of test period
        test_end: End index of test period
    """

    period_id: int
    train_start: int
    train_end: int
    test_start: int
    test_end: int

    @property
    def train_size(self) -> int:
        return self.train_end - self.train_start

    @property
    def test_size(self) -> int:
        return self.test_end - self.test_start


@dataclass
class BacktestResult:
    """
    Complete results from a backtest run.

    Attributes:
        config: Backtest configuration used
        metrics: Performance metrics
        trades: List of all trades executed
        equity_curve: Series of equity values over time
        drawdown_curve: Series of drawdown values over time
        daily_returns: Daily return series
        signals_generated: Total signals generated by strategy
        signals_traded: Signals that resulted in trades
        start_time: Backtest start timestamp
        end_time: Backtest end timestamp
    """

    config: BacktestConfig
    metrics: BacktestMetrics
    trades: List[Trade]
    equity_curve: pd.Series
    drawdown_curve: pd.Series
    daily_returns: pd.Series

    signals_generated: int = 0
    signals_traded: int = 0
    signals_filtered: int = 0

    start_time: Optional[datetime] = None
    end_time: Optional[datetime] = None

    walk_forward_results: Optional[List["BacktestResult"]] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert result to dictionary."""
        return {
            "metrics": self.metrics.to_dict(),
            "signals_generated": self.signals_generated,
            "signals_traded": self.signals_traded,
            "signals_filtered": self.signals_filtered,
            "start_time": self.start_time.isoformat() if self.start_time else None,
            "end_time": self.end_time.isoformat() if self.end_time else None,
            "num_trades": len(self.trades),
            "final_equity": float(self.equity_curve.iloc[-1]) if len(self.equity_curve) > 0 else 0,
            "config": {
                "initial_capital": self.config.initial_capital,
                "asset_type": self.config.asset_type,
                "position_sizing": self.config.position_sizing,
                "max_position_pct": self.config.max_position_pct
            }
        }

    def save_report(self, filepath: str) -> None:
        """Save detailed report to JSON file."""
        report = {
            "summary": self.to_dict(),
            "trades": [t.to_dict() for t in self.trades],
            "equity_curve": self.equity_curve.tolist(),
            "drawdown_curve": self.drawdown_curve.tolist(),
            "daily_returns": self.daily_returns.tolist() if self.daily_returns is not None else []
        }

        path = Path(filepath)
        path.parent.mkdir(parents=True, exist_ok=True)

        with open(path, "w") as f:
            json.dump(report, f, indent=2, default=str)

        logger.info(f"Saved backtest report to {filepath}")

    def print_summary(self) -> None:
        """Print formatted summary of results."""
        m = self.metrics
        print("\n" + "=" * 60)
        print("BACKTEST RESULTS SUMMARY")
        print("=" * 60)

        print(f"\nPerformance:")
        print(f"  Total Trades: {m.total_trades}")
        print(f"  Win Rate: {m.win_rate:.1%}")
        print(f"  Profit Factor: {m.profit_factor:.2f}")
        print(f"  Total Profit: ${m.total_profit:,.2f} ({m.total_profit_pct:.1f}%)")

        print(f"\nRisk Metrics:")
        print(f"  Max Drawdown: {m.max_drawdown_pct:.1%}")
        print(f"  Sharpe Ratio: {m.sharpe_ratio:.2f}")
        print(f"  Sortino Ratio: {m.sortino_ratio:.2f}")

        print(f"\nTrade Analysis:")
        print(f"  Avg Win: ${m.avg_win:,.2f}")
        print(f"  Avg Loss: ${m.avg_loss:,.2f}")
        print(f"  Best Trade: ${m.best_trade:,.2f}")
        print(f"  Worst Trade: ${m.worst_trade:,.2f}")
        print(f"  Expectancy: ${m.expectancy:,.2f}")

        print(f"\nTransaction Costs:")
        print(f"  Total Commission: ${m.total_commission:,.2f}")
        print(f"  Total Slippage: ${m.total_slippage:,.2f}")

        print("=" * 60 + "\n")


class BacktestEngine:
    """
    Main backtesting engine for ML model validation.

    This engine provides comprehensive backtesting capabilities including:
    - Individual strategy testing
    - Ensemble model testing
    - Walk-forward validation
    - Transaction cost modeling
    - Kelly criterion position sizing

    Example usage:
        ```python
        config = BacktestConfig.for_forex(
            initial_capital=10000,
            position_sizing='kelly',
            max_position_pct=0.02
        )

        engine = BacktestEngine(config)
        result = engine.run_backtest(strategy, data)
        result.print_summary()
        ```
    """

    def __init__(self, config: Optional[BacktestConfig] = None):
        """
        Initialize the backtesting engine.

        Args:
            config: Backtest configuration
        """
        self.config = config or BacktestConfig()

        sizing_config = PositionSizingConfig(
            method=self.config.position_sizing,
            max_position_pct=self.config.max_position_pct,
            kelly_fraction=self.config.kelly_fraction,
            risk_per_trade_pct=self.config.risk_per_trade,
        )

        self.position_manager = PositionManager(
            initial_capital=self.config.initial_capital,
            sizing_config=sizing_config,
            max_positions=self.config.max_concurrent_positions
        )

        self.equity_history: List[Tuple[datetime, float]] = []
        self.signals_generated = 0
        self.signals_traded = 0
        self.signals_filtered = 0

        logger.info(
            f"BacktestEngine initialized: capital=${self.config.initial_capital:,.2f}, "
            f"sizing={self.config.position_sizing}, max_pos={self.config.max_position_pct:.1%}"
        )

    def reset(self) -> None:
        """Reset engine to initial state."""
        self.position_manager.reset()
        self.equity_history = []
        self.signals_generated = 0
        self.signals_traded = 0
        self.signals_filtered = 0

    def run_backtest(
        self,
        strategy: Union[Strategy, Callable],
        data: pd.DataFrame,
        symbol: str = "DEFAULT"
    ) -> BacktestResult:
        """
        Run a backtest with the given strategy and data.

        Args:
            strategy: Trading strategy implementing generate_signal method
                     or a callable that takes (data, index) and returns signal dict
            data: OHLCV DataFrame with datetime index and columns:
                 ['open', 'high', 'low', 'close', 'volume']
            symbol: Trading symbol name

        Returns:
            BacktestResult with complete performance metrics and trade history
        """
        self.reset()

        if self.config.start_date:
            data = data[data.index >= self.config.start_date]
        if self.config.end_date:
            data = data[data.index <= self.config.end_date]

        if len(data) < 10:
            raise ValueError("Insufficient data for backtesting")

        logger.info(f"Starting backtest on {len(data)} bars for {symbol}")

        start_time = data.index[0]
        initial_equity = self.config.initial_capital
        self.equity_history.append((start_time, initial_equity))

        for i in range(len(data)):
            current_time = data.index[i]
            current_bar = data.iloc[i]
            current_price = current_bar["close"]

            self._update_positions(data, i, current_time, symbol)

            if i < len(data) - 1:
                signal = self._get_signal(strategy, data, i)

                if signal is not None:
                    self.signals_generated += 1
                    self._process_signal(
                        signal, data, i, current_time, current_price, symbol
                    )

            equity = self.position_manager.get_equity({symbol: current_price})
            self.equity_history.append((current_time, equity))

        self._close_all_positions(data, symbol)
        end_time = data.index[-1]

        result = self._generate_result(start_time, end_time)
        logger.info(
            f"Backtest complete: {len(self.position_manager.closed_positions)} trades, "
            f"Net P&L: ${result.metrics.total_profit:,.2f}"
        )

        return result

    def run_backtest_with_signals(
        self,
        signals: pd.DataFrame,
        data: pd.DataFrame,
        symbol: str = "DEFAULT"
    ) -> BacktestResult:
        """
        Run backtest with pre-generated signals DataFrame.

        This is useful for testing ML model predictions directly.

        Args:
            signals: DataFrame with signal information including:
                    - direction: 'long' or 'short'
                    - confidence: float 0-1
                    - stop_loss: price level (optional)
                    - take_profit: price level (optional)
            data: OHLCV DataFrame
            symbol: Trading symbol

        Returns:
            BacktestResult
        """
        self.reset()

        common_idx = data.index.intersection(signals.index)
        data = data.loc[common_idx]
        signals = signals.loc[common_idx]

        if len(data) < 10:
            raise ValueError("Insufficient data for backtesting")

        logger.info(f"Starting signal-based backtest on {len(data)} bars")

        start_time = data.index[0]
        self.equity_history.append((start_time, self.config.initial_capital))

        for i in range(len(data)):
            current_time = data.index[i]
            current_bar = data.iloc[i]
            current_price = current_bar["close"]

            self._update_positions(data, i, current_time, symbol)

            if current_time in signals.index:
                signal_row = signals.loc[current_time]

                if pd.notna(signal_row.get("direction")):
                    signal = {
                        "direction": signal_row["direction"],
                        "confidence": signal_row.get("confidence", 0.6),
                        "stop_loss": signal_row.get("stop_loss"),
                        "take_profit": signal_row.get("take_profit")
                    }
                    self.signals_generated += 1
                    self._process_signal(
                        signal, data, i, current_time, current_price, symbol
                    )

            equity = self.position_manager.get_equity({symbol: current_price})
            self.equity_history.append((current_time, equity))

        self._close_all_positions(data, symbol)
        end_time = data.index[-1]

        return self._generate_result(start_time, end_time)

    def run_ensemble_backtest(
        self,
        strategies: List[Union[Strategy, Callable]],
        data: pd.DataFrame,
        symbol: str = "DEFAULT",
        combination_method: str = "majority_vote",
        weights: Optional[List[float]] = None
    ) -> BacktestResult:
        """
        Run backtest with an ensemble of strategies.

        Args:
            strategies: List of trading strategies
            data: OHLCV DataFrame
            symbol: Trading symbol
            combination_method: How to combine signals:
                              'majority_vote' - trade when majority agree
                              'weighted_avg' - weighted confidence average
                              'unanimous' - all strategies must agree
            weights: Weights for each strategy (for weighted_avg)

        Returns:
            BacktestResult
        """
        self.reset()

        if weights is None:
            weights = [1.0 / len(strategies)] * len(strategies)

        logger.info(
            f"Starting ensemble backtest with {len(strategies)} strategies, "
            f"method={combination_method}"
        )

        start_time = data.index[0]
        self.equity_history.append((start_time, self.config.initial_capital))

        for i in range(len(data)):
            current_time = data.index[i]
            current_bar = data.iloc[i]
            current_price = current_bar["close"]

            self._update_positions(data, i, current_time, symbol)

            if i < len(data) - 1:
                signals = []
                for strategy in strategies:
                    signal = self._get_signal(strategy, data, i)
                    signals.append(signal)

                combined_signal = self._combine_signals(
                    signals, combination_method, weights
                )

                if combined_signal is not None:
                    self.signals_generated += 1
                    self._process_signal(
                        combined_signal, data, i, current_time, current_price, symbol
                    )

            equity = self.position_manager.get_equity({symbol: current_price})
            self.equity_history.append((current_time, equity))

        self._close_all_positions(data, symbol)
        end_time = data.index[-1]

        return self._generate_result(start_time, end_time)

    def run_walk_forward_validation(
        self,
        strategy_factory: Callable[[pd.DataFrame], Union[Strategy, Callable]],
        data: pd.DataFrame,
        symbol: str = "DEFAULT"
    ) -> BacktestResult:
        """
        Run walk-forward validation.

        This method trains a new strategy instance on each training period
        and tests it on the subsequent out-of-sample period.

        Args:
            strategy_factory: Function that takes training data and returns
                            a trained strategy
            data: Full OHLCV DataFrame
            symbol: Trading symbol

        Returns:
            BacktestResult with combined results and individual period results
        """
        n_splits = self.config.walk_forward_splits
        train_pct = self.config.walk_forward_train_pct
        gap = self.config.walk_forward_gap

        periods = self._create_walk_forward_periods(len(data), n_splits, train_pct, gap)

        logger.info(f"Starting walk-forward validation with {len(periods)} periods")

        period_results = []
        all_trades = []

        for period in periods:
            logger.info(
                f"Period {period.period_id}: "
                f"Train [{period.train_start}:{period.train_end}], "
                f"Test [{period.test_start}:{period.test_end}]"
            )

            train_data = data.iloc[period.train_start:period.train_end]
            test_data = data.iloc[period.test_start:period.test_end]

            strategy = strategy_factory(train_data)

            self.reset()
            result = self.run_backtest(strategy, test_data, symbol)

            period_results.append(result)
            all_trades.extend(result.trades)

        combined_result = self._combine_walk_forward_results(
            period_results, all_trades, data.index[0], data.index[-1]
        )
        combined_result.walk_forward_results = period_results

        return combined_result

    def _get_signal(
        self,
        strategy: Union[Strategy, Callable],
        data: pd.DataFrame,
        index: int
    ) -> Optional[Dict[str, Any]]:
        """Get signal from strategy."""
        try:
            if hasattr(strategy, "generate_signal"):
                return strategy.generate_signal(data, index)
            else:
                return strategy(data, index)
        except Exception as e:
            logger.warning(f"Error generating signal at index {index}: {e}")
            return None

    def _process_signal(
        self,
        signal: Dict[str, Any],
        data: pd.DataFrame,
        index: int,
        current_time: datetime,
        current_price: float,
        symbol: str
    ) -> None:
        """Process a trading signal."""
        direction_str = signal.get("direction", "").lower()
        confidence = signal.get("confidence", 0.0)
        stop_loss = signal.get("stop_loss")
        take_profit = signal.get("take_profit")

        if confidence < self.config.min_confidence:
            self.signals_filtered += 1
            return

        if direction_str == "short" and not self.config.enable_shorting:
            self.signals_filtered += 1
            return

        if not self.position_manager.can_open_position():
            self.signals_filtered += 1
            return

        try:
            direction = TradeDirection.from_string(direction_str)
        except ValueError:
            self.signals_filtered += 1
            return

        commission = self.config.get_commission(current_price, 1.0)
        slippage = self.config.get_slippage(current_price)

        trade = self.position_manager.open_position(
            symbol=symbol,
            direction=direction,
            entry_price=current_price,
            entry_time=current_time,
            stop_loss=stop_loss,
            take_profit=take_profit,
            commission=commission,
            slippage=slippage,
            signal_confidence=confidence,
            timeframe=self.config.timeframe,
            metadata=signal.get("metadata", {})
        )

        if trade is not None:
            self.signals_traded += 1

    def _update_positions(
        self,
        data: pd.DataFrame,
        index: int,
        current_time: datetime,
        symbol: str
    ) -> None:
        """Update open positions with current bar data."""
        if index >= len(data):
            return

        bar = data.iloc[index]
        high = bar["high"]
        low = bar["low"]
        close = bar["close"]

        for trade in self.position_manager.open_positions[:]:
            if trade.symbol != symbol:
                continue

            commission = self.config.get_commission(close, trade.size)
            slippage = self.config.get_slippage(close)

            self.position_manager.update_position_with_ohlc(
                trade=trade,
                high=high,
                low=low,
                close=close,
                current_time=current_time,
                commission_rate=0,
                slippage=slippage
            )

    def _close_all_positions(self, data: pd.DataFrame, symbol: str) -> None:
        """Close all remaining open positions at end of backtest."""
        if len(data) == 0:
            return

        last_bar = data.iloc[-1]
        last_time = data.index[-1]
        close_price = last_bar["close"]

        for trade in self.position_manager.open_positions[:]:
            if trade.symbol == symbol:
                commission = self.config.get_commission(close_price, trade.size)
                slippage = self.config.get_slippage(close_price)

                self.position_manager.close_position(
                    trade=trade,
                    exit_price=close_price,
                    exit_time=last_time,
                    status=TradeStatus.TIMEOUT,
                    commission=commission,
                    slippage=slippage
                )

    def _combine_signals(
        self,
        signals: List[Optional[Dict[str, Any]]],
        method: str,
        weights: List[float]
    ) -> Optional[Dict[str, Any]]:
        """Combine signals from multiple strategies."""
        valid_signals = [(s, w) for s, w in zip(signals, weights) if s is not None]

        if not valid_signals:
            return None

        if method == "majority_vote":
            long_votes = sum(
                1 for s, _ in valid_signals if s.get("direction", "").lower() == "long"
            )
            short_votes = sum(
                1 for s, _ in valid_signals if s.get("direction", "").lower() == "short"
            )

            threshold = len(valid_signals) / 2

            if long_votes > threshold:
                direction = "long"
            elif short_votes > threshold:
                direction = "short"
            else:
                return None

            avg_confidence = np.mean([s.get("confidence", 0.5) for s, _ in valid_signals])

            return {
                "direction": direction,
                "confidence": avg_confidence,
                "stop_loss": valid_signals[0][0].get("stop_loss"),
                "take_profit": valid_signals[0][0].get("take_profit")
            }

        elif method == "weighted_avg":
            long_score = sum(
                w * s.get("confidence", 0.5)
                for s, w in valid_signals
                if s.get("direction", "").lower() == "long"
            )
            short_score = sum(
                w * s.get("confidence", 0.5)
                for s, w in valid_signals
                if s.get("direction", "").lower() == "short"
            )

            total_weight = sum(w for _, w in valid_signals)

            if long_score > short_score and long_score / total_weight > 0.5:
                direction = "long"
                confidence = long_score / total_weight
            elif short_score > long_score and short_score / total_weight > 0.5:
                direction = "short"
                confidence = short_score / total_weight
            else:
                return None

            return {
                "direction": direction,
                "confidence": confidence,
                "stop_loss": valid_signals[0][0].get("stop_loss"),
                "take_profit": valid_signals[0][0].get("take_profit")
            }

        elif method == "unanimous":
            if len(valid_signals) != len(signals):
                return None

            directions = [s.get("direction", "").lower() for s, _ in valid_signals]

            if len(set(directions)) != 1:
                return None

            avg_confidence = np.mean([s.get("confidence", 0.5) for s, _ in valid_signals])

            return {
                "direction": directions[0],
                "confidence": avg_confidence,
                "stop_loss": valid_signals[0][0].get("stop_loss"),
                "take_profit": valid_signals[0][0].get("take_profit")
            }

        return None

    def _create_walk_forward_periods(
        self,
        n_samples: int,
        n_splits: int,
        train_pct: float,
        gap: int
    ) -> List[WalkForwardPeriod]:
        """Create walk-forward validation periods."""
        periods = []
        step_size = n_samples // (n_splits + 1)

        for i in range(n_splits):
            train_start = 0 if i == 0 else (i * step_size // 2)
            train_end = (i + 1) * step_size
            test_start = train_end + gap
            test_end = min(test_start + int(step_size * (1 - train_pct)), n_samples)

            if test_end > n_samples or (train_end - train_start) < 100:
                continue

            periods.append(WalkForwardPeriod(
                period_id=i + 1,
                train_start=train_start,
                train_end=train_end,
                test_start=test_start,
                test_end=test_end
            ))

        return periods

    def _combine_walk_forward_results(
        self,
        results: List[BacktestResult],
        all_trades: List[Trade],
        start_time: datetime,
        end_time: datetime
    ) -> BacktestResult:
        """Combine results from all walk-forward periods."""
        equity_values = [self.config.initial_capital]
        equity_times = [start_time]

        for result in results:
            if len(result.equity_curve) > 0:
                scale_factor = equity_values[-1] / result.equity_curve.iloc[0]
                scaled_equity = result.equity_curve * scale_factor
                equity_times.extend(scaled_equity.index.tolist())
                equity_values.extend(scaled_equity.values.tolist())

        equity_curve = pd.Series(equity_values, index=equity_times)
        equity_curve = equity_curve[~equity_curve.index.duplicated(keep="last")]

        metrics = self._calculate_metrics(all_trades, equity_curve)

        drawdown_curve = self._calculate_drawdown(equity_curve)
        daily_returns = equity_curve.resample("D").last().pct_change().dropna()

        return BacktestResult(
            config=self.config,
            metrics=metrics,
            trades=all_trades,
            equity_curve=equity_curve,
            drawdown_curve=drawdown_curve,
            daily_returns=daily_returns,
            signals_generated=sum(r.signals_generated for r in results),
            signals_traded=sum(r.signals_traded for r in results),
            signals_filtered=sum(r.signals_filtered for r in results),
            start_time=start_time,
            end_time=end_time
        )

    def _generate_result(
        self,
        start_time: datetime,
        end_time: datetime
    ) -> BacktestResult:
        """Generate backtest result from current state."""
        equity_curve = pd.Series(
            [e[1] for e in self.equity_history],
            index=[e[0] for e in self.equity_history]
        )

        trades = self.position_manager.closed_positions
        metrics = self._calculate_metrics(trades, equity_curve)

        drawdown_curve = self._calculate_drawdown(equity_curve)
        daily_returns = equity_curve.resample("D").last().pct_change().dropna()

        return BacktestResult(
            config=self.config,
            metrics=metrics,
            trades=trades,
            equity_curve=equity_curve,
            drawdown_curve=drawdown_curve,
            daily_returns=daily_returns,
            signals_generated=self.signals_generated,
            signals_traded=self.signals_traded,
            signals_filtered=self.signals_filtered,
            start_time=start_time,
            end_time=end_time
        )

    def _calculate_metrics(
        self,
        trades: List[Trade],
        equity_curve: pd.Series
    ) -> BacktestMetrics:
        """Calculate comprehensive trading metrics."""
        metrics = BacktestMetrics()

        if not trades:
            return metrics

        metrics.total_trades = len(trades)
        pnls = [t.pnl for t in trades]

        winning = [p for p in pnls if p > 0]
        losing = [p for p in pnls if p < 0]

        metrics.winning_trades = len(winning)
        metrics.losing_trades = len(losing)
        metrics.win_rate = metrics.winning_trades / metrics.total_trades

        metrics.total_profit = sum(pnls)
        metrics.total_profit_pct = (metrics.total_profit / self.config.initial_capital) * 100

        metrics.gross_profit = sum(winning) if winning else 0
        metrics.gross_loss = abs(sum(losing)) if losing else 0

        if metrics.gross_loss > 0:
            metrics.profit_factor = metrics.gross_profit / metrics.gross_loss
        else:
            metrics.profit_factor = float("inf") if metrics.gross_profit > 0 else 0

        metrics.avg_win = np.mean(winning) if winning else 0
        metrics.avg_loss = abs(np.mean(losing)) if losing else 0
        metrics.avg_trade = np.mean(pnls)

        metrics.expectancy = (
            metrics.win_rate * metrics.avg_win -
            (1 - metrics.win_rate) * metrics.avg_loss
        )

        metrics.best_trade = max(pnls) if pnls else 0
        metrics.worst_trade = min(pnls) if pnls else 0

        durations = [t.duration_minutes for t in trades if t.duration_minutes is not None]
        metrics.avg_trade_duration_minutes = np.mean(durations) if durations else 0

        if len(equity_curve) > 1:
            running_max = equity_curve.cummax()
            drawdown = (running_max - equity_curve) / running_max
            metrics.max_drawdown_pct = drawdown.max()
            metrics.max_drawdown = (running_max - equity_curve).max()

            in_dd = drawdown > 0
            dd_duration = 0
            max_dd_duration = 0
            for is_dd in in_dd:
                if is_dd:
                    dd_duration += 1
                    max_dd_duration = max(max_dd_duration, dd_duration)
                else:
                    dd_duration = 0
            metrics.max_drawdown_duration_days = max_dd_duration

            daily_returns = equity_curve.resample("D").last().pct_change().dropna()

            if len(daily_returns) > 1:
                excess_return = daily_returns.mean() - (self.config.risk_free_rate / 252)
                std_return = daily_returns.std()

                if std_return > 0:
                    metrics.sharpe_ratio = np.sqrt(252) * excess_return / std_return

                negative_returns = daily_returns[daily_returns < 0]
                if len(negative_returns) > 0:
                    downside_std = negative_returns.std()
                    if downside_std > 0:
                        metrics.sortino_ratio = np.sqrt(252) * excess_return / downside_std

                if metrics.max_drawdown_pct > 0:
                    annual_return = daily_returns.mean() * 252
                    metrics.calmar_ratio = annual_return / metrics.max_drawdown_pct

        max_wins = 0
        max_losses = 0
        current_wins = 0
        current_losses = 0

        for pnl in pnls:
            if pnl > 0:
                current_wins += 1
                current_losses = 0
                max_wins = max(max_wins, current_wins)
            elif pnl < 0:
                current_losses += 1
                current_wins = 0
                max_losses = max(max_losses, current_losses)

        metrics.max_consecutive_wins = max_wins
        metrics.max_consecutive_losses = max_losses

        long_trades = [t for t in trades if t.is_long]
        short_trades = [t for t in trades if t.is_short]

        metrics.long_trades = len(long_trades)
        metrics.short_trades = len(short_trades)

        if long_trades:
            long_wins = sum(1 for t in long_trades if t.pnl > 0)
            metrics.long_win_rate = long_wins / len(long_trades)

        if short_trades:
            short_wins = sum(1 for t in short_trades if t.pnl > 0)
            metrics.short_win_rate = short_wins / len(short_trades)

        metrics.total_commission = sum(t.commission for t in trades)
        metrics.total_slippage = sum(t.slippage for t in trades)

        return metrics

    def _calculate_drawdown(self, equity_curve: pd.Series) -> pd.Series:
        """Calculate drawdown series."""
        running_max = equity_curve.cummax()
        drawdown = (running_max - equity_curve) / running_max
        return drawdown


def create_simple_strategy(
    confidence_threshold: float = 0.6,
    risk_reward: float = 2.0,
    atr_multiplier: float = 1.5
) -> Callable:
    """
    Create a simple strategy function for testing.

    Args:
        confidence_threshold: Minimum confidence to trade
        risk_reward: Risk/reward ratio for TP calculation
        atr_multiplier: ATR multiplier for SL calculation

    Returns:
        Strategy function
    """
    def strategy(data: pd.DataFrame, index: int) -> Optional[Dict[str, Any]]:
        if index < 20:
            return None

        close = data.iloc[index]["close"]
        sma20 = data["close"].iloc[index - 19:index + 1].mean()

        tr = np.maximum(
            data["high"].iloc[index - 13:index + 1] - data["low"].iloc[index - 13:index + 1],
            np.abs(data["high"].iloc[index - 13:index + 1] - data["close"].iloc[index - 14:index].values),
            np.abs(data["low"].iloc[index - 13:index + 1] - data["close"].iloc[index - 14:index].values)
        )
        atr = tr.mean()

        momentum = (close - data["close"].iloc[index - 5]) / data["close"].iloc[index - 5]

        if close > sma20 and momentum > 0.005:
            direction = "long"
            confidence = min(0.5 + abs(momentum) * 10, 0.9)
            stop_loss = close - atr * atr_multiplier
            take_profit = close + atr * atr_multiplier * risk_reward
        elif close < sma20 and momentum < -0.005:
            direction = "short"
            confidence = min(0.5 + abs(momentum) * 10, 0.9)
            stop_loss = close + atr * atr_multiplier
            take_profit = close - atr * atr_multiplier * risk_reward
        else:
            return None

        if confidence < confidence_threshold:
            return None

        return {
            "direction": direction,
            "confidence": confidence,
            "stop_loss": stop_loss,
            "take_profit": take_profit
        }

    return strategy
