"""MCP router for VectorBT backtesting tools with structured logging."""

from typing import Any

import numpy as np
from fastmcp import Context

from maverick_mcp.backtesting import (
    BacktestAnalyzer,
    StrategyOptimizer,
    VectorBTEngine,
)
from maverick_mcp.backtesting.strategies import STRATEGY_TEMPLATES, StrategyParser
from maverick_mcp.backtesting.strategies.templates import (
    get_strategy_info,
    list_available_strategies,
)
from maverick_mcp.backtesting.visualization import (
    generate_equity_curve,
    generate_optimization_heatmap,
    generate_performance_dashboard,
    generate_trade_scatter,
)
from maverick_mcp.utils.debug_utils import debug_operation
from maverick_mcp.utils.logging import get_logger
from maverick_mcp.utils.structured_logger import (
    CorrelationIDGenerator,
    get_performance_logger,
    with_structured_logging,
)

# Initialize performance logger for backtesting router
performance_logger = get_performance_logger("backtesting_router")
logger = get_logger(__name__)


def convert_numpy_types(obj: Any) -> Any:
    """Recursively convert numpy types to Python native types for JSON serialization.

    Args:
        obj: Any object that might contain numpy types

    Returns:
        Object with all numpy types converted to Python native types
    """
    import pandas as pd

    # Check for numpy integer types (more robust using numpy's type hierarchy)
    if isinstance(obj, np.integer):
        return int(obj)
    # Check for numpy floating point types
    elif isinstance(obj, np.floating):
        return float(obj)
    # Check for numpy boolean type
    elif isinstance(obj, np.bool_ | bool) and hasattr(obj, "item"):
        return bool(obj)
    # Check for numpy complex types
    elif isinstance(obj, np.complexfloating):
        return complex(obj)
    # Handle numpy arrays
    elif isinstance(obj, np.ndarray):
        return obj.tolist()
    # Handle pandas Series
    elif isinstance(obj, pd.Series):
        return obj.tolist()
    # Handle pandas DataFrame
    elif isinstance(obj, pd.DataFrame):
        return obj.to_dict("records")
    # Handle NaN/None values
    elif pd.isna(obj):
        return None
    # Handle other numpy scalars with .item() method
    elif hasattr(obj, "item") and hasattr(obj, "dtype"):
        try:
            return obj.item()
        except Exception:
            return str(obj)
    # Recursively handle dictionaries
    elif isinstance(obj, dict):
        return {key: convert_numpy_types(value) for key, value in obj.items()}
    # Recursively handle lists and tuples
    elif isinstance(obj, list | tuple):
        return [convert_numpy_types(item) for item in obj]
    # Try to handle custom objects with __dict__
    elif hasattr(obj, "__dict__") and not isinstance(obj, type):
        try:
            return convert_numpy_types(obj.__dict__)
        except Exception:
            return str(obj)
    else:
        # Return as-is for regular Python types
        return obj


def setup_backtesting_tools(mcp):
    """Set up VectorBT backtesting tools for MCP.

    Args:
        mcp: FastMCP instance
    """

    @mcp.tool()
    @with_structured_logging("run_backtest", include_performance=True, log_params=True)
    @debug_operation("run_backtest", enable_profiling=True, symbol="backtest_symbol")
    async def run_backtest(
        ctx: Context,
        symbol: str,
        strategy: str = "sma_cross",
        start_date: str | None = None,
        end_date: str | None = None,
        initial_capital: float = 10000.0,
        fast_period: str | int | None = None,
        slow_period: str | int | None = None,
        period: str | int | None = None,
        oversold: str | float | None = None,
        overbought: str | float | None = None,
        signal_period: str | int | None = None,
        std_dev: str | float | None = None,
        lookback: str | int | None = None,
        threshold: str | float | None = None,
        z_score_threshold: str | float | None = None,
        breakout_factor: str | float | None = None,
    ) -> dict[str, Any]:
        """Run a VectorBT backtest with specified strategy and parameters.

        Args:
            symbol: Stock symbol to backtest
            strategy: Strategy type (sma_cross, rsi, macd, bollinger, momentum, etc.)
            start_date: Start date (YYYY-MM-DD), defaults to 1 year ago
            end_date: End date (YYYY-MM-DD), defaults to today
            initial_capital: Starting capital for backtest
            Strategy-specific parameters passed as individual arguments (e.g., fast_period=10, slow_period=20)

        Returns:
            Comprehensive backtest results including metrics, trades, and analysis

        Examples:
            run_backtest("AAPL", "sma_cross", fast_period=10, slow_period=20)
            run_backtest("TSLA", "rsi", period=14, oversold=30, overbought=70)
        """
        from datetime import datetime, timedelta

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")

        # Convert string parameters to appropriate types
        def convert_param(value, param_type):
            """Convert string parameter to appropriate type."""
            if value is None:
                return None
            if isinstance(value, str):
                try:
                    if param_type is int:
                        return int(value)
                    elif param_type is float:
                        return float(value)
                except (ValueError, TypeError) as e:
                    raise ValueError(
                        f"Invalid {param_type.__name__} value: {value}"
                    ) from e
            return value

        # Build parameters dict from provided arguments with type conversion
        param_map = {
            "fast_period": convert_param(fast_period, int),
            "slow_period": convert_param(slow_period, int),
            "period": convert_param(period, int),
            "oversold": convert_param(oversold, float),
            "overbought": convert_param(overbought, float),
            "signal_period": convert_param(signal_period, int),
            "std_dev": convert_param(std_dev, float),
            "lookback": convert_param(lookback, int),
            "threshold": convert_param(threshold, float),
            "z_score_threshold": convert_param(z_score_threshold, float),
            "breakout_factor": convert_param(breakout_factor, float),
        }

        # Get default parameters for strategy
        if strategy in STRATEGY_TEMPLATES:
            parameters = dict(STRATEGY_TEMPLATES[strategy]["parameters"])
            # Override with provided non-None parameters
            for param_name, param_value in param_map.items():
                if param_value is not None:
                    parameters[param_name] = param_value
        else:
            # Use only provided parameters for unknown strategies
            parameters = {k: v for k, v in param_map.items() if v is not None}

        # Initialize engine
        engine = VectorBTEngine()

        # Run backtest
        results = await engine.run_backtest(
            symbol=symbol,
            strategy_type=strategy,
            parameters=parameters,
            start_date=start_date,
            end_date=end_date,
            initial_capital=initial_capital,
        )

        # Analyze results
        analyzer = BacktestAnalyzer()
        analysis = analyzer.analyze(results)

        # Combine results and analysis
        results["analysis"] = analysis

        # Log business metrics
        if results.get("metrics"):
            metrics = results["metrics"]
            performance_logger.log_business_metric(
                "backtest_completion",
                1,
                symbol=symbol,
                strategy=strategy,
                total_return=metrics.get("total_return", 0),
                sharpe_ratio=metrics.get("sharpe_ratio", 0),
                max_drawdown=metrics.get("max_drawdown", 0),
                total_trades=metrics.get("total_trades", 0),
            )

            # Set correlation context for downstream operations
            CorrelationIDGenerator.set_correlation_id()

        return results

    @mcp.tool()
    @with_structured_logging(
        "optimize_strategy", include_performance=True, log_params=True
    )
    @debug_operation(
        "optimize_strategy", enable_profiling=True, strategy="optimization_strategy"
    )
    async def optimize_strategy(
        ctx: Context,
        symbol: str,
        strategy: str = "sma_cross",
        start_date: str | None = None,
        end_date: str | None = None,
        optimization_metric: str = "sharpe_ratio",
        optimization_level: str = "medium",
        top_n: int = 10,
    ) -> dict[str, Any]:
        """Optimize strategy parameters using VectorBT grid search.

        Args:
            symbol: Stock symbol to optimize
            strategy: Strategy type to optimize
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)
            optimization_metric: Metric to optimize (sharpe_ratio, total_return, win_rate, etc.)
            optimization_level: Level of optimization (coarse, medium, fine)
            top_n: Number of top results to return

        Returns:
            Optimization results with best parameters and performance metrics
        """
        from datetime import datetime, timedelta

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365 * 2)).strftime("%Y-%m-%d")

        # Initialize engine and optimizer
        engine = VectorBTEngine()
        optimizer = StrategyOptimizer(engine)

        # Generate parameter grid
        param_grid = optimizer.generate_param_grid(strategy, optimization_level)

        # Run optimization
        results = await engine.optimize_parameters(
            symbol=symbol,
            strategy_type=strategy,
            param_grid=param_grid,
            start_date=start_date,
            end_date=end_date,
            optimization_metric=optimization_metric,
            top_n=top_n,
        )

        return results

    @mcp.tool()
    async def walk_forward_analysis(
        ctx: Context,
        symbol: str,
        strategy: str = "sma_cross",
        start_date: str | None = None,
        end_date: str | None = None,
        window_size: int = 252,
        step_size: int = 63,
    ) -> dict[str, Any]:
        """Perform walk-forward analysis to test strategy robustness.

        Args:
            symbol: Stock symbol to analyze
            strategy: Strategy type
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)
            window_size: Test window size in trading days (default: 1 year)
            step_size: Step size for rolling window (default: 1 quarter)

        Returns:
            Walk-forward analysis results with out-of-sample performance
        """
        from datetime import datetime, timedelta

        # Default date range (3 years for walk-forward)
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365 * 3)).strftime("%Y-%m-%d")

        # Initialize engine and optimizer
        engine = VectorBTEngine()
        optimizer = StrategyOptimizer(engine)

        # Get default parameters
        parameters = STRATEGY_TEMPLATES.get(strategy, {}).get("parameters", {})

        # Run walk-forward analysis
        results = await optimizer.walk_forward_analysis(
            symbol=symbol,
            strategy_type=strategy,
            parameters=parameters,
            start_date=start_date,
            end_date=end_date,
            window_size=window_size,
            step_size=step_size,
        )

        return results

    @mcp.tool()
    async def monte_carlo_simulation(
        ctx: Context,
        symbol: str,
        strategy: str = "sma_cross",
        start_date: str | None = None,
        end_date: str | None = None,
        num_simulations: int = 1000,
        fast_period: str | int | None = None,
        slow_period: str | int | None = None,
        period: str | int | None = None,
    ) -> dict[str, Any]:
        """Run Monte Carlo simulation on backtest results.

        Args:
            symbol: Stock symbol
            strategy: Strategy type
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)
            num_simulations: Number of Monte Carlo simulations
            Strategy-specific parameters as individual arguments

        Returns:
            Monte Carlo simulation results with confidence intervals
        """
        from datetime import datetime, timedelta

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")

        # Convert string parameters to appropriate types
        def convert_param(value, param_type):
            """Convert string parameter to appropriate type."""
            if value is None:
                return None
            if isinstance(value, str):
                try:
                    if param_type is int:
                        return int(value)
                    elif param_type is float:
                        return float(value)
                except (ValueError, TypeError) as e:
                    raise ValueError(
                        f"Invalid {param_type.__name__} value: {value}"
                    ) from e
            return value

        # Build parameters dict from provided arguments with type conversion
        param_map = {
            "fast_period": convert_param(fast_period, int),
            "slow_period": convert_param(slow_period, int),
            "period": convert_param(period, int),
        }

        # Get parameters
        if strategy in STRATEGY_TEMPLATES:
            parameters = dict(STRATEGY_TEMPLATES[strategy]["parameters"])
            # Override with provided non-None parameters
            for param_name, param_value in param_map.items():
                if param_value is not None:
                    parameters[param_name] = param_value
        else:
            # Use only provided parameters for unknown strategies
            parameters = {k: v for k, v in param_map.items() if v is not None}

        # Run backtest first
        engine = VectorBTEngine()
        backtest_results = await engine.run_backtest(
            symbol=symbol,
            strategy_type=strategy,
            parameters=parameters,
            start_date=start_date,
            end_date=end_date,
        )

        # Run Monte Carlo simulation
        optimizer = StrategyOptimizer(engine)
        mc_results = await optimizer.monte_carlo_simulation(
            backtest_results=backtest_results,
            num_simulations=num_simulations,
        )

        return mc_results

    @mcp.tool()
    async def compare_strategies(
        ctx: Context,
        symbol: str,
        strategies: list[str] | str | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
    ) -> dict[str, Any]:
        """Compare multiple strategies on the same symbol.

        Args:
            symbol: Stock symbol
            strategies: List of strategy types to compare (defaults to all)
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)

        Returns:
            Comparison results with rankings and analysis
        """
        from datetime import datetime, timedelta

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")

        # Handle strategies as JSON string from some clients
        if isinstance(strategies, str):
            import json

            try:
                strategies = json.loads(strategies)
            except json.JSONDecodeError:
                # If it's not JSON, treat it as a single strategy
                strategies = [strategies]

        # Default to comparing top strategies
        if not strategies:
            strategies = ["sma_cross", "rsi", "macd", "bollinger", "momentum"]

        # Run backtests for each strategy
        engine = VectorBTEngine()
        results_list = []

        for strategy in strategies:
            try:
                # Get default parameters
                parameters = STRATEGY_TEMPLATES.get(strategy, {}).get("parameters", {})

                # Run backtest
                results = await engine.run_backtest(
                    symbol=symbol,
                    strategy_type=strategy,
                    parameters=parameters,
                    start_date=start_date,
                    end_date=end_date,
                )
                results_list.append(results)
            except Exception:
                # Skip failed strategies
                continue

        # Compare results
        analyzer = BacktestAnalyzer()
        comparison = analyzer.compare_strategies(results_list)

        return comparison

    @mcp.tool()
    async def list_strategies(ctx: Context) -> dict[str, Any]:
        """List all available VectorBT strategies with descriptions.

        Returns:
            Dictionary of available strategies and their information
        """
        strategies = {}

        for strategy_type in list_available_strategies():
            strategies[strategy_type] = get_strategy_info(strategy_type)

        return {
            "available_strategies": strategies,
            "total_count": len(strategies),
            "categories": {
                "trend_following": ["sma_cross", "ema_cross", "macd", "breakout"],
                "mean_reversion": ["rsi", "bollinger", "mean_reversion"],
                "momentum": ["momentum", "volume_momentum"],
            },
        }

    @mcp.tool()
    async def parse_strategy(ctx: Context, description: str) -> dict[str, Any]:
        """Parse natural language strategy description into VectorBT parameters.

        Args:
            description: Natural language description of trading strategy

        Returns:
            Parsed strategy configuration with type and parameters

        Examples:
            "Buy when RSI is below 30 and sell when above 70"
            "Use 10-day and 20-day moving average crossover"
            "MACD strategy with standard parameters"
        """
        parser = StrategyParser()
        config = parser.parse_simple(description)

        # Validate the parsed strategy
        if parser.validate_strategy(config):
            return {
                "success": True,
                "strategy": config,
                "message": f"Successfully parsed as {config['strategy_type']} strategy",
            }
        else:
            return {
                "success": False,
                "strategy": config,
                "message": "Could not fully parse strategy, using defaults",
            }

    @mcp.tool()
    async def backtest_portfolio(
        ctx: Context,
        symbols: list[str],
        strategy: str = "sma_cross",
        start_date: str | None = None,
        end_date: str | None = None,
        initial_capital: float = 10000.0,
        position_size: float = 0.1,
        fast_period: str | int | None = None,
        slow_period: str | int | None = None,
        period: str | int | None = None,
    ) -> dict[str, Any]:
        """Backtest a strategy across multiple symbols (portfolio).

        Args:
            symbols: List of stock symbols
            strategy: Strategy type to apply
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)
            initial_capital: Starting capital
            position_size: Position size per symbol (0.1 = 10%)
            Strategy-specific parameters as individual arguments

        Returns:
            Portfolio backtest results with aggregate metrics
        """
        from datetime import datetime, timedelta

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")

        # Convert string parameters to appropriate types
        def convert_param(value, param_type):
            """Convert string parameter to appropriate type."""
            if value is None:
                return None
            if isinstance(value, str):
                try:
                    if param_type is int:
                        return int(value)
                    elif param_type is float:
                        return float(value)
                except (ValueError, TypeError) as e:
                    raise ValueError(
                        f"Invalid {param_type.__name__} value: {value}"
                    ) from e
            return value

        # Build parameters dict from provided arguments with type conversion
        param_map = {
            "fast_period": convert_param(fast_period, int),
            "slow_period": convert_param(slow_period, int),
            "period": convert_param(period, int),
        }

        # Get parameters
        if strategy in STRATEGY_TEMPLATES:
            parameters = dict(STRATEGY_TEMPLATES[strategy]["parameters"])
            # Override with provided non-None parameters
            for param_name, param_value in param_map.items():
                if param_value is not None:
                    parameters[param_name] = param_value
        else:
            # Use only provided parameters for unknown strategies
            parameters = {k: v for k, v in param_map.items() if v is not None}

        # Run backtests for each symbol
        engine = VectorBTEngine()
        portfolio_results = []
        capital_per_symbol = initial_capital * position_size

        for symbol in symbols:
            try:
                results = await engine.run_backtest(
                    symbol=symbol,
                    strategy_type=strategy,
                    parameters=parameters,
                    start_date=start_date,
                    end_date=end_date,
                    initial_capital=capital_per_symbol,
                )
                portfolio_results.append(results)
            except Exception:
                # Skip failed symbols
                continue

        if not portfolio_results:
            return {"error": "No symbols could be backtested"}

        # Aggregate portfolio metrics
        total_return = sum(
            r["metrics"]["total_return"] for r in portfolio_results
        ) / len(portfolio_results)
        avg_sharpe = sum(r["metrics"]["sharpe_ratio"] for r in portfolio_results) / len(
            portfolio_results
        )
        max_drawdown = max(r["metrics"]["max_drawdown"] for r in portfolio_results)
        total_trades = sum(r["metrics"]["total_trades"] for r in portfolio_results)

        return {
            "portfolio_metrics": {
                "symbols_tested": len(portfolio_results),
                "total_return": total_return,
                "average_sharpe": avg_sharpe,
                "max_drawdown": max_drawdown,
                "total_trades": total_trades,
            },
            "individual_results": portfolio_results,
            "summary": f"Portfolio backtest of {len(portfolio_results)} symbols with {strategy} strategy",
        }

    @mcp.tool()
    async def generate_backtest_charts(
        ctx: Context,
        symbol: str,
        strategy: str = "sma_cross",
        start_date: str | None = None,
        end_date: str | None = None,
        theme: str = "light",
    ) -> dict[str, str]:
        """Generate comprehensive charts for a backtest.

        Args:
            symbol: Stock symbol
            strategy: Strategy type
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)
            theme: Chart theme (light or dark)

        Returns:
            Dictionary of base64-encoded chart images
        """
        from datetime import datetime, timedelta

        import pandas as pd

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")

        # Run backtest
        engine = VectorBTEngine()

        # Get default parameters for the strategy
        from maverick_mcp.backtesting.strategies import STRATEGY_TEMPLATES

        parameters = STRATEGY_TEMPLATES.get(strategy, {}).get("parameters", {})

        results = await engine.run_backtest(
            symbol=symbol,
            strategy_type=strategy,
            parameters=parameters,
            start_date=start_date,
            end_date=end_date,
        )

        # Prepare data for charts
        equity_curve_data = results["equity_curve"]
        drawdown_data = results["drawdown_series"]

        # Convert to pandas Series for charting
        returns = pd.Series(equity_curve_data)
        drawdown = pd.Series(drawdown_data)
        trades = pd.DataFrame(results["trades"])

        # Generate charts
        charts = {
            "equity_curve": generate_equity_curve(
                returns, drawdown, f"{symbol} {strategy} Equity Curve", theme
            ),
            "trade_scatter": generate_trade_scatter(
                returns, trades, f"{symbol} {strategy} Trades", theme
            ),
            "performance_dashboard": generate_performance_dashboard(
                results["metrics"], f"{symbol} {strategy} Performance", theme
            ),
        }

        return charts

    @mcp.tool()
    async def generate_optimization_charts(
        ctx: Context,
        symbol: str,
        strategy: str = "sma_cross",
        start_date: str | None = None,
        end_date: str | None = None,
        theme: str = "light",
    ) -> dict[str, str]:
        """Generate chart for strategy parameter optimization.

        Args:
            symbol: Stock symbol
            strategy: Strategy type
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)
            theme: Chart theme (light or dark)

        Returns:
            Dictionary of base64-encoded chart images
        """
        from datetime import datetime, timedelta

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")

        # Run optimization
        engine = VectorBTEngine()
        optimizer = StrategyOptimizer(engine)
        param_grid = optimizer.generate_param_grid(strategy, "medium")

        # Create optimization results dictionary for heatmap
        optimization_results = {}
        for param_set, results in param_grid.items():
            optimization_results[str(param_set)] = {
                "performance": results.get("total_return", 0)
            }

        # Generate optimization heatmap
        heatmap = generate_optimization_heatmap(
            optimization_results, f"{symbol} {strategy} Parameter Optimization", theme
        )

        return {"optimization_heatmap": heatmap}

    # ============ ML-ENHANCED STRATEGY TOOLS ============

    @mcp.tool()
    async def run_ml_strategy_backtest(
        ctx: Context,
        symbol: str,
        strategy_type: str = "ml_predictor",
        start_date: str | None = None,
        end_date: str | None = None,
        initial_capital: float = 10000.0,
        train_ratio: float = 0.8,
        model_type: str = "random_forest",
        n_estimators: int = 100,
        max_depth: int | None = None,
        learning_rate: float = 0.01,
        adaptation_method: str = "gradient",
    ) -> dict[str, Any]:
        """Run backtest using ML-enhanced strategies.

        Args:
            symbol: Stock symbol to backtest
            strategy_type: ML strategy type (ml_predictor, adaptive, ensemble, regime_aware)
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)
            initial_capital: Initial capital amount
            train_ratio: Ratio of data for training (0.0-1.0)
            Strategy-specific parameters passed as individual arguments

        Returns:
            Backtest results with ML-specific metrics
        """
        from datetime import datetime, timedelta

        from maverick_mcp.backtesting.strategies.ml import (
            AdaptiveStrategy,
            MLPredictor,
            RegimeAwareStrategy,
            StrategyEnsemble,
        )
        from maverick_mcp.backtesting.strategies.templates import (
            SimpleMovingAverageStrategy,
        )

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=730)).strftime(
                "%Y-%m-%d"
            )  # 2 years for ML

        # Get historical data
        engine = VectorBTEngine()
        data = await engine.get_historical_data(symbol, start_date, end_date)

        # Enhanced data validation for ML strategies
        min_total_data = 200  # Minimum total data points for ML strategies
        if len(data) < min_total_data:
            return {
                "error": f"Insufficient data for ML strategy: {len(data)} < {min_total_data} required"
            }

        # Split data for training/testing
        split_idx = int(len(data) * train_ratio)
        train_data = data.iloc[:split_idx]
        test_data = data.iloc[split_idx:]

        # Validate split data sizes
        min_train_data = 100
        min_test_data = 50

        if len(train_data) < min_train_data:
            return {
                "error": f"Insufficient training data: {len(train_data)} < {min_train_data} required"
            }

        if len(test_data) < min_test_data:
            return {
                "error": f"Insufficient test data: {len(test_data)} < {min_test_data} required"
            }

        logger.info(
            f"ML backtest data split: {len(train_data)} training, {len(test_data)} testing samples"
        )

        try:
            # Create ML strategy based on type
            if strategy_type == "ml_predictor":
                ml_strategy = MLPredictor(
                    model_type=model_type,
                    n_estimators=n_estimators,
                    max_depth=max_depth,
                )
                # Train the model
                training_metrics = ml_strategy.train(train_data)

            elif strategy_type == "adaptive" or strategy_type == "online_learning":
                # online_learning is an alias for adaptive strategy
                base_strategy = SimpleMovingAverageStrategy()
                ml_strategy = AdaptiveStrategy(
                    base_strategy,
                    learning_rate=learning_rate,
                    adaptation_method=adaptation_method,
                )
                training_metrics = {
                    "adaptation_method": adaptation_method,
                    "strategy_alias": strategy_type,
                }

            elif strategy_type == "ensemble":
                # Create ensemble with basic strategies
                base_strategies = [
                    SimpleMovingAverageStrategy({"fast_period": 10, "slow_period": 20}),
                    SimpleMovingAverageStrategy({"fast_period": 5, "slow_period": 15}),
                ]
                ml_strategy = StrategyEnsemble(base_strategies)
                training_metrics = {"ensemble_size": len(base_strategies)}

            elif strategy_type == "regime_aware":
                base_strategies = {
                    0: SimpleMovingAverageStrategy(
                        {"fast_period": 5, "slow_period": 20}
                    ),  # Bear
                    1: SimpleMovingAverageStrategy(
                        {"fast_period": 10, "slow_period": 30}
                    ),  # Sideways
                    2: SimpleMovingAverageStrategy(
                        {"fast_period": 20, "slow_period": 50}
                    ),  # Bull
                }
                ml_strategy = RegimeAwareStrategy(base_strategies)
                # Fit regime detector
                ml_strategy.fit_regime_detector(train_data)
                training_metrics = {"n_regimes": len(base_strategies)}

            else:
                return {"error": f"Unsupported ML strategy type: {strategy_type}"}

            # Generate signals on test data
            entry_signals, exit_signals = ml_strategy.generate_signals(test_data)

            # Run backtest analysis on test period
            analyzer = BacktestAnalyzer()
            backtest_results = await analyzer.run_vectorbt_backtest(
                data=test_data,
                entry_signals=entry_signals,
                exit_signals=exit_signals,
                initial_capital=initial_capital,
            )

            # Add ML-specific metrics
            ml_metrics = {
                "strategy_type": strategy_type,
                "training_period": len(train_data),
                "testing_period": len(test_data),
                "train_test_split": train_ratio,
                "training_metrics": training_metrics,
            }

            # Add strategy-specific analysis
            if hasattr(ml_strategy, "get_feature_importance"):
                ml_metrics["feature_importance"] = ml_strategy.get_feature_importance()

            if hasattr(ml_strategy, "get_regime_analysis"):
                ml_metrics["regime_analysis"] = ml_strategy.get_regime_analysis()

            if hasattr(ml_strategy, "get_strategy_weights"):
                ml_metrics["strategy_weights"] = ml_strategy.get_strategy_weights()

            backtest_results["ml_metrics"] = ml_metrics

            # Convert all numpy types before returning
            return convert_numpy_types(backtest_results)

        except Exception as e:
            return {"error": f"ML backtest failed: {str(e)}"}

    @mcp.tool()
    async def train_ml_predictor(
        ctx: Context,
        symbol: str,
        start_date: str | None = None,
        end_date: str | None = None,
        model_type: str = "random_forest",
        target_periods: int = 5,
        return_threshold: float = 0.02,
        n_estimators: int = 100,
        max_depth: int | None = None,
        min_samples_split: int = 2,
    ) -> dict[str, Any]:
        """Train an ML predictor model for trading signals.

        Args:
            symbol: Stock symbol to train on
            start_date: Start date for training data
            end_date: End date for training data
            model_type: ML model type (random_forest)
            target_periods: Forward periods for target variable
            return_threshold: Return threshold for signal classification
            n_estimators, max_depth, min_samples_split: Model-specific parameters

        Returns:
            Training results and model metrics
        """
        from datetime import datetime, timedelta

        from maverick_mcp.backtesting.strategies.ml import MLPredictor

        # Default date range (2 years for good ML training)
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=730)).strftime("%Y-%m-%d")

        try:
            # Get training data
            engine = VectorBTEngine()
            data = await engine.get_historical_data(symbol, start_date, end_date)

            if len(data) < 200:
                return {
                    "error": "Insufficient data for ML training (minimum 200 data points)"
                }

            # Create and train ML predictor
            ml_predictor = MLPredictor(
                model_type=model_type,
                n_estimators=n_estimators,
                max_depth=max_depth,
                min_samples_split=min_samples_split,
            )
            training_metrics = ml_predictor.train(
                data=data,
                target_periods=target_periods,
                return_threshold=return_threshold,
            )

            # Create model parameters dictionary
            model_params = {
                "n_estimators": n_estimators,
                "max_depth": max_depth,
                "min_samples_split": min_samples_split,
            }
            # Add training details
            training_results = {
                "symbol": symbol,
                "model_type": model_type,
                "training_period": f"{start_date} to {end_date}",
                "data_points": len(data),
                "target_periods": target_periods,
                "return_threshold": return_threshold,
                "model_parameters": model_params,
                "training_metrics": training_metrics,
            }

            # Convert all numpy types before returning
            return convert_numpy_types(training_results)

        except Exception as e:
            return {"error": f"ML training failed: {str(e)}"}

    @mcp.tool()
    async def analyze_market_regimes(
        ctx: Context,
        symbol: str,
        start_date: str | None = None,
        end_date: str | None = None,
        method: str = "hmm",
        n_regimes: int = 3,
        lookback_period: int = 50,
    ) -> dict[str, Any]:
        """Analyze market regimes for a stock using ML methods.

        Args:
            symbol: Stock symbol to analyze
            start_date: Start date for analysis
            end_date: End date for analysis
            method: Detection method (hmm, kmeans, threshold)
            n_regimes: Number of regimes to detect
            lookback_period: Lookback period for regime detection

        Returns:
            Market regime analysis results
        """
        from datetime import datetime, timedelta

        from maverick_mcp.backtesting.strategies.ml.regime_aware import (
            MarketRegimeDetector,
        )

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")

        try:
            # Get historical data
            engine = VectorBTEngine()
            data = await engine.get_historical_data(symbol, start_date, end_date)

            if len(data) < lookback_period + 50:
                return {
                    "error": f"Insufficient data for regime analysis (minimum {lookback_period + 50} data points)"
                }

            # Create regime detector and analyze
            regime_detector = MarketRegimeDetector(
                method=method, n_regimes=n_regimes, lookback_period=lookback_period
            )

            # Fit regime detector
            regime_detector.fit_regimes(data)

            # Analyze regimes over time
            regime_history = []
            regime_probabilities = []

            for i in range(lookback_period, len(data)):
                window_data = data.iloc[i - lookback_period : i + 1]
                current_regime = regime_detector.detect_current_regime(window_data)
                regime_probs = regime_detector.get_regime_probabilities(window_data)

                regime_history.append(
                    {
                        "date": data.index[i].strftime("%Y-%m-%d"),
                        "regime": int(current_regime),
                        "probabilities": regime_probs.tolist(),
                    }
                )
                regime_probabilities.append(regime_probs)

            # Calculate regime statistics
            regimes = [r["regime"] for r in regime_history]
            regime_counts = {i: regimes.count(i) for i in range(n_regimes)}
            regime_percentages = {
                k: (v / len(regimes)) * 100 for k, v in regime_counts.items()
            }

            # Calculate average regime durations
            regime_durations = {i: [] for i in range(n_regimes)}
            current_regime = regimes[0]
            duration = 1

            for regime in regimes[1:]:
                if regime == current_regime:
                    duration += 1
                else:
                    regime_durations[current_regime].append(duration)
                    current_regime = regime
                    duration = 1
            regime_durations[current_regime].append(duration)

            avg_durations = {
                k: np.mean(v) if v else 0 for k, v in regime_durations.items()
            }

            analysis_results = {
                "symbol": symbol,
                "analysis_period": f"{start_date} to {end_date}",
                "method": method,
                "n_regimes": n_regimes,
                "regime_names": {
                    0: "Bear/Declining",
                    1: "Sideways/Uncertain",
                    2: "Bull/Trending",
                },
                "current_regime": regimes[-1] if regimes else 1,
                "regime_counts": regime_counts,
                "regime_percentages": regime_percentages,
                "average_regime_durations": avg_durations,
                "recent_regime_history": regime_history[-20:],  # Last 20 periods
                "total_regime_switches": len(
                    [i for i in range(1, len(regimes)) if regimes[i] != regimes[i - 1]]
                ),
            }

            return analysis_results

        except Exception as e:
            return {"error": f"Regime analysis failed: {str(e)}"}

    @mcp.tool()
    async def create_strategy_ensemble(
        ctx: Context,
        symbols: list[str],
        base_strategies: list[str] | None = None,
        weighting_method: str = "performance",
        start_date: str | None = None,
        end_date: str | None = None,
        initial_capital: float = 10000.0,
    ) -> dict[str, Any]:
        """Create and backtest a strategy ensemble across multiple symbols.

        Args:
            symbols: List of stock symbols
            base_strategies: List of base strategy names to ensemble
            weighting_method: Weighting method (performance, equal, volatility)
            start_date: Start date for backtesting
            end_date: End date for backtesting
            initial_capital: Initial capital per symbol

        Returns:
            Ensemble backtest results with strategy weights
        """
        from datetime import datetime, timedelta

        from maverick_mcp.backtesting.strategies.ml import StrategyEnsemble
        from maverick_mcp.backtesting.strategies.templates import (
            SimpleMovingAverageStrategy,
        )

        # Default strategies if none provided
        if base_strategies is None:
            base_strategies = ["sma_cross", "rsi", "macd"]

        # Default date range
        if not end_date:
            end_date = datetime.now().strftime("%Y-%m-%d")
        if not start_date:
            start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")

        try:
            # Create base strategy instances
            strategy_instances = []
            for strategy_name in base_strategies:
                if strategy_name == "sma_cross":
                    strategy_instances.append(SimpleMovingAverageStrategy())
                elif strategy_name == "rsi":
                    # Create RSI-based SMA strategy with different parameters
                    strategy_instances.append(
                        SimpleMovingAverageStrategy(
                            {"fast_period": 14, "slow_period": 28}
                        )
                    )
                elif strategy_name == "macd":
                    # Create MACD-like SMA strategy with MACD default periods
                    strategy_instances.append(
                        SimpleMovingAverageStrategy(
                            {"fast_period": 12, "slow_period": 26}
                        )
                    )
                # Add more strategies as needed

            if not strategy_instances:
                return {"error": "No valid base strategies provided"}

            # Create ensemble strategy
            ensemble = StrategyEnsemble(
                strategies=strategy_instances, weighting_method=weighting_method
            )

            # Run ensemble backtest on multiple symbols
            ensemble_results = []
            total_return = 0
            total_trades = 0

            for symbol in symbols[:5]:  # Limit to 5 symbols for performance
                try:
                    # Get data and run backtest
                    engine = VectorBTEngine()
                    data = await engine.get_historical_data(
                        symbol, start_date, end_date
                    )

                    if len(data) < 100:
                        continue

                    # Generate ensemble signals
                    entry_signals, exit_signals = ensemble.generate_signals(data)

                    # Run backtest
                    analyzer = BacktestAnalyzer()
                    results = await analyzer.run_vectorbt_backtest(
                        data=data,
                        entry_signals=entry_signals,
                        exit_signals=exit_signals,
                        initial_capital=initial_capital,
                    )

                    # Add ensemble-specific metrics
                    results["ensemble_metrics"] = {
                        "strategy_weights": ensemble.get_strategy_weights(),
                        "strategy_performance": ensemble.get_strategy_performance(),
                    }

                    ensemble_results.append({"symbol": symbol, "results": results})

                    total_return += results["metrics"]["total_return"]
                    total_trades += results["metrics"]["total_trades"]

                except Exception:
                    continue

            if not ensemble_results:
                return {"error": "No symbols could be processed"}

            # Calculate aggregate metrics
            avg_return = total_return / len(ensemble_results)
            avg_trades = total_trades / len(ensemble_results)

            # Convert all numpy types before returning
            return convert_numpy_types(
                {
                    "ensemble_summary": {
                        "symbols_tested": len(ensemble_results),
                        "base_strategies": base_strategies,
                        "weighting_method": weighting_method,
                        "average_return": avg_return,
                        "total_trades": total_trades,
                        "average_trades_per_symbol": avg_trades,
                    },
                    "individual_results": ensemble_results,
                    "final_strategy_weights": ensemble.get_strategy_weights(),
                    "strategy_performance_analysis": ensemble.get_strategy_performance(),
                }
            )

        except Exception as e:
            return {"error": f"Ensemble creation failed: {str(e)}"}
