Spike HOD/LOD (High/Low of Day)

El Arte del Timing

En small caps, los spikes de HOD (High of Day) y LOD (Low of Day) son momentos decisivos. Un break del HOD con volumen puede significar el inicio de un runner. Un break del LOD puede ser capitulación.

Tracking HOD/LOD en Tiempo Real

def track_hod_lod(df):
    """Trackear HOD/LOD durante el día"""
    # Agrupar por fecha
    df['date'] = df.index.date
    
    # HOD/LOD running
    df['hod'] = df.groupby('date')['high'].cummax()
    df['lod'] = df.groupby('date')['low'].cummin()
    
    # Distancia de HOD/LOD
    df['distance_from_hod'] = (df['hod'] - df['close']) / df['close'] * 100
    df['distance_from_lod'] = (df['close'] - df['lod']) / df['lod'] * 100
    
    # Tiempo desde último HOD/LOD
    df['is_new_hod'] = df['high'] >= df['hod']
    df['is_new_lod'] = df['low'] <= df['lod']
    
    # Minutes since HOD/LOD
    df['minutes_since_hod'] = 0
    df['minutes_since_lod'] = 0
    
    for date in df['date'].unique():
        mask = df['date'] == date
        daily_data = df[mask].copy()
        
        last_hod_idx = 0
        last_lod_idx = 0
        
        for i, (idx, row) in enumerate(daily_data.iterrows()):
            if row['is_new_hod']:
                last_hod_idx = i
            if row['is_new_lod']:
                last_lod_idx = i
                
            df.loc[idx, 'minutes_since_hod'] = i - last_hod_idx
            df.loc[idx, 'minutes_since_lod'] = i - last_lod_idx
    
    return df

HOD Spike Setup

def hod_spike_setup(df, volume_threshold=2, strength_threshold=0.02):
    """Detectar spike de HOD con confirmación"""
    df = track_hod_lod(df)
    df = calculate_rvol(df)
    
    # Condiciones para HOD spike
    df['hod_break'] = (
        df['is_new_hod'] &  # Nuevo HOD
        (df['rvol'] > volume_threshold) &  # Volumen alto
        (df['close'] > df['open'])  # Vela verde
    )
    
    # Fuerza del break
    df['hod_break_strength'] = np.where(
        df['hod_break'],
        (df['close'] - df['hod'].shift(1)) / df['hod'].shift(1) * 100,
        0
    )
    
    # Clasificar calidad del break
    df['hod_quality'] = pd.cut(
        df['hod_break_strength'],
        bins=[0, 0.5, 2, 5, np.inf],
        labels=['weak', 'decent', 'strong', 'explosive']
    )
    
    # Continuación del spike
    df['hod_continuation'] = (
        df['hod_break'] &
        (df['hod_break_strength'] > strength_threshold) &
        (df['close'] > df['hod'].shift(1) * 1.01)  # 1% sobre HOD anterior
    )
    
    return df

LOD Bounce Detection

def lod_bounce_setup(df, bounce_threshold=0.03):
    """Detectar bounces del LOD"""
    df = track_hod_lod(df)
    
    # Test del LOD
    df['lod_test'] = (
        (df['low'] <= df['lod'] * 1.001) &  # Cerca del LOD
        (df['close'] > df['lod'] * 1.01)    # Cierra 1% arriba del LOD
    )
    
    # Fuerza del bounce
    df['bounce_strength'] = np.where(
        df['lod_test'],
        (df['close'] - df['low']) / df['low'] * 100,
        0
    )
    
    # Double bottom
    df['double_bottom'] = (
        df['lod_test'] &
        (df['minutes_since_lod'] > 30) &  # Al menos 30 min desde último LOD
        (abs(df['low'] - df['lod']) / df['lod'] < 0.005)  # Dentro del 0.5%
    )
    
    # Hammer/Doji en LOD
    df['hammer_at_lod'] = (
        df['lod_test'] &
        ((df['close'] - df['low']) / (df['high'] - df['low']) > 0.7) &  # Close en top 30%
        ((df['high'] - df['low']) / df['open'] > 0.02)  # Rango mínimo 2%
    )
    
    return df

Multi-Day HOD/LOD Levels

def multi_day_levels(ticker, lookback_days=5):
    """Obtener niveles HOD/LOD de múltiples días"""
    levels = {}
    
    for i in range(lookback_days):
        date = pd.Timestamp.now().date() - pd.Timedelta(days=i)
        
        try:
            daily_data = get_intraday_data(ticker, date)
            levels[date] = {
                'hod': daily_data['high'].max(),
                'lod': daily_data['low'].min(),
                'volume': daily_data['volume'].sum(),
                'range_pct': (daily_data['high'].max() - daily_data['low'].min()) / daily_data['low'].min() * 100
            }
        except:
            continue
    
    # Crear DataFrame de niveles
    levels_df = pd.DataFrame(levels).T
    
    # Identificar niveles clave
    levels_df['key_resistance'] = levels_df['hod'] > levels_df['hod'].quantile(0.8)
    levels_df['key_support'] = levels_df['lod'] < levels_df['lod'].quantile(0.2)
    
    return levels_df

Time-Based HOD/LOD Analysis

def hod_lod_by_time(df):
    """Analizar cuándo ocurren típicamente HOD/LOD"""
    df = track_hod_lod(df)
    
    # Agregar timestamp info
    df['hour'] = df.index.hour
    df['minute'] = df.index.minute
    df['time_of_day'] = df.index.time
    
    # Frecuencia de HOD por hora
    hod_times = df[df['is_new_hod']]['hour'].value_counts().sort_index()
    lod_times = df[df['is_new_lod']]['hour'].value_counts().sort_index()
    
    # Probabilidad de HOD/LOD por período
    time_periods = {
        'opening': (9, 10),
        'morning': (10, 12),
        'midday': (12, 14),
        'afternoon': (14, 16)
    }
    
    hod_probabilities = {}
    lod_probabilities = {}
    
    for period, (start, end) in time_periods.items():
        period_mask = (df['hour'] >= start) & (df['hour'] < end)
        
        hod_in_period = df[period_mask & df['is_new_hod']].shape[0]
        lod_in_period = df[period_mask & df['is_new_lod']].shape[0]
        total_in_period = df[period_mask].shape[0]
        
        hod_probabilities[period] = hod_in_period / total_in_period
        lod_probabilities[period] = lod_in_period / total_in_period
    
    return {
        'hod_by_hour': hod_times,
        'lod_by_hour': lod_times,
        'hod_probabilities': hod_probabilities,
        'lod_probabilities': lod_probabilities
    }

Failed Break Analysis

def analyze_failed_breaks(df, failure_threshold=0.005):
    """Analizar breaks fallidos de HOD/LOD"""
    df = track_hod_lod(df)
    
    # Identificar breaks iniciales
    df['hod_break_attempt'] = df['high'] > df['hod'].shift(1)
    df['lod_break_attempt'] = df['low'] < df['lod'].shift(1)
    
    # Failed breaks
    df['failed_hod_break'] = (
        df['hod_break_attempt'] &
        (df['close'] < df['hod'].shift(1) * (1 + failure_threshold))
    )
    
    df['failed_lod_break'] = (
        df['lod_break_attempt'] &
        (df['close'] > df['lod'].shift(1) * (1 - failure_threshold))
    )
    
    # Strength of rejection
    df['hod_rejection_strength'] = np.where(
        df['failed_hod_break'],
        (df['hod'].shift(1) - df['close']) / df['close'] * 100,
        0
    )
    
    df['lod_rejection_strength'] = np.where(
        df['failed_lod_break'],
        (df['close'] - df['lod'].shift(1)) / df['lod'].shift(1) * 100,
        0
    )
    
    return df

Progressive HOD/LOD Strategy

def progressive_hod_strategy(df, position_sizes=[0.25, 0.25, 0.5]):
    """Estrategia de entries progresivos en HOD breaks"""
    df = hod_spike_setup(df)
    
    # Diferentes niveles de confirmación
    df['hod_level_1'] = df['hod_break']  # Break inicial
    df['hod_level_2'] = (  # Break con volumen
        df['hod_break'] & 
        (df['rvol'] > 3)
    )
    df['hod_level_3'] = (  # Break fuerte con continuación
        df['hod_continuation'] &
        (df['hod_break_strength'] > 2)
    )
    
    # Backtesting con entries progresivos
    signals = []
    
    for i, row in df.iterrows():
        if row['hod_level_1']:
            signals.append({
                'timestamp': i,
                'entry_level': 1,
                'price': row['close'],
                'size': position_sizes[0],
                'stop': row['hod'] * 0.98
            })
        
        if row['hod_level_2']:
            signals.append({
                'timestamp': i,
                'entry_level': 2,
                'price': row['close'],
                'size': position_sizes[1],
                'stop': row['hod'] * 0.99
            })
        
        if row['hod_level_3']:
            signals.append({
                'timestamp': i,
                'entry_level': 3,
                'price': row['close'],
                'size': position_sizes[2],
                'stop': row['lod']  # Wider stop for strongest signal
            })
    
    return pd.DataFrame(signals)

Gap and HOD Combination

def gap_hod_combo(df, gap_threshold=10):
    """Combinar gap analysis con HOD breaks"""
    # Calcular gap
    df['gap_pct'] = (df['open'] - df['close'].shift(1)) / df['close'].shift(1) * 100
    
    df = track_hod_lod(df)
    df = hod_spike_setup(df)
    
    # Gap up + holding gains + HOD break
    df['gap_hod_setup'] = (
        (df['gap_pct'] > gap_threshold) &  # Gap up significativo
        (df['close'] > df['open']) &       # Manteniendo gains
        df['hod_break']                    # Breaking HOD
    )
    
    # Classify setup strength
    df['setup_strength'] = 0
    df.loc[df['gap_hod_setup'] & (df['gap_pct'] > 20), 'setup_strength'] = 3
    df.loc[df['gap_hod_setup'] & (df['gap_pct'] > 15), 'setup_strength'] = 2
    df.loc[df['gap_hod_setup'], 'setup_strength'] = 1
    
    return df

Real-Time Monitoring

class HODLODMonitor:
    def __init__(self, ticker):
        self.ticker = ticker
        self.daily_hod = 0
        self.daily_lod = float('inf')
        self.hod_breaks = []
        self.lod_tests = []
        
    def update(self, new_bar):
        """Update con nueva barra"""
        # Actualizar HOD/LOD
        if new_bar['high'] > self.daily_hod:
            self.daily_hod = new_bar['high']
            self.hod_breaks.append({
                'time': new_bar.name,
                'price': new_bar['high'],
                'volume': new_bar['volume']
            })
        
        if new_bar['low'] < self.daily_lod:
            self.daily_lod = new_bar['low']
            self.lod_tests.append({
                'time': new_bar.name,
                'price': new_bar['low'],
                'volume': new_bar['volume']
            })
    
    def get_status(self):
        """Estado actual"""
        return {
            'ticker': self.ticker,
            'hod': self.daily_hod,
            'lod': self.daily_lod,
            'hod_breaks_count': len(self.hod_breaks),
            'lod_tests_count': len(self.lod_tests),
            'latest_hod_break': self.hod_breaks[-1] if self.hod_breaks else None,
            'latest_lod_test': self.lod_tests[-1] if self.lod_tests else None
        }

Alertas HOD/LOD

def hod_lod_alerts(df, ticker):
    """Generar alertas para HOD/LOD"""
    alerts = []
    latest = df.iloc[-1]
    
    # HOD break
    if latest.get('hod_break', False):
        alerts.append(f"🚀 {ticker}: NEW HOD @ ${latest['hod']:.2f} (RVol: {latest['rvol']:.1f}x)")
    
    # Strong HOD continuation
    if latest.get('hod_continuation', False):
        alerts.append(f"💪 {ticker}: HOD CONTINUATION - Strength: {latest['hod_break_strength']:.1f}%")
    
    # LOD bounce
    if latest.get('lod_test', False):
        alerts.append(f"⚡ {ticker}: LOD BOUNCE @ ${latest['lod']:.2f}")
    
    # Failed break (reversal)
    if latest.get('failed_hod_break', False):
        alerts.append(f"⚠️ {ticker}: FAILED HOD BREAK - Rejection at ${latest['hod']:.2f}")
    
    return alerts

Siguiente Paso

Finalizemos los indicadores con Gap % y Float, fundamentales para el screening de small caps.