Librerías y Herramientas Esenciales

Core Data Science Stack

Pandas - Manipulación de Datos

import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# Configuraciones esenciales para trading
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)
pd.set_option('display.max_colwidth', 50)

class PandasTradingUtils:
    """Utilidades específicas de trading con Pandas"""
    
    @staticmethod
    def resample_ohlc(df, timeframe='5T'):
        """Resample a diferentes timeframes"""
        ohlc_dict = {
            'open': 'first',
            'high': 'max', 
            'low': 'min',
            'close': 'last',
            'volume': 'sum'
        }
        
        resampled = df.resample(timeframe).agg(ohlc_dict)
        return resampled.dropna()
    
    @staticmethod
    def calculate_returns(df, price_col='close'):
        """Calcular diferentes tipos de returns"""
        returns_df = df.copy()
        
        # Simple returns
        returns_df['simple_return'] = df[price_col].pct_change()
        
        # Log returns
        returns_df['log_return'] = np.log(df[price_col] / df[price_col].shift(1))
        
        # Cumulative returns
        returns_df['cumulative_return'] = (1 + returns_df['simple_return']).cumprod() - 1
        
        return returns_df
    
    @staticmethod
    def add_trading_session_flags(df):
        """Agregar flags de sesiones de trading"""
        df = df.copy()
        
        # Convertir a ET si no está ya
        if df.index.tz is None:
            df.index = df.index.tz_localize('UTC').tz_convert('US/Eastern')
        
        # Pre-market: 4:00 AM - 9:30 AM ET
        df['is_premarket'] = (df.index.time >= pd.Timestamp('04:00').time()) & \
                            (df.index.time < pd.Timestamp('09:30').time())
        
        # Regular hours: 9:30 AM - 4:00 PM ET
        df['is_regular_hours'] = (df.index.time >= pd.Timestamp('09:30').time()) & \
                                (df.index.time < pd.Timestamp('16:00').time())
        
        # After hours: 4:00 PM - 8:00 PM ET
        df['is_afterhours'] = (df.index.time >= pd.Timestamp('16:00').time()) & \
                             (df.index.time < pd.Timestamp('20:00').time())
        
        return df
    
    @staticmethod
    def filter_trading_hours(df, session='regular'):
        """Filtrar por sesión de trading"""
        df_with_flags = PandasTradingUtils.add_trading_session_flags(df)
        
        if session == 'regular':
            return df_with_flags[df_with_flags['is_regular_hours']]
        elif session == 'extended':
            return df_with_flags[df_with_flags['is_premarket'] | 
                               df_with_flags['is_regular_hours'] | 
                               df_with_flags['is_afterhours']]
        elif session == 'premarket':
            return df_with_flags[df_with_flags['is_premarket']]
        elif session == 'afterhours':
            return df_with_flags[df_with_flags['is_afterhours']]
        
        return df_with_flags

# Ejemplo de uso
def demo_pandas_utils():
    """Demo de utilidades de Pandas"""
    
    # Crear datos de ejemplo
    dates = pd.date_range('2024-01-01 09:30:00', '2024-01-01 16:00:00', freq='1T')
    sample_data = pd.DataFrame({
        'open': np.random.randn(len(dates)).cumsum() + 100,
        'high': np.random.randn(len(dates)).cumsum() + 102,
        'low': np.random.randn(len(dates)).cumsum() + 98,
        'close': np.random.randn(len(dates)).cumsum() + 100,
        'volume': np.random.randint(1000, 10000, len(dates))
    }, index=dates)
    
    # Resample a 5 minutos
    ohlc_5min = PandasTradingUtils.resample_ohlc(sample_data, '5T')
    print(f"📊 Original: {len(sample_data)} bars, 5min: {len(ohlc_5min)} bars")
    
    # Calcular returns
    returns_data = PandasTradingUtils.calculate_returns(sample_data)
    print(f"💰 Average return: {returns_data['simple_return'].mean():.4f}")
    
    return sample_data

NumPy - Computación Numérica

import numpy as np
from numba import jit
import scipy.stats as stats

class NumPyTradingUtils:
    """Utilidades de NumPy optimizadas para trading"""
    
    @staticmethod
    @jit(nopython=True)
    def fast_rolling_max(arr, window):
        """Rolling max optimizado con Numba"""
        result = np.empty(len(arr))
        result[:window-1] = np.nan
        
        for i in range(window-1, len(arr)):
            result[i] = np.max(arr[i-window+1:i+1])
        
        return result
    
    @staticmethod
    @jit(nopython=True)
    def fast_rolling_min(arr, window):
        """Rolling min optimizado con Numba"""
        result = np.empty(len(arr))
        result[:window-1] = np.nan
        
        for i in range(window-1, len(arr)):
            result[i] = np.min(arr[i-window+1:i+1])
        
        return result
    
    @staticmethod
    @jit(nopython=True)
    def calculate_rsi_fast(prices, period=14):
        """RSI optimizado con Numba"""
        deltas = np.diff(prices)
        gains = np.where(deltas > 0, deltas, 0)
        losses = np.where(deltas < 0, -deltas, 0)
        
        avg_gains = np.zeros(len(gains))
        avg_losses = np.zeros(len(losses))
        
        # Initial SMA
        avg_gains[period-1] = np.mean(gains[:period])
        avg_losses[period-1] = np.mean(losses[:period])
        
        # EMA calculation
        for i in range(period, len(gains)):
            avg_gains[i] = (avg_gains[i-1] * (period-1) + gains[i]) / period
            avg_losses[i] = (avg_losses[i-1] * (period-1) + losses[i]) / period
        
        rs = avg_gains / avg_losses
        rsi = 100 - (100 / (1 + rs))
        
        return rsi
    
    @staticmethod
    def calculate_sharpe_ratio(returns, risk_free_rate=0.02):
        """Calcular Sharpe ratio"""
        excess_returns = returns - risk_free_rate/252  # Daily risk-free rate
        return np.sqrt(252) * np.mean(excess_returns) / np.std(excess_returns)
    
    @staticmethod
    def calculate_max_drawdown(cumulative_returns):
        """Calcular maximum drawdown"""
        peak = np.maximum.accumulate(cumulative_returns)
        drawdown = (cumulative_returns - peak) / peak
        return np.min(drawdown)
    
    @staticmethod
    def calculate_var(returns, confidence_level=0.05):
        """Calcular Value at Risk"""
        return np.percentile(returns, confidence_level * 100)
    
    @staticmethod
    def calculate_cvar(returns, confidence_level=0.05):
        """Calcular Conditional Value at Risk (Expected Shortfall)"""
        var = NumPyTradingUtils.calculate_var(returns, confidence_level)
        return np.mean(returns[returns <= var])

# Ejemplo de uso
def demo_numpy_utils():
    """Demo de utilidades NumPy"""
    
    # Generar returns sintéticos
    np.random.seed(42)
    returns = np.random.normal(0.001, 0.02, 252)  # 1 año de returns diarios
    prices = 100 * np.exp(np.cumsum(returns))
    
    # Calcular métricas
    sharpe = NumPyTradingUtils.calculate_sharpe_ratio(returns)
    max_dd = NumPyTradingUtils.calculate_max_drawdown(np.cumsum(returns))
    var_5 = NumPyTradingUtils.calculate_var(returns, 0.05)
    cvar_5 = NumPyTradingUtils.calculate_cvar(returns, 0.05)
    
    print(f"📊 Métricas de Performance:")
    print(f"Sharpe Ratio: {sharpe:.2f}")
    print(f"Max Drawdown: {max_dd:.2%}")
    print(f"VaR (5%): {var_5:.2%}")
    print(f"CVaR (5%): {cvar_5:.2%}")
    
    # RSI rápido
    rsi = NumPyTradingUtils.calculate_rsi_fast(prices)
    print(f"RSI actual: {rsi[-1]:.1f}")

Technical Analysis Libraries

TA-Lib - Indicadores Técnicos

import talib

class TALibIntegration:
    """Integración completa con TA-Lib"""
    
    @staticmethod
    def comprehensive_analysis(df):
        """Análisis técnico comprehensivo"""
        
        # Preparar datos
        high = df['high'].values
        low = df['low'].values
        close = df['close'].values
        volume = df['volume'].values
        
        analysis = {}
        
        # Trend Indicators
        analysis['sma_20'] = talib.SMA(close, timeperiod=20)
        analysis['ema_20'] = talib.EMA(close, timeperiod=20)
        analysis['bbands_upper'], analysis['bbands_middle'], analysis['bbands_lower'] = \
            talib.BBANDS(close, timeperiod=20, nbdevup=2, nbdevdn=2)
        
        # Momentum Indicators
        analysis['rsi'] = talib.RSI(close, timeperiod=14)
        analysis['macd'], analysis['macd_signal'], analysis['macd_hist'] = \
            talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)
        analysis['stoch_k'], analysis['stoch_d'] = \
            talib.STOCH(high, low, close, fastk_period=14, slowk_period=3, slowd_period=3)
        
        # Volume Indicators
        analysis['obv'] = talib.OBV(close, volume)
        analysis['ad'] = talib.AD(high, low, close, volume)
        analysis['adosc'] = talib.ADOSC(high, low, close, volume, fastperiod=3, slowperiod=10)
        
        # Volatility Indicators
        analysis['atr'] = talib.ATR(high, low, close, timeperiod=14)
        analysis['natr'] = talib.NATR(high, low, close, timeperiod=14)
        
        # Pattern Recognition
        analysis['doji'] = talib.CDLDOJI(df['open'].values, high, low, close)
        analysis['hammer'] = talib.CDLHAMMER(df['open'].values, high, low, close)
        analysis['shooting_star'] = talib.CDLSHOOTINGSTAR(df['open'].values, high, low, close)
        analysis['engulfing'] = talib.CDLENGULFING(df['open'].values, high, low, close)
        
        # Cycle Indicators
        analysis['ht_trendmode'] = talib.HT_TRENDMODE(close)
        
        return analysis
    
    @staticmethod
    def get_all_patterns(df):
        """Obtener todos los patrones de candlestick"""
        
        open_prices = df['open'].values
        high = df['high'].values
        low = df['low'].values
        close = df['close'].values
        
        patterns = {}
        
        # Lista de todos los patrones disponibles en TA-Lib
        pattern_functions = [
            'CDL2CROWS', 'CDL3BLACKCROWS', 'CDL3INSIDE', 'CDL3LINESTRIKE',
            'CDL3OUTSIDE', 'CDL3STARSINSOUTH', 'CDL3WHITESOLDIERS', 'CDLABANDONEDBABY',
            'CDLADVANCEBLOCK', 'CDLBELTHOLD', 'CDLBREAKAWAY', 'CDLCLOSINGMARUBOZU',
            'CDLCONCEALBABYSWALL', 'CDLCOUNTERATTACK', 'CDLDARKCLOUDCOVER', 'CDLDOJI',
            'CDLDOJISTAR', 'CDLDRAGONFLYDOJI', 'CDLENGULFING', 'CDLEVENINGDOJISTAR',
            'CDLEVENINGSTAR', 'CDLGAPSIDESIDEWHITE', 'CDLGRAVESTONEDOJI', 'CDLHAMMER',
            'CDLHANGINGMAN', 'CDLHARAMI', 'CDLHARAMICROSS', 'CDLHIGHWAVE',
            'CDLHIKKAKE', 'CDLHIKKAKEMOD', 'CDLHOMINGPIGEON', 'CDLIDENTICAL3CROWS',
            'CDLINNECK', 'CDLINVERTEDHAMMER', 'CDLKICKING', 'CDLKICKINGBYLENGTH',
            'CDLLADDERBOTTOM', 'CDLLONGLEGGEDDOJI', 'CDLLONGLINE', 'CDLMARUBOZU',
            'CDLMATCHINGLOW', 'CDLMATHOLD', 'CDLMORNINGDOJISTAR', 'CDLMORNINGSTAR',
            'CDLONNECK', 'CDLPIERCING', 'CDLRICKSHAWMAN', 'CDLRISEFALL3METHODS',
            'CDLSEPARATINGLINES', 'CDLSHOOTINGSTAR', 'CDLSHORTLINE', 'CDLSPINNINGTOP',
            'CDLSTALLEDPATTERN', 'CDLSTICKSANDWICH', 'CDLTAKURI', 'CDLTASUKIGAP',
            'CDLTHRUSTING', 'CDLTRISTAR', 'CDLUNIQUE3RIVER', 'CDLUPSIDEGAP2CROWS',
            'CDLXSIDEGAP3METHODS'
        ]
        
        for pattern_name in pattern_functions:
            try:
                pattern_func = getattr(talib, pattern_name)
                patterns[pattern_name] = pattern_func(open_prices, high, low, close)
            except:
                continue
        
        return patterns
    
    @staticmethod
    def detect_active_patterns(df, min_strength=80):
        """Detectar patrones activos con fuerza mínima"""
        
        patterns = TALibIntegration.get_all_patterns(df)
        active_patterns = {}
        
        for pattern_name, pattern_values in patterns.items():
            if len(pattern_values) > 0:
                current_value = pattern_values[-1]
                if abs(current_value) >= min_strength:
                    active_patterns[pattern_name] = {
                        'strength': current_value,
                        'bullish': current_value > 0,
                        'bearish': current_value < 0
                    }
        
        return active_patterns

# Ejemplo de uso
def demo_talib_integration():
    """Demo de integración TA-Lib"""
    
    # Crear datos de ejemplo
    dates = pd.date_range('2024-01-01', periods=100, freq='D')
    sample_data = pd.DataFrame({
        'open': np.random.randn(100).cumsum() + 100,
        'high': np.random.randn(100).cumsum() + 102,
        'low': np.random.randn(100).cumsum() + 98,
        'close': np.random.randn(100).cumsum() + 100,
        'volume': np.random.randint(100000, 1000000, 100)
    }, index=dates)
    
    # Análisis comprehensivo
    analysis = TALibIntegration.comprehensive_analysis(sample_data)
    
    print("📊 Análisis Técnico:")
    print(f"RSI actual: {analysis['rsi'][-1]:.1f}")
    print(f"MACD: {analysis['macd'][-1]:.3f}")
    print(f"ATR: {analysis['atr'][-1]:.2f}")
    
    # Detectar patrones activos
    active_patterns = TALibIntegration.detect_active_patterns(sample_data)
    if active_patterns:
        print(f"\n🕯️ Patrones Activos:")
        for pattern, info in active_patterns.items():
            direction = "📈 Bullish" if info['bullish'] else "📉 Bearish"
            print(f"{pattern}: {direction} (Strength: {info['strength']})")

Custom Indicators

class CustomIndicators:
    """Indicadores personalizados para small cap trading"""
    
    @staticmethod
    def vwap(df):
        """Volume Weighted Average Price"""
        typical_price = (df['high'] + df['low'] + df['close']) / 3
        cumulative_tp_volume = (typical_price * df['volume']).cumsum()
        cumulative_volume = df['volume'].cumsum()
        
        return cumulative_tp_volume / cumulative_volume
    
    @staticmethod
    def anchored_vwap(df, anchor_date):
        """VWAP anclado desde fecha específica"""
        anchor_index = df.index.get_loc(anchor_date, method='nearest')
        anchor_df = df.iloc[anchor_index:]
        
        return CustomIndicators.vwap(anchor_df)
    
    @staticmethod
    def relative_volume(df, lookback_periods=20):
        """Volumen relativo vs promedio"""
        avg_volume = df['volume'].rolling(window=lookback_periods).mean()
        return df['volume'] / avg_volume
    
    @staticmethod
    def gap_percentage(df):
        """Porcentaje de gap vs cierre anterior"""
        prev_close = df['close'].shift(1)
        gap_pct = (df['open'] - prev_close) / prev_close
        return gap_pct
    
    @staticmethod
    def daily_range_percentage(df):
        """Rango diario como porcentaje"""
        return (df['high'] - df['low']) / df['low']
    
    @staticmethod
    def money_flow_index(df, period=14):
        """Money Flow Index - RSI basado en volumen"""
        typical_price = (df['high'] + df['low'] + df['close']) / 3
        money_flow = typical_price * df['volume']
        
        # Positive and negative money flow
        positive_flow = pd.Series(index=df.index, dtype=float)
        negative_flow = pd.Series(index=df.index, dtype=float)
        
        for i in range(1, len(df)):
            if typical_price.iloc[i] > typical_price.iloc[i-1]:
                positive_flow.iloc[i] = money_flow.iloc[i]
                negative_flow.iloc[i] = 0
            elif typical_price.iloc[i] < typical_price.iloc[i-1]:
                negative_flow.iloc[i] = money_flow.iloc[i]
                positive_flow.iloc[i] = 0
            else:
                positive_flow.iloc[i] = 0
                negative_flow.iloc[i] = 0
        
        # Calculate MFI
        positive_mf = positive_flow.rolling(window=period).sum()
        negative_mf = negative_flow.rolling(window=period).sum()
        
        money_ratio = positive_mf / negative_mf
        mfi = 100 - (100 / (1 + money_ratio))
        
        return mfi
    
    @staticmethod
    def squeeze_indicator(df, bb_period=20, kc_period=20, kc_mult=1.5):
        """Bollinger Bands / Keltner Channel Squeeze"""
        
        # Bollinger Bands
        bb_middle = df['close'].rolling(window=bb_period).mean()
        bb_std = df['close'].rolling(window=bb_period).std()
        bb_upper = bb_middle + (bb_std * 2)
        bb_lower = bb_middle - (bb_std * 2)
        
        # Keltner Channels
        kc_middle = df['close'].rolling(window=kc_period).mean()
        atr = CustomIndicators.true_range(df).rolling(window=kc_period).mean()
        kc_upper = kc_middle + (atr * kc_mult)
        kc_lower = kc_middle - (atr * kc_mult)
        
        # Squeeze condition
        squeeze = (bb_upper < kc_upper) & (bb_lower > kc_lower)
        
        return squeeze
    
    @staticmethod
    def true_range(df):
        """True Range calculation"""
        prev_close = df['close'].shift(1)
        
        tr1 = df['high'] - df['low']
        tr2 = np.abs(df['high'] - prev_close)
        tr3 = np.abs(df['low'] - prev_close)
        
        return np.maximum(tr1, np.maximum(tr2, tr3))
    
    @staticmethod
    def consecutive_bars(df, direction='up'):
        """Contar barras consecutivas en una dirección"""
        if direction == 'up':
            condition = df['close'] > df['open']
        else:
            condition = df['close'] < df['open']
        
        # Reset counter when condition changes
        groups = condition.ne(condition.shift()).cumsum()
        consecutive = condition.groupby(groups).cumsum()
        
        # Only count when condition is true
        consecutive = consecutive.where(condition, 0)
        
        return consecutive
    
    @staticmethod
    def pivot_points(df, method='traditional'):
        """Calcular pivot points"""
        
        if method == 'traditional':
            pivot = (df['high'] + df['low'] + df['close']) / 3
            
            resistance_1 = 2 * pivot - df['low']
            support_1 = 2 * pivot - df['high']
            
            resistance_2 = pivot + (df['high'] - df['low'])
            support_2 = pivot - (df['high'] - df['low'])
            
            resistance_3 = df['high'] + 2 * (pivot - df['low'])
            support_3 = df['low'] - 2 * (df['high'] - pivot)
            
            return {
                'pivot': pivot,
                'r1': resistance_1, 'r2': resistance_2, 'r3': resistance_3,
                's1': support_1, 's2': support_2, 's3': support_3
            }
        
        elif method == 'fibonacci':
            pivot = (df['high'] + df['low'] + df['close']) / 3
            range_hl = df['high'] - df['low']
            
            resistance_1 = pivot + 0.382 * range_hl
            resistance_2 = pivot + 0.618 * range_hl
            resistance_3 = pivot + range_hl
            
            support_1 = pivot - 0.382 * range_hl
            support_2 = pivot - 0.618 * range_hl
            support_3 = pivot - range_hl
            
            return {
                'pivot': pivot,
                'r1': resistance_1, 'r2': resistance_2, 'r3': resistance_3,
                's1': support_1, 's2': support_2, 's3': support_3
            }

# Demo de indicadores personalizados
def demo_custom_indicators():
    """Demo de indicadores personalizados"""
    
    # Datos de ejemplo
    dates = pd.date_range('2024-01-01', periods=50, freq='D')
    sample_data = pd.DataFrame({
        'open': np.random.randn(50).cumsum() + 100,
        'high': np.random.randn(50).cumsum() + 102,
        'low': np.random.randn(50).cumsum() + 98,
        'close': np.random.randn(50).cumsum() + 100,
        'volume': np.random.randint(100000, 1000000, 50)
    }, index=dates)
    
    # Calcular indicadores
    vwap = CustomIndicators.vwap(sample_data)
    rvol = CustomIndicators.relative_volume(sample_data)
    gap_pct = CustomIndicators.gap_percentage(sample_data)
    mfi = CustomIndicators.money_flow_index(sample_data)
    squeeze = CustomIndicators.squeeze_indicator(sample_data)
    
    print("🔧 Indicadores Personalizados:")
    print(f"VWAP actual: ${vwap.iloc[-1]:.2f}")
    print(f"Relative Volume: {rvol.iloc[-1]:.2f}x")
    print(f"Gap %: {gap_pct.iloc[-1]:.2%}")
    print(f"MFI: {mfi.iloc[-1]:.1f}")
    print(f"En squeeze: {'Sí' if squeeze.iloc[-1] else 'No'}")

Visualization Libraries

Matplotlib & Seaborn

import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.dates as mdates
from matplotlib.patches import Rectangle

# Configuración de estilo
plt.style.use('dark_background')
sns.set_palette("husl")

class TradingVisualization:
    """Visualizaciones específicas para trading"""
    
    @staticmethod
    def setup_plot_style():
        """Configurar estilo de plots"""
        plt.rcParams['figure.figsize'] = (15, 8)
        plt.rcParams['font.size'] = 10
        plt.rcParams['axes.grid'] = True
        plt.rcParams['grid.alpha'] = 0.3
        
    @staticmethod
    def candlestick_chart(df, indicators=None, title=""):
        """Gráfico de candlestick con indicadores"""
        
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10), 
                                      gridspec_kw={'height_ratios': [3, 1]})
        
        # Candlestick chart
        for i in range(len(df)):
            date = df.index[i]
            open_price = df['open'].iloc[i]
            high_price = df['high'].iloc[i]
            low_price = df['low'].iloc[i] 
            close_price = df['close'].iloc[i]
            
            color = 'green' if close_price > open_price else 'red'
            
            # Wick
            ax1.plot([i, i], [low_price, high_price], color='gray', linewidth=1)
            
            # Body
            body_height = abs(close_price - open_price)
            body_bottom = min(open_price, close_price)
            
            rect = Rectangle((i-0.3, body_bottom), 0.6, body_height, 
                           facecolor=color, alpha=0.7)
            ax1.add_patch(rect)
        
        # Add indicators if provided
        if indicators:
            if 'sma_20' in indicators:
                ax1.plot(range(len(df)), indicators['sma_20'], 
                        label='SMA 20', alpha=0.7)
            if 'vwap' in indicators:
                ax1.plot(range(len(df)), indicators['vwap'], 
                        label='VWAP', alpha=0.7)
        
        ax1.set_title(title)
        ax1.set_ylabel('Price')
        ax1.legend()
        
        # Volume chart
        colors = ['green' if df['close'].iloc[i] > df['open'].iloc[i] 
                 else 'red' for i in range(len(df))]
        ax2.bar(range(len(df)), df['volume'], color=colors, alpha=0.7)
        ax2.set_ylabel('Volume')
        ax2.set_xlabel('Time')
        
        # Format x-axis
        if len(df) < 50:
            step = 1
        elif len(df) < 200:
            step = 5
        else:
            step = len(df) // 20
            
        ax1.set_xticks(range(0, len(df), step))
        ax1.set_xticklabels([df.index[i].strftime('%m/%d') 
                            for i in range(0, len(df), step)], rotation=45)
        
        ax2.set_xticks(range(0, len(df), step))
        ax2.set_xticklabels([df.index[i].strftime('%m/%d') 
                            for i in range(0, len(df), step)], rotation=45)
        
        plt.tight_layout()
        return fig
    
    @staticmethod
    def performance_dashboard(returns, title="Performance Dashboard"):
        """Dashboard de performance comprehensivo"""
        
        fig = plt.figure(figsize=(20, 12))
        
        # Cumulative returns
        ax1 = plt.subplot(2, 3, 1)
        cumulative_returns = (1 + returns).cumprod()
        ax1.plot(cumulative_returns.index, cumulative_returns.values)
        ax1.set_title('Cumulative Returns')
        ax1.set_ylabel('Cumulative Return')
        
        # Returns distribution
        ax2 = plt.subplot(2, 3, 2)
        ax2.hist(returns, bins=50, alpha=0.7, edgecolor='black')
        ax2.axvline(returns.mean(), color='red', linestyle='--', 
                   label=f'Mean: {returns.mean():.4f}')
        ax2.set_title('Returns Distribution')
        ax2.set_xlabel('Daily Return')
        ax2.legend()
        
        # Rolling Sharpe ratio
        ax3 = plt.subplot(2, 3, 3)
        rolling_sharpe = returns.rolling(60).apply(
            lambda x: np.sqrt(252) * x.mean() / x.std()
        )
        ax3.plot(rolling_sharpe.index, rolling_sharpe.values)
        ax3.set_title('Rolling 60-Day Sharpe Ratio')
        ax3.set_ylabel('Sharpe Ratio')
        
        # Drawdown
        ax4 = plt.subplot(2, 3, 4)
        cumulative = (1 + returns).cumprod()
        running_max = cumulative.expanding().max()
        drawdown = (cumulative - running_max) / running_max
        ax4.fill_between(drawdown.index, drawdown.values, 0, 
                        alpha=0.3, color='red')
        ax4.set_title('Drawdown')
        ax4.set_ylabel('Drawdown %')
        
        # Monthly returns heatmap
        ax5 = plt.subplot(2, 3, 5)
        monthly_returns = returns.resample('M').apply(lambda x: (1 + x).prod() - 1)
        monthly_returns_pct = monthly_returns * 100
        
        # Create heatmap data
        years = monthly_returns.index.year.unique()
        months = range(1, 13)
        heatmap_data = np.full((len(years), 12), np.nan)
        
        for i, year in enumerate(years):
            year_data = monthly_returns_pct[monthly_returns_pct.index.year == year]
            for month_ret in year_data:
                month = year_data[year_data == month_ret].index[0].month
                heatmap_data[i, month-1] = month_ret
        
        sns.heatmap(heatmap_data, 
                   xticklabels=['Jan','Feb','Mar','Apr','May','Jun',
                               'Jul','Aug','Sep','Oct','Nov','Dec'],
                   yticklabels=years,
                   annot=True, fmt='.1f', cmap='RdYlGn', center=0,
                   ax=ax5)
        ax5.set_title('Monthly Returns Heatmap (%)')
        
        # Risk/Return scatter
        ax6 = plt.subplot(2, 3, 6)
        annual_return = returns.mean() * 252
        annual_vol = returns.std() * np.sqrt(252)
        ax6.scatter(annual_vol, annual_return, s=100, alpha=0.7)
        ax6.set_xlabel('Annual Volatility')
        ax6.set_ylabel('Annual Return')
        ax6.set_title('Risk/Return Profile')
        
        plt.suptitle(title, fontsize=16)
        plt.tight_layout()
        return fig

# Demo de visualizaciones
def demo_trading_visualization():
    """Demo de visualizaciones de trading"""
    
    # Configurar estilo
    TradingVisualization.setup_plot_style()
    
    # Datos de ejemplo
    dates = pd.date_range('2024-01-01', periods=60, freq='D')
    sample_data = pd.DataFrame({
        'open': np.random.randn(60).cumsum() + 100,
        'high': np.random.randn(60).cumsum() + 102,
        'low': np.random.randn(60).cumsum() + 98,
        'close': np.random.randn(60).cumsum() + 100,
        'volume': np.random.randint(100000, 1000000, 60)
    }, index=dates)
    
    # Indicadores
    indicators = {
        'sma_20': sample_data['close'].rolling(20).mean(),
        'vwap': CustomIndicators.vwap(sample_data)
    }
    
    # Gráfico de candlestick
    fig1 = TradingVisualization.candlestick_chart(
        sample_data, indicators, "Sample Stock - Candlestick Chart"
    )
    plt.show()
    
    # Dashboard de performance
    returns = sample_data['close'].pct_change().dropna()
    fig2 = TradingVisualization.performance_dashboard(
        returns, "Sample Strategy Performance"
    )
    plt.show()

Estas librerías forman el core stack para análisis cuantitativo y visualización de datos de trading, optimizadas específicamente para small cap trading strategies.