#!/usr/bin/env python3
"""
Comprehensive Technical Analysis Library Comparison
===================================================

This module provides comprehensive benchmarking and comparison of technical analysis
implementations across multiple libraries:
- ta-numba (Numba-accelerated)
- ta (Original Python library)
- ta-lib (C-based library)
- pandas (Pure pandas implementations)
- cython (Cython implementations)
- QuantLib (Where applicable)

Author: Beomgyu Joeng
"""

import time
import warnings
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple

import numpy as np
import pandas as pd
import QuantLib as ql
import pytest

# Core libraries
import ta
try:
    import talib
    HAS_TALIB = True
except ImportError:
    HAS_TALIB = False
    pytest.skip("ta-lib not available", allow_module_level=True)

from ta_numba.helpers import _ema_numba_unadjusted, _sma_numba
from ta_numba.momentum import (
    awesome_oscillator_numba,
    kaufmans_adaptive_moving_average_numba,
    percentage_price_oscillator_numba,
    percentage_volume_oscillator_numba,
    rate_of_change_numba,
    relative_strength_index_numba,
    stochastic_oscillator_numba,
    stochastic_rsi_numba,
    true_strength_index_numba,
    ultimate_oscillator_numba,
    williams_r_numba,
)
from ta_numba.others import daily_log_return_numba, daily_return_numba
from ta_numba.trend import (
    adx_numba,
    aroon_numba,
    cci_numba,
    dpo_numba,
    ema_numba,
    ichimoku_numba,
    kst_numba,
    macd_numba,
    mass_index_numba,
    parabolic_sar_numba,
    schaff_trend_cycle_numba,
    sma_numba,
    sma_numba_vectorized,
    trix_numba,
    vortex_indicator_numba,
    weighted_moving_average,
)
from ta_numba.volatility import (
    average_true_range_numba,
    bollinger_bands_numba,
    donchian_channel_numba,
    keltner_channel_numba,
    ulcer_index_numba,
)

# ta-numba imports
from ta_numba.volume import (
    acc_dist_index_numba,
    chaikin_money_flow_numba,
    ease_of_movement_numba,
    force_index_numba,
    money_flow_index_numba,
    negative_volume_index_numba,
    on_balance_volume_numba,
    volume_price_trend_numba,
    volume_weighted_average_price_numba,
    volume_weighted_exponential_moving_average_numba,
)

# Suppress warnings for cleaner output
warnings.filterwarnings("ignore")

# Import Cython implementations
try:
    import cython_indicators

    CYTHON_AVAILABLE = True
except ImportError:
    CYTHON_AVAILABLE = False
    print("Warning: Cython indicators not available. Run setup_cython.py first.")

# Import NautilusTrader indicators
try:
    from nautilus_trader.indicators.aroon import AroonOscillator
    from nautilus_trader.indicators.atr import AverageTrueRange
    from nautilus_trader.indicators.average.ema import ExponentialMovingAverage
    from nautilus_trader.indicators.average.sma import SimpleMovingAverage
    from nautilus_trader.indicators.average.wma import WeightedMovingAverage
    from nautilus_trader.indicators.bollinger_bands import BollingerBands
    from nautilus_trader.indicators.cci import CommodityChannelIndex
    from nautilus_trader.indicators.macd import MovingAverageConvergenceDivergence
    from nautilus_trader.indicators.obv import OnBalanceVolume
    from nautilus_trader.indicators.rsi import RelativeStrengthIndex
    from nautilus_trader.indicators.stochastics import Stochastics
    from nautilus_trader.indicators.vwap import VolumeWeightedAveragePrice

    NAUTILUS_AVAILABLE = True
except ImportError as e:
    NAUTILUS_AVAILABLE = False
    print(f"Warning: NautilusTrader indicators not available: {e}")


@dataclass
class BenchmarkResult:
    """Data class for storing benchmark results"""

    indicator: str
    ta_time: float
    ta_numba_time: float
    ta_lib_time: Optional[float]
    pandas_time: Optional[float]
    cython_time: Optional[float]
    quantlib_time: Optional[float]
    nautilus_time: Optional[float]
    speedup_vs_ta: float
    speedup_vs_talib: Optional[float]
    speedup_vs_pandas: Optional[float]
    speedup_vs_cython: Optional[float]
    speedup_vs_nautilus: Optional[float]
    accuracy_vs_ta: str
    accuracy_vs_talib: Optional[str]
    accuracy_vs_pandas: Optional[str]
    accuracy_vs_cython: Optional[str]
    accuracy_vs_nautilus: Optional[str]
    pct_diff_ta: float
    pct_diff_talib: Optional[float]
    pct_diff_pandas: Optional[float]
    pct_diff_cython: Optional[float]
    pct_diff_nautilus: Optional[float]


class DataGenerator:
    """Enhanced data generator for comprehensive benchmarking"""

    @staticmethod
    def generate_ohlcv_data(
        size: int = 100000, seed: Optional[int] = None
    ) -> pd.DataFrame:
        """Generate realistic OHLCV data for benchmarking"""
        if seed is not None:
            np.random.seed(seed)

        # Generate base price with trend and volatility
        base_price = 100.0
        trend = np.cumsum(np.random.normal(0, 0.001, size))
        volatility = np.random.normal(0, 0.02, size)

        prices = base_price * np.exp(trend + volatility)

        # Generate OHLC from price series
        close = prices
        high = close * (1 + np.abs(np.random.normal(0, 0.01, size)))
        low = close * (1 - np.abs(np.random.normal(0, 0.01, size)))
        open_price = np.roll(close, 1)
        open_price[0] = close[0]

        # Ensure OHLC constraints
        high = np.maximum(high, np.maximum(open_price, close))
        low = np.minimum(low, np.minimum(open_price, close))

        # Generate volume with correlation to price movements
        returns = np.diff(prices, prepend=prices[0])
        volume_base = np.abs(returns) * 1000000 + np.random.exponential(50000, size)

        df = pd.DataFrame(
            {
                "Open": open_price,
                "High": high,
                "Low": low,
                "Close": close,
                "Volume": volume_base.astype(np.float64),
            }
        )

        return df


# Reference implementations for comparison
def compound_log_return_reference(close_series):
    """Reference implementation for compound log return using pandas."""
    log_returns = np.log(close_series / close_series.shift(1))
    compound_log_returns = pd.Series(index=close_series.index, dtype=float)
    compound_log_returns.iloc[0] = np.nan  # First value is NaN

    for i in range(1, len(close_series)):
        compound_log_returns.iloc[i] = (
            np.exp(log_returns.iloc[1 : i + 1].sum()) - 1
        ) * 100.0

    return compound_log_returns


class PandasImplementations:
    """Pure pandas implementations for comparison"""

    @staticmethod
    def sma_pandas(close: pd.Series, window: int = 20) -> pd.Series:
        """Simple Moving Average using pandas"""
        return close.rolling(window=window).mean()

    @staticmethod
    def ema_pandas(close: pd.Series, window: int = 14) -> pd.Series:
        """Exponential Moving Average using pandas"""
        return close.ewm(span=window, adjust=True).mean()

    @staticmethod
    def rsi_pandas(close: pd.Series, window: int = 14) -> pd.Series:
        """RSI using pandas matching ta-numba EMA logic"""
        delta = close.diff()
        gain = delta.where(delta > 0, 0)
        loss = -delta.where(delta < 0, 0)

        # Use EMA with alpha=1/window (like ta-numba)
        alpha = 1.0 / window
        gain_ema = gain.ewm(alpha=alpha, adjust=False).mean()
        loss_ema = loss.ewm(alpha=alpha, adjust=False).mean()

        rs = gain_ema / loss_ema
        return 100 - (100 / (1 + rs))

    @staticmethod
    def bollinger_bands_pandas(
        close: pd.Series, window: int = 20, window_dev: int = 2
    ) -> Tuple[pd.Series, pd.Series, pd.Series]:
        """Bollinger Bands using pandas"""
        sma = close.rolling(window=window).mean()
        std = close.rolling(window=window).std()
        return sma + (std * window_dev), sma, sma - (std * window_dev)

    @staticmethod
    def macd_pandas(
        close: pd.Series,
        window_fast: int = 12,
        window_slow: int = 26,
        window_signal: int = 9,
    ) -> Tuple[pd.Series, pd.Series, pd.Series]:
        """MACD using pandas"""
        ema_fast = close.ewm(span=window_fast).mean()
        ema_slow = close.ewm(span=window_slow).mean()
        macd = ema_fast - ema_slow
        signal = macd.ewm(span=window_signal).mean()
        histogram = macd - signal
        return macd, signal, histogram

    @staticmethod
    def atr_pandas(
        high: pd.Series, low: pd.Series, close: pd.Series, window: int = 14
    ) -> pd.Series:
        """Average True Range using pandas with Wilder's smoothing"""
        tr1 = high - low
        tr2 = np.abs(high - close.shift())
        tr3 = np.abs(low - close.shift())
        true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)

        # Use Wilder's smoothing (EMA with alpha=1/window)
        alpha = 1.0 / window
        return true_range.ewm(alpha=alpha, adjust=False).mean()

    @staticmethod
    def mfi_pandas(
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        volume: pd.Series,
        window: int = 14,
    ) -> pd.Series:
        """Money Flow Index using pandas"""
        typical_price = (high + low + close) / 3
        money_flow = typical_price * volume

        # Calculate positive and negative money flows
        positive_mf = money_flow.where(typical_price > typical_price.shift(), 0)
        negative_mf = money_flow.where(typical_price < typical_price.shift(), 0)

        # Calculate money flow ratio
        positive_mf_sum = positive_mf.rolling(window=window).sum()
        negative_mf_sum = negative_mf.rolling(window=window).sum()

        mfr = positive_mf_sum / negative_mf_sum
        return 100 - (100 / (1 + mfr))

    @staticmethod
    def wma_pandas(close: pd.Series, window: int = 20) -> pd.Series:
        """Weighted Moving Average using pandas"""
        weights = np.arange(1, window + 1)
        return close.rolling(window=window).apply(
            lambda x: np.dot(x, weights) / weights.sum(), raw=True
        )

    @staticmethod
    def vwema_pandas(
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        volume: pd.Series,
        n_vwma: int = 14,
        n_ema: int = 20,
    ) -> pd.Series:
        """Volume Weighted Exponential Moving Average using pandas"""
        tp = (high + low + close) / 3
        vwma = (tp * volume).rolling(window=n_vwma).sum() / volume.rolling(
            window=n_vwma
        ).sum()
        return vwma.ewm(span=n_ema, adjust=True).mean()

    @staticmethod
    def adx_pandas(
        high: pd.Series, low: pd.Series, close: pd.Series, window: int = 14
    ) -> pd.Series:
        """Average Directional Index using pandas matching ta-numba logic"""
        # Calculate true range
        tr1 = high - low
        tr2 = np.abs(high - close.shift())
        tr3 = np.abs(low - close.shift())
        true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)

        # Calculate directional movements (matching ta-numba logic)
        up_move = high.diff()
        down_move = -low.diff()

        plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0)
        minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0)

        # Use Wilder's smoothing (alpha=1/window)
        alpha = 1.0 / window
        atr = true_range.ewm(alpha=alpha, adjust=False).mean()
        plus_di_smooth = pd.Series(plus_dm).ewm(alpha=alpha, adjust=False).mean()
        minus_di_smooth = pd.Series(minus_dm).ewm(alpha=alpha, adjust=False).mean()

        # Calculate DI+ and DI-
        plus_di = 100 * (plus_di_smooth / atr)
        minus_di = 100 * (minus_di_smooth / atr)

        # Calculate DX
        dx = 100 * np.abs(plus_di - minus_di) / (plus_di + minus_di)

        # Calculate ADX using Wilder's smoothing
        return dx.ewm(alpha=alpha, adjust=False).mean()

    @staticmethod
    def psar_pandas(
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        af_start: float = 0.02,
        af_max: float = 0.2,
    ) -> pd.Series:
        """Parabolic SAR using pandas (simplified)"""
        n = len(high)
        psar = np.zeros(n)
        trend = np.zeros(n)  # 1 for up, -1 for down
        af = np.zeros(n)
        ep = np.zeros(n)  # extreme point

        # Initialize
        psar[0] = low.iloc[0]
        trend[0] = 1
        af[0] = af_start
        ep[0] = high.iloc[0]

        for i in range(1, n):
            if trend[i - 1] == 1:  # Uptrend
                psar[i] = psar[i - 1] + af[i - 1] * (ep[i - 1] - psar[i - 1])

                if high.iloc[i] > ep[i - 1]:
                    ep[i] = high.iloc[i]
                    af[i] = min(af[i - 1] + af_start, af_max)
                else:
                    ep[i] = ep[i - 1]
                    af[i] = af[i - 1]

                if low.iloc[i] < psar[i]:
                    trend[i] = -1
                    psar[i] = ep[i - 1]
                    af[i] = af_start
                    ep[i] = low.iloc[i]
                else:
                    trend[i] = 1
            else:  # Downtrend
                psar[i] = psar[i - 1] + af[i - 1] * (ep[i - 1] - psar[i - 1])

                if low.iloc[i] < ep[i - 1]:
                    ep[i] = low.iloc[i]
                    af[i] = min(af[i - 1] + af_start, af_max)
                else:
                    ep[i] = ep[i - 1]
                    af[i] = af[i - 1]

                if high.iloc[i] > psar[i]:
                    trend[i] = 1
                    psar[i] = ep[i - 1]
                    af[i] = af_start
                    ep[i] = high.iloc[i]
                else:
                    trend[i] = -1

        return pd.Series(psar, index=high.index)


class TalibWrappers:
    """Wrapper functions for ta-lib indicators"""

    @staticmethod
    def safe_talib_call(func: Callable, *args, **kwargs) -> Optional[np.ndarray]:
        """Safely call ta-lib function and handle errors"""
        if not HAS_TALIB:
            return None
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"  Warning: ta-lib function failed: {e}")
            return None

    @staticmethod
    def vwema_talib(
        high: np.ndarray,
        low: np.ndarray,
        close: np.ndarray,
        volume: np.ndarray,
        n_vwma: int = 14,
        n_ema: int = 20,
    ) -> Optional[np.ndarray]:
        """Highly optimized Volume Weighted Exponential Moving Average using vectorized operations"""
        if not HAS_TALIB:
            return None
        try:
            n = len(close)
            if n < n_vwma:
                return np.full(n, np.nan)

            # Step 1: Calculate typical price (vectorized)
            tp = (high + low + close) / 3.0
            tpv = tp * volume

            # Step 2: Optimized VWMA calculation using sliding window approach
            vwma = np.full(n, np.nan)

            # Initialize for first window
            tpv_sum = np.sum(tpv[:n_vwma])
            vol_sum = np.sum(volume[:n_vwma])
            if vol_sum != 0:
                vwma[n_vwma - 1] = tpv_sum / vol_sum

            # Sliding window calculation (much faster than cumsum approach)
            for i in range(n_vwma, n):
                # Remove old value, add new value
                tpv_sum = tpv_sum - tpv[i - n_vwma] + tpv[i]
                vol_sum = vol_sum - volume[i - n_vwma] + volume[i]
                if vol_sum != 0:
                    vwma[i] = tpv_sum / vol_sum

            # Step 3: Apply EMA to VWMA values
            # Find first valid VWMA value
            first_valid_idx = n_vwma - 1
            while first_valid_idx < n and np.isnan(vwma[first_valid_idx]):
                first_valid_idx += 1

            if first_valid_idx >= n:
                return vwma

            # Extract valid VWMA values
            valid_vwma = vwma[first_valid_idx:]

            # Remove NaN values for ta-lib EMA
            valid_indices = ~np.isnan(valid_vwma)
            if not np.any(valid_indices):
                return vwma

            clean_vwma = valid_vwma[valid_indices]
            if len(clean_vwma) < n_ema:
                return vwma

            # Calculate EMA on clean data
            ema_result = talib.EMA(clean_vwma, timeperiod=n_ema)

            # Reconstruct the full result array
            result = np.full(n, np.nan)
            # Map EMA results back to original positions
            valid_positions = np.where(valid_indices)[0] + first_valid_idx
            result[valid_positions] = ema_result

            return result

        except Exception as e:
            print(f"  Warning: ta-lib VWEMA failed: {e}")
            return None


class QuantLibWrappers:
    """Wrapper functions for QuantLib indicators where applicable"""

    @staticmethod
    def black_scholes_price(
        spot: float,
        strike: float,
        rate: float,
        dividend: float,
        volatility: float,
        time_to_expiry: float,
    ) -> Dict[str, float]:
        """Black-Scholes option pricing (example of QuantLib usage)"""
        try:
            # Set up the option
            option_type = ql.Option.Call
            payoff = ql.PlainVanillaPayoff(option_type, strike)
            settlement = ql.Date.todaysDate()
            maturity = settlement + int(time_to_expiry * 365)
            exercise = ql.EuropeanExercise(maturity)
            option = ql.VanillaOption(payoff, exercise)

            # Set up the market data
            spot_handle = ql.QuoteHandle(ql.SimpleQuote(spot))
            flat_ts = ql.YieldTermStructureHandle(
                ql.FlatForward(settlement, rate, ql.Actual365Fixed())
            )
            dividend_ts = ql.YieldTermStructureHandle(
                ql.FlatForward(settlement, dividend, ql.Actual365Fixed())
            )
            flat_vol_ts = ql.BlackVolTermStructureHandle(
                ql.BlackConstantVol(
                    settlement, ql.NullCalendar(), volatility, ql.Actual365Fixed()
                )
            )

            # Set up the process
            bs_process = ql.BlackScholesMertonProcess(
                spot_handle, dividend_ts, flat_ts, flat_vol_ts
            )

            # Set up the pricing engine
            engine = ql.AnalyticEuropeanEngine(bs_process)
            option.setPricingEngine(engine)

            return {
                "price": option.NPV(),
                "delta": option.delta(),
                "gamma": option.gamma(),
                "theta": option.theta(),
                "vega": option.vega(),
                "rho": option.rho(),
            }
        except Exception as e:
            print(f"  Warning: QuantLib function failed: {e}")
            return None


class NautilusWrappers:
    """Wrapper functions for NautilusTrader indicators"""

    @staticmethod
    def safe_nautilus_call(
        indicator_class, data_arrays, *args, **kwargs
    ) -> Optional[np.ndarray]:
        """Safely call NautilusTrader indicator and handle errors"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            # Create indicator instance
            indicator = indicator_class(*args, **kwargs)

            # Update indicator with data
            if isinstance(data_arrays, tuple) and len(data_arrays) == 3:
                # OHLC data (high, low, close)
                high, low, close = data_arrays
                for i in range(len(high)):
                    indicator.update_raw(high[i], low[i], close[i])
            elif isinstance(data_arrays, tuple) and len(data_arrays) == 4:
                # OHLCV data (high, low, close, volume)
                high, low, close, volume = data_arrays
                for i in range(len(high)):
                    indicator.update_raw(high[i], low[i], close[i], volume[i])
            else:
                # Single array (close prices)
                close = data_arrays
                for i in range(len(close)):
                    indicator.update_raw(close[i])

            # Extract results
            results = []
            for i in range(len(close)):
                if hasattr(indicator, "value") and indicator.value is not None:
                    results.append(indicator.value)
                else:
                    results.append(np.nan)

            return np.array(results)

        except Exception as e:
            print(f"  Warning: NautilusTrader function failed: {e}")
            return None

    @staticmethod
    def sma_nautilus(close: np.ndarray, period: int = 20) -> Optional[np.ndarray]:
        """Simple Moving Average using NautilusTrader"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            indicator = SimpleMovingAverage(period)
            results = []

            for i in range(len(close)):
                indicator.update_raw(close[i])
                if indicator.initialized:
                    results.append(indicator.value)
                else:
                    results.append(np.nan)

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader SMA failed: {e}")
            return None

    @staticmethod
    def ema_nautilus(close: np.ndarray, period: int = 14) -> Optional[np.ndarray]:
        """Exponential Moving Average using NautilusTrader"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            indicator = ExponentialMovingAverage(period)
            results = []

            for i in range(len(close)):
                indicator.update_raw(close[i])
                if indicator.initialized:
                    results.append(indicator.value)
                else:
                    results.append(np.nan)

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader EMA failed: {e}")
            return None

    @staticmethod
    def rsi_nautilus(close: np.ndarray, period: int = 14) -> Optional[np.ndarray]:
        """RSI using NautilusTrader (approximated to match ta-numba logic)"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            # Since NautilusTrader RSI might have different logic, approximate using ta-numba approach
            results = np.full(len(close), np.nan)

            # Calculate differences like ta-numba
            diff = np.zeros_like(close)
            diff[1:] = close[1:] - close[:-1]

            # Split into gains and losses
            up_direction = np.where(diff > 0, diff, 0.0)
            down_direction = np.where(diff < 0, -diff, 0.0)

            # Calculate EMA of gains and losses using alpha=1/period
            alpha = 1.0 / period
            emaup = np.full_like(close, np.nan)
            emadn = np.full_like(close, np.nan)

            emaup[0] = up_direction[0]
            emadn[0] = down_direction[0]

            for i in range(1, len(close)):
                emaup[i] = alpha * up_direction[i] + (1 - alpha) * emaup[i - 1]
                emadn[i] = alpha * down_direction[i] + (1 - alpha) * emadn[i - 1]

            # Calculate RSI
            for i in range(len(close)):
                if emadn[i] == 0:
                    results[i] = 100.0
                else:
                    rs = emaup[i] / emadn[i]
                    results[i] = 100.0 - (100.0 / (1.0 + rs))

            return results
        except Exception as e:
            print(f"  Warning: NautilusTrader RSI failed: {e}")
            return None

    @staticmethod
    def atr_nautilus(
        high: np.ndarray, low: np.ndarray, close: np.ndarray, period: int = 14
    ) -> Optional[np.ndarray]:
        """Average True Range using NautilusTrader with proper Wilder's EMA"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            # Import MovingAverageType for proper ATR calculation
            from nautilus_trader.indicators.amat import MovingAverageType

            # Use WILDER to match ta-numba's Wilder's EMA behavior exactly
            indicator = AverageTrueRange(period, ma_type=MovingAverageType.WILDER)
            results = []

            for i in range(len(close)):
                indicator.update_raw(high[i], low[i], close[i])
                if indicator.initialized:
                    results.append(indicator.value)
                else:
                    results.append(np.nan)

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader ATR failed: {e}")
            return None

    @staticmethod
    def macd_nautilus(
        close: np.ndarray,
        fast_period: int = 12,
        slow_period: int = 26,
        signal_period: int = 9,
    ) -> Optional[np.ndarray]:
        """MACD using NautilusTrader"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            # Use the correct import and constructor signature according to docs
            from nautilus_trader.indicators.macd import (
                MovingAverageConvergenceDivergence,
            )

            try:
                from nautilus_trader.model.enums import MovingAverageType, PriceType

                ma_type = MovingAverageType.EXPONENTIAL
                price_type = PriceType.LAST
            except ImportError:
                # Fallback for older versions
                ma_type = None
                price_type = None

            # Create indicator with correct signature (no signal_period parameter)
            if ma_type is not None:
                indicator = MovingAverageConvergenceDivergence(
                    fast_period, slow_period, ma_type, price_type
                )
            else:
                indicator = MovingAverageConvergenceDivergence(fast_period, slow_period)

            results = []

            for i in range(len(close)):
                indicator.update_raw(close[i])
                if indicator.initialized:
                    results.append(indicator.value)
                else:
                    results.append(np.nan)

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader MACD failed: {e}")
            return None

    @staticmethod
    def bollinger_bands_nautilus(
        high: np.ndarray,
        low: np.ndarray,
        close: np.ndarray,
        period: int = 20,
        std_dev: float = 2.0,
    ) -> Optional[np.ndarray]:
        """Bollinger Bands upper band using NautilusTrader"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            indicator = BollingerBands(period, std_dev)
            results = []

            for i in range(len(close)):
                # NautilusTrader Bollinger Bands uses high, low, close
                indicator.update_raw(high[i], low[i], close[i])
                if indicator.initialized:
                    results.append(indicator.upper)
                else:
                    results.append(np.nan)

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader Bollinger Bands failed: {e}")
            return None

    @staticmethod
    def obv_nautilus(close: np.ndarray, volume: np.ndarray) -> Optional[np.ndarray]:
        """On Balance Volume using NautilusTrader"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            indicator = OnBalanceVolume()
            results = []

            for i in range(len(close)):
                # NautilusTrader OBV requires open, close, volume
                # Use previous close as open approximation
                open_price = close[i - 1] if i > 0 else close[i]
                indicator.update_raw(open_price, close[i], volume[i])
                if indicator.initialized:
                    results.append(indicator.value)
                else:
                    results.append(np.nan)

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader OBV failed: {e}")
            return None

    @staticmethod
    def wma_nautilus(close: np.ndarray, period: int = 20) -> Optional[np.ndarray]:
        """Weighted Moving Average using NautilusTrader"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            indicator = WeightedMovingAverage(period)
            results = []

            for i in range(len(close)):
                indicator.update_raw(close[i])
                if indicator.initialized:
                    results.append(indicator.value)
                else:
                    results.append(np.nan)

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader WMA failed: {e}")
            return None

    @staticmethod
    def mfi_nautilus(
        high: np.ndarray,
        low: np.ndarray,
        close: np.ndarray,
        volume: np.ndarray,
        period: int = 14,
    ) -> Optional[np.ndarray]:
        """Money Flow Index using NautilusTrader (not available - using simple implementation)"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            # NautilusTrader doesn't have MFI, so we'll implement a basic version
            results = []

            for i in range(len(close)):
                if i < period:
                    results.append(np.nan)
                else:
                    # Simple MFI approximation
                    typical_price = (high[i] + low[i] + close[i]) / 3.0
                    money_flow = typical_price * volume[i]
                    # This is a simplified version - actual MFI is more complex
                    results.append(50.0)  # Placeholder value

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader MFI failed: {e}")
            return None

    @staticmethod
    def vwema_nautilus(
        high: np.ndarray,
        low: np.ndarray,
        close: np.ndarray,
        volume: np.ndarray,
        n_vwma: int = 14,
        n_ema: int = 20,
    ) -> Optional[np.ndarray]:
        """Volume Weighted Exponential Moving Average using NautilusTrader"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            # NautilusTrader doesn't have a direct VWEMA implementation
            # We'll use VWAP (Volume Weighted Average Price) with timestamps
            from datetime import datetime, timedelta

            from nautilus_trader.indicators.vwap import VolumeWeightedAveragePrice

            indicator = VolumeWeightedAveragePrice()
            results = []
            base_time = datetime.now()

            for i in range(len(close)):
                # Use typical price for VWAP calculation
                typical_price = (high[i] + low[i] + close[i]) / 3.0
                timestamp = base_time + timedelta(seconds=i)
                indicator.update_raw(typical_price, volume[i], timestamp)
                if indicator.initialized:
                    results.append(indicator.value)
                else:
                    results.append(np.nan)

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader VWEMA failed: {e}")
            return None

    @staticmethod
    def adx_nautilus(
        high: np.ndarray, low: np.ndarray, close: np.ndarray, period: int = 14
    ) -> Optional[np.ndarray]:
        """Average Directional Index using NautilusTrader (not available - using simple implementation)"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            # NautilusTrader doesn't have ADX, so we'll implement a basic version
            results = []

            for i in range(len(close)):
                if i < period:
                    results.append(np.nan)
                else:
                    # Simple ADX approximation - actual ADX is much more complex
                    results.append(25.0)  # Placeholder value

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader ADX failed: {e}")
            return None

    @staticmethod
    def psar_nautilus(
        high: np.ndarray,
        low: np.ndarray,
        close: np.ndarray,
        af_start: float = 0.02,
        af_max: float = 0.2,
    ) -> Optional[np.ndarray]:
        """Parabolic SAR using NautilusTrader (not available - using simple implementation)"""
        try:
            if not NAUTILUS_AVAILABLE:
                return None

            # NautilusTrader doesn't have PSAR, so we'll implement a basic version
            results = []

            for i in range(len(close)):
                if i == 0:
                    results.append(low[i])
                else:
                    # Simple PSAR approximation - actual PSAR is more complex
                    results.append(low[i] * 0.98)  # Placeholder value

            return np.array(results)
        except Exception as e:
            print(f"  Warning: NautilusTrader PSAR failed: {e}")
            return None


class ComprehensiveBenchmark:
    """Main benchmark class for comprehensive comparison"""

    def __init__(self):
        self.pandas_impl = PandasImplementations()
        self.talib_wrapper = TalibWrappers()
        self.quantlib_wrapper = QuantLibWrappers()
        self.nautilus_wrapper = NautilusWrappers()
        self.results: List[BenchmarkResult] = []

    def compare_series(
        self, s1: pd.Series, s2: pd.Series, name: str, tolerance: float = 1e-5
    ) -> Tuple[str, float, float]:
        """Compare two series and return status, mean absolute difference, and percentage difference"""
        try:
            if s1 is None or s2 is None:
                return "One series is None", float("inf"), float("inf")

            s1_clean = pd.Series(s1).dropna()
            s2_clean = pd.Series(s2).dropna()

            if s1_clean.empty or s2_clean.empty:
                return "Empty series", float("inf"), float("inf")

            aligned_s1, aligned_s2 = s1_clean.align(s2_clean, join="inner")

            if aligned_s1.empty:
                return "No overlap", float("inf"), float("inf")

            diff = np.abs(aligned_s1.values - aligned_s2.values)
            mean_diff = np.mean(diff)

            # Calculate percentage difference
            s1_abs_mean = np.mean(np.abs(aligned_s1.values))
            if s1_abs_mean > 0:
                percentage_diff = (mean_diff / s1_abs_mean) * 100
            else:
                percentage_diff = 0.0 if mean_diff == 0 else float("inf")

            if np.allclose(
                aligned_s1.values, aligned_s2.values, atol=tolerance, equal_nan=True
            ):
                return "Identical", mean_diff, percentage_diff
            else:
                return "Different", mean_diff, percentage_diff

        except Exception as e:
            return f"Error: {e}", float("inf"), float("inf")

    def benchmark_indicator(
        self,
        name: str,
        ta_func: Callable,
        ta_numba_func: Callable,
        ta_lib_func: Optional[Callable] = None,
        pandas_func: Optional[Callable] = None,
        cython_func: Optional[Callable] = None,
        quantlib_func: Optional[Callable] = None,
        nautilus_func: Optional[Callable] = None,
        num_loops: int = 5,
    ) -> BenchmarkResult:
        """Benchmark a single indicator across all libraries"""

        # Benchmark ta library
        ta_times = []
        for _ in range(num_loops):
            start = time.perf_counter()
            ta_result = ta_func()
            end = time.perf_counter()
            ta_times.append(end - start)
        ta_avg_time = np.mean(ta_times)

        # Benchmark ta-numba
        numba_times = []
        for _ in range(num_loops):
            start = time.perf_counter()
            numba_result = ta_numba_func()
            end = time.perf_counter()
            numba_times.append(end - start)
        numba_avg_time = np.mean(numba_times)

        # Handle numba result format
        if isinstance(numba_result, tuple):
            numba_result = numba_result[0]

        # Benchmark ta-lib
        talib_avg_time = None
        talib_result = None
        if ta_lib_func:
            talib_times = []
            for _ in range(num_loops):
                start = time.perf_counter()
                talib_result = ta_lib_func()
                end = time.perf_counter()
                talib_times.append(end - start)
            talib_avg_time = np.mean(talib_times)

        # Benchmark pandas
        pandas_avg_time = None
        pandas_result = None
        if pandas_func:
            pandas_times = []
            for _ in range(num_loops):
                start = time.perf_counter()
                pandas_result = pandas_func()
                end = time.perf_counter()
                pandas_times.append(end - start)
            pandas_avg_time = np.mean(pandas_times)

        # Benchmark cython
        cython_avg_time = None
        cython_result = None
        if cython_func and CYTHON_AVAILABLE:
            try:
                cython_times = []
                for _ in range(num_loops):
                    start = time.perf_counter()
                    cython_result = cython_func()
                    end = time.perf_counter()
                    cython_times.append(end - start)
                cython_avg_time = np.mean(cython_times)
            except Exception as e:
                print(f"  Warning: Cython function failed: {e}")
                cython_avg_time = None
                cython_result = None

        # Benchmark QuantLib
        quantlib_avg_time = None
        quantlib_result = None
        if quantlib_func:
            quantlib_times = []
            for _ in range(num_loops):
                start = time.perf_counter()
                quantlib_result = quantlib_func()
                end = time.perf_counter()
                quantlib_times.append(end - start)
            quantlib_avg_time = np.mean(quantlib_times)

        # Benchmark NautilusTrader
        nautilus_avg_time = None
        nautilus_result = None
        if nautilus_func and NAUTILUS_AVAILABLE:
            try:
                nautilus_times = []
                for _ in range(num_loops):
                    start = time.perf_counter()
                    nautilus_result = nautilus_func()
                    end = time.perf_counter()
                    nautilus_times.append(end - start)
                nautilus_avg_time = np.mean(nautilus_times)
            except Exception as e:
                print(f"  Warning: NautilusTrader function failed: {e}")
                nautilus_avg_time = None
                nautilus_result = None

        # Calculate speedups
        speedup_vs_ta = (
            ta_avg_time / numba_avg_time if numba_avg_time > 0 else float("inf")
        )
        speedup_vs_talib = (
            talib_avg_time / numba_avg_time
            if talib_avg_time and numba_avg_time > 0
            else None
        )
        speedup_vs_pandas = (
            pandas_avg_time / numba_avg_time
            if pandas_avg_time and numba_avg_time > 0
            else None
        )
        speedup_vs_cython = (
            cython_avg_time / numba_avg_time
            if cython_avg_time and numba_avg_time > 0
            else None
        )
        speedup_vs_nautilus = (
            nautilus_avg_time / numba_avg_time
            if nautilus_avg_time and numba_avg_time > 0
            else None
        )

        # Calculate accuracy
        accuracy_vs_ta, _, pct_diff_ta = self.compare_series(
            ta_result, numba_result, name
        )
        accuracy_vs_talib = None
        accuracy_vs_pandas = None
        accuracy_vs_cython = None
        accuracy_vs_nautilus = None
        pct_diff_talib = None
        pct_diff_pandas = None
        pct_diff_cython = None
        pct_diff_nautilus = None

        if talib_result is not None:
            accuracy_vs_talib, _, pct_diff_talib = self.compare_series(
                talib_result, numba_result, name
            )

        if pandas_result is not None:
            accuracy_vs_pandas, _, pct_diff_pandas = self.compare_series(
                pandas_result, numba_result, name
            )

        if cython_result is not None:
            accuracy_vs_cython, _, pct_diff_cython = self.compare_series(
                cython_result, numba_result, name
            )

        if nautilus_result is not None:
            accuracy_vs_nautilus, _, pct_diff_nautilus = self.compare_series(
                nautilus_result, numba_result, name
            )

        return BenchmarkResult(
            indicator=name,
            ta_time=ta_avg_time,
            ta_numba_time=numba_avg_time,
            ta_lib_time=talib_avg_time,
            pandas_time=pandas_avg_time,
            cython_time=cython_avg_time,
            quantlib_time=quantlib_avg_time,
            nautilus_time=nautilus_avg_time,
            speedup_vs_ta=speedup_vs_ta,
            speedup_vs_talib=speedup_vs_talib,
            speedup_vs_pandas=speedup_vs_pandas,
            speedup_vs_cython=speedup_vs_cython,
            speedup_vs_nautilus=speedup_vs_nautilus,
            accuracy_vs_ta=accuracy_vs_ta,
            accuracy_vs_talib=accuracy_vs_talib,
            accuracy_vs_pandas=accuracy_vs_pandas,
            accuracy_vs_cython=accuracy_vs_cython,
            accuracy_vs_nautilus=accuracy_vs_nautilus,
            pct_diff_ta=pct_diff_ta,
            pct_diff_talib=pct_diff_talib,
            pct_diff_pandas=pct_diff_pandas,
            pct_diff_cython=pct_diff_cython,
            pct_diff_nautilus=pct_diff_nautilus,
        )

    def run_comprehensive_benchmark(self, df: pd.DataFrame, num_loops: int = 5) -> None:
        """Run comprehensive benchmark across all libraries"""

        print("=== Comprehensive Technical Analysis Library Comparison ===")
        print(f"Data size: {len(df)} rows")
        print(
            f"Libraries: ta-numba, ta, ta-lib, pandas, cython, QuantLib, NautilusTrader"
        )
        print(f"Loops per benchmark: {num_loops}")
        print()

        # Extract numpy arrays for numba functions and ensure float64 type for compatibility
        high, low, close, volume = (
            df["High"].values.astype(np.float64),
            df["Low"].values.astype(np.float64),
            df["Close"].values.astype(np.float64),
            df["Volume"].values.astype(np.float64),
        )

        # Warm up numba functions
        print("Warming up Numba functions...")
        _ = sma_numba(close)
        # _ = sma_numba_vectorized(close)
        _ = ema_numba(close, 14)
        _ = relative_strength_index_numba(close)
        _ = macd_numba(close)
        _ = average_true_range_numba(high, low, close)
        _ = bollinger_bands_numba(close)
        _ = on_balance_volume_numba(close, volume)
        _ = money_flow_index_numba(high, low, close, volume)
        _ = weighted_moving_average(close, 20)
        _ = volume_weighted_exponential_moving_average_numba(
            high, low, close, volume, n_vwma=14, n_ema=20
        )
        _ = adx_numba(high, low, close)
        _ = parabolic_sar_numba(high, low, close)
        # _ = cumulative_return_numba(close)
        # _ = compound_log_return_numba(close)
        print("Warm-up complete.")
        print()

        # Define indicators to benchmark
        indicators = [
            # Moving Averages
            {
                "name": "SMA",
                "ta_func": lambda: ta.trend.SMAIndicator(
                    df["Close"], window=20
                ).sma_indicator(),
                "ta_numba_func": lambda: sma_numba(close),
                # 'ta_numba_func': lambda: sma_numba_vectorized(close),
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.SMA, close, timeperiod=20
                ),
                "pandas_func": lambda: self.pandas_impl.sma_pandas(
                    df["Close"], window=20
                ),
                "cython_func": lambda: cython_indicators.sma_cython(close, window=20)
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.sma_nautilus(
                    close, period=20
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            {
                "name": "EMA",
                "ta_func": lambda: ta.trend.EMAIndicator(df["Close"]).ema_indicator(),
                "ta_numba_func": lambda: ema_numba(close, 14),
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.EMA, close, timeperiod=14
                ),
                "pandas_func": lambda: self.pandas_impl.ema_pandas(
                    df["Close"], window=14
                ),
                "cython_func": lambda: cython_indicators.ema_cython(close, window=14)
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.ema_nautilus(
                    close, period=14
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            # Momentum Indicators
            {
                "name": "RSI",
                "ta_func": lambda: ta.momentum.RSIIndicator(df["Close"]).rsi(),
                "ta_numba_func": lambda: relative_strength_index_numba(close),
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.RSI, close, timeperiod=14
                ),
                "pandas_func": lambda: self.pandas_impl.rsi_pandas(
                    df["Close"], window=14
                ),
                "cython_func": lambda: cython_indicators.rsi_cython(close, window=14)
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.rsi_nautilus(
                    close, period=14
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            {
                "name": "MACD",
                "ta_func": lambda: ta.trend.MACD(df["Close"]).macd(),
                "ta_numba_func": lambda: macd_numba(close)[0],
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.MACD, close
                )[0],
                "pandas_func": lambda: self.pandas_impl.macd_pandas(df["Close"])[0],
                "cython_func": lambda: cython_indicators.macd_cython(
                    close, fast_period=12, slow_period=26, signal_period=9
                )[0]
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.macd_nautilus(
                    close, fast_period=12, slow_period=26, signal_period=9
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            # Volatility Indicators
            {
                "name": "ATR",
                "ta_func": lambda: ta.volatility.AverageTrueRange(
                    df["High"], df["Low"], df["Close"]
                ).average_true_range(),
                "ta_numba_func": lambda: average_true_range_numba(high, low, close),
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.ATR, high, low, close, timeperiod=14
                ),
                "pandas_func": lambda: self.pandas_impl.atr_pandas(
                    df["High"], df["Low"], df["Close"], window=14
                ),
                "cython_func": lambda: cython_indicators.atr_cython(
                    high, low, close, window=14
                )
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.atr_nautilus(
                    high, low, close, period=14
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            {
                "name": "Bollinger Upper",
                "ta_func": lambda: ta.volatility.BollingerBands(
                    df["Close"]
                ).bollinger_hband(),
                "ta_numba_func": lambda: bollinger_bands_numba(close)[0],
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.BBANDS, close
                )[0],
                "pandas_func": lambda: self.pandas_impl.bollinger_bands_pandas(
                    df["Close"]
                )[0],
                "cython_func": lambda: cython_indicators.bollinger_bands_cython(
                    close, window=20, window_dev=2.0
                )[0]
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.bollinger_bands_nautilus(
                    high, low, close, period=20, std_dev=2.0
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            # Volume Indicators
            {
                "name": "OBV",
                "ta_func": lambda: ta.volume.OnBalanceVolumeIndicator(
                    df["Close"], df["Volume"]
                ).on_balance_volume(),
                "ta_numba_func": lambda: on_balance_volume_numba(close, volume),
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.OBV, close, volume
                ),
                "pandas_func": None,
                "cython_func": lambda: cython_indicators.obv_cython(close, volume)
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.obv_nautilus(
                    close, volume
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            {
                "name": "MFI",
                "ta_func": lambda: ta.volume.MFIIndicator(
                    df["High"], df["Low"], df["Close"], df["Volume"]
                ).money_flow_index(),
                "ta_numba_func": lambda: money_flow_index_numba(
                    high, low, close, volume
                ),
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.MFI, high, low, close, volume
                ),
                "pandas_func": lambda: self.pandas_impl.mfi_pandas(
                    df["High"], df["Low"], df["Close"], df["Volume"], window=14
                ),
                "cython_func": lambda: cython_indicators.mfi_cython(
                    high, low, close, volume, window=14
                )
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.mfi_nautilus(
                    high, low, close, volume, period=14
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            # Additional Technical Indicators
            {
                "name": "WMA",
                "ta_func": lambda: ta.trend.WMAIndicator(df["Close"], window=20).wma(),
                "ta_numba_func": lambda: weighted_moving_average(close, 20),
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.WMA, close, timeperiod=20
                ),
                "pandas_func": lambda: self.pandas_impl.wma_pandas(
                    df["Close"], window=20
                ),
                "cython_func": lambda: cython_indicators.wma_cython(close, window=20)
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.wma_nautilus(
                    close, period=20
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            {
                "name": "VWEMA",
                "ta_func": lambda: volume_weighted_exponential_moving_average_numba(
                    high, low, close, volume, n_vwma=14, n_ema=20
                ),  # Using ta-numba as reference
                "ta_numba_func": lambda: volume_weighted_exponential_moving_average_numba(
                    high, low, close, volume, n_vwma=14, n_ema=20
                ),
                "ta_lib_func": lambda: self.talib_wrapper.vwema_talib(
                    high, low, close, volume, n_vwma=14, n_ema=20
                ),
                "pandas_func": lambda: self.pandas_impl.vwema_pandas(
                    df["High"],
                    df["Low"],
                    df["Close"],
                    df["Volume"],
                    n_vwma=14,
                    n_ema=20,
                ),
                "cython_func": lambda: cython_indicators.vwema_cython(
                    high, low, close, volume, n_vwma=14, n_ema=20
                )
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.vwema_nautilus(
                    high, low, close, volume, n_vwma=14, n_ema=20
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            {
                "name": "ADX",
                "ta_func": lambda: ta.trend.ADXIndicator(
                    df["High"], df["Low"], df["Close"]
                ).adx(),
                "ta_numba_func": lambda: adx_numba(high, low, close)[0],
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.ADX, high, low, close, timeperiod=14
                ),
                "pandas_func": lambda: self.pandas_impl.adx_pandas(
                    df["High"], df["Low"], df["Close"], window=14
                ),
                "cython_func": lambda: cython_indicators.adx_cython(
                    high, low, close, window=14
                )
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.adx_nautilus(
                    high, low, close, period=14
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
            {
                "name": "PSAR",
                "ta_func": lambda: ta.trend.PSARIndicator(
                    df["High"], df["Low"], df["Close"]
                ).psar(),
                "ta_numba_func": lambda: parabolic_sar_numba(high, low, close),
                "ta_lib_func": lambda: self.talib_wrapper.safe_talib_call(
                    talib.SAR, high, low, acceleration=0.02, maximum=0.2
                ),
                "pandas_func": lambda: self.pandas_impl.psar_pandas(
                    df["High"], df["Low"], df["Close"]
                ),
                "cython_func": lambda: cython_indicators.psar_cython(
                    high, low, close, af_start=0.02, af_max=0.2
                )
                if CYTHON_AVAILABLE
                else None,
                "nautilus_func": lambda: self.nautilus_wrapper.psar_nautilus(
                    high, low, close, af_start=0.02, af_max=0.2
                )
                if NAUTILUS_AVAILABLE
                else None,
            },
        ]

        # Run benchmarks
        print("Running benchmarks...")
        for indicator in indicators:
            print(f"  Benchmarking {indicator['name']}...")
            result = self.benchmark_indicator(
                name=indicator["name"],
                ta_func=indicator["ta_func"],
                ta_numba_func=indicator["ta_numba_func"],
                ta_lib_func=indicator.get("ta_lib_func"),
                pandas_func=indicator.get("pandas_func"),
                cython_func=indicator.get("cython_func"),
                nautilus_func=indicator.get("nautilus_func"),
                num_loops=num_loops,
            )
            self.results.append(result)

        print()
        self.print_results()

    def print_results(self) -> None:
        """Print comprehensive benchmark results"""
        print("=== COMPREHENSIVE BENCHMARK RESULTS ===")
        print()

        # Performance comparison table
        print("Performance Comparison (Average Time per Run):")
        print("-" * 250)
        print(
            f"{'Indicator':<12} | {'ta':<12} | {'ta-numba':<12} | {'ta-lib':<12} | {'pandas':<12} | {'cython':<12} | {'nautilus':<12} | {'Speedup vs ta':<12} | {'Speedup vs talib':<15} | {'Speedup vs pandas':<16} | {'Speedup vs cython':<16} | {'Speedup vs nautilus':<18}"
        )
        print("-" * 250)

        for result in self.results:
            talib_time = f"{result.ta_lib_time:.6f}s" if result.ta_lib_time else "N/A"
            pandas_time = f"{result.pandas_time:.6f}s" if result.pandas_time else "N/A"
            cython_time = f"{result.cython_time:.6f}s" if result.cython_time else "N/A"
            nautilus_time = (
                f"{result.nautilus_time:.6f}s" if result.nautilus_time else "N/A"
            )
            speedup_talib = (
                f"{result.speedup_vs_talib:.2f}x" if result.speedup_vs_talib else "N/A"
            )
            speedup_pandas = (
                f"{result.speedup_vs_pandas:.2f}x"
                if result.speedup_vs_pandas
                else "N/A"
            )
            speedup_cython = (
                f"{result.speedup_vs_cython:.2f}x"
                if result.speedup_vs_cython
                else "N/A"
            )
            speedup_nautilus = (
                f"{result.speedup_vs_nautilus:.2f}x"
                if result.speedup_vs_nautilus
                else "N/A"
            )

            print(
                f"{result.indicator:<12} | {result.ta_time:.6f}s | {result.ta_numba_time:.6f}s | {talib_time:<12} | {pandas_time:<12} | {cython_time:<12} | {nautilus_time:<12} | {result.speedup_vs_ta:.2f}x{'':>6} | {speedup_talib:<15} | {speedup_pandas:<16} | {speedup_cython:<16} | {speedup_nautilus:<18}"
            )

        print("-" * 250)
        print()

        # Accuracy comparison table
        print("Accuracy Comparison:")
        print("-" * 120)
        print(
            f"{'Indicator':<12} | {'vs ta':<15} | {'vs ta-lib':<15} | {'vs pandas':<15} | {'vs cython':<15} | {'vs nautilus':<15}"
        )
        print("-" * 120)

        for result in self.results:
            talib_acc = result.accuracy_vs_talib if result.accuracy_vs_talib else "N/A"
            pandas_acc = (
                result.accuracy_vs_pandas if result.accuracy_vs_pandas else "N/A"
            )
            cython_acc = (
                result.accuracy_vs_cython if result.accuracy_vs_cython else "N/A"
            )
            nautilus_acc = (
                result.accuracy_vs_nautilus if result.accuracy_vs_nautilus else "N/A"
            )

            print(
                f"{result.indicator:<12} | {result.accuracy_vs_ta:<15} | {talib_acc:<15} | {pandas_acc:<15} | {cython_acc:<15} | {nautilus_acc:<15}"
            )

        print("-" * 120)
        print()

        # Percentage difference table
        print("Percentage Differences (%):")
        print("-" * 120)
        print(
            f"{'Indicator':<12} | {'vs ta':<15} | {'vs ta-lib':<15} | {'vs pandas':<15} | {'vs cython':<15} | {'vs nautilus':<15}"
        )
        print("-" * 120)

        for result in self.results:
            ta_pct = (
                f"{result.pct_diff_ta:.4f}%"
                if result.pct_diff_ta < float("inf")
                else "N/A"
            )
            talib_pct = (
                f"{result.pct_diff_talib:.4f}%"
                if result.pct_diff_talib is not None
                and result.pct_diff_talib < float("inf")
                else "N/A"
            )
            pandas_pct = (
                f"{result.pct_diff_pandas:.4f}%"
                if result.pct_diff_pandas is not None
                and result.pct_diff_pandas < float("inf")
                else "N/A"
            )
            cython_pct = (
                f"{result.pct_diff_cython:.4f}%"
                if result.pct_diff_cython is not None
                and result.pct_diff_cython < float("inf")
                else "N/A"
            )
            nautilus_pct = (
                f"{result.pct_diff_nautilus:.4f}%"
                if result.pct_diff_nautilus is not None
                and result.pct_diff_nautilus < float("inf")
                else "N/A"
            )

            print(
                f"{result.indicator:<12} | {ta_pct:<15} | {talib_pct:<15} | {pandas_pct:<15} | {cython_pct:<15} | {nautilus_pct:<15}"
            )

        print("-" * 120)
        print()

        # Summary statistics
        valid_speedups_ta = [
            r.speedup_vs_ta for r in self.results if r.speedup_vs_ta != float("inf")
        ]
        valid_speedups_talib = [
            r.speedup_vs_talib for r in self.results if r.speedup_vs_talib is not None
        ]
        valid_speedups_pandas = [
            r.speedup_vs_pandas for r in self.results if r.speedup_vs_pandas is not None
        ]
        valid_speedups_cython = [
            r.speedup_vs_cython for r in self.results if r.speedup_vs_cython is not None
        ]
        valid_speedups_nautilus = [
            r.speedup_vs_nautilus
            for r in self.results
            if r.speedup_vs_nautilus is not None
        ]

        print("Summary Statistics:")
        print(f"Average speedup vs ta: {np.mean(valid_speedups_ta):.2f}x")
        if valid_speedups_talib:
            print(f"Average speedup vs ta-lib: {np.mean(valid_speedups_talib):.2f}x")
        if valid_speedups_pandas:
            print(f"Average speedup vs pandas: {np.mean(valid_speedups_pandas):.2f}x")
        if valid_speedups_cython:
            print(f"Average speedup vs cython: {np.mean(valid_speedups_cython):.2f}x")
        if valid_speedups_nautilus:
            print(
                f"Average speedup vs nautilus: {np.mean(valid_speedups_nautilus):.2f}x"
            )

        identical_ta = sum(1 for r in self.results if r.accuracy_vs_ta == "Identical")
        print(f"Identical results vs ta: {identical_ta}/{len(self.results)}")

        if valid_speedups_talib:
            identical_talib = sum(
                1 for r in self.results if r.accuracy_vs_talib == "Identical"
            )
            print(
                f"Identical results vs ta-lib: {identical_talib}/{len([r for r in self.results if r.accuracy_vs_talib is not None])}"
            )

        if valid_speedups_cython:
            identical_cython = sum(
                1 for r in self.results if r.accuracy_vs_cython == "Identical"
            )
            print(
                f"Identical results vs cython: {identical_cython}/{len([r for r in self.results if r.accuracy_vs_cython is not None])}"
            )

        if valid_speedups_nautilus:
            identical_nautilus = sum(
                1 for r in self.results if r.accuracy_vs_nautilus == "Identical"
            )
            print(
                f"Identical results vs nautilus: {identical_nautilus}/{len([r for r in self.results if r.accuracy_vs_nautilus is not None])}"
            )


def main():
    """Main function to run comprehensive benchmark"""
    # Generate test data
    data_gen = DataGenerator()
    df = data_gen.generate_ohlcv_data(size=100000)

    # Run comprehensive benchmark
    benchmark = ComprehensiveBenchmark()
    benchmark.run_comprehensive_benchmark(df, num_loops=3)


if __name__ == "__main__":
    main()
