🇪🇸 Leer en Español 🇺🇸 English

Spike HOD/LOD (High/Low of Day)

The Art of Timing

In small caps, HOD (High of Day) and LOD (Low of Day) spikes are decisive moments. An HOD break with volume can signal the start of a runner. An LOD break can mean capitulation.

Real-Time HOD/LOD Tracking

def track_hod_lod(df):
    """Track HOD/LOD during the day"""
    # Group by date
    df['date'] = df.index.date
    
    # Running HOD/LOD
    df['hod'] = df.groupby('date')['high'].cummax()
    df['lod'] = df.groupby('date')['low'].cummin()
    
    # Distance from HOD/LOD
    df['distance_from_hod'] = (df['hod'] - df['close']) / df['close'] * 100
    df['distance_from_lod'] = (df['close'] - df['lod']) / df['lod'] * 100
    
    # Time since last 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):
    """Detect HOD spike with confirmation"""
    df = track_hod_lod(df)
    df = calculate_rvol(df)
    
    # Conditions for HOD spike
    df['hod_break'] = (
        df['is_new_hod'] &  # New HOD
        (df['rvol'] > volume_threshold) &  # High volume
        (df['close'] > df['open'])  # Green candle
    )
    
    # Break strength
    df['hod_break_strength'] = np.where(
        df['hod_break'],
        (df['close'] - df['hod'].shift(1)) / df['hod'].shift(1) * 100,
        0
    )
    
    # Classify break quality
    df['hod_quality'] = pd.cut(
        df['hod_break_strength'],
        bins=[0, 0.5, 2, 5, np.inf],
        labels=['weak', 'decent', 'strong', 'explosive']
    )
    
    # Spike continuation
    df['hod_continuation'] = (
        df['hod_break'] &
        (df['hod_break_strength'] > strength_threshold) &
        (df['close'] > df['hod'].shift(1) * 1.01)  # 1% above previous HOD
    )
    
    return df

LOD Bounce Detection

def lod_bounce_setup(df, bounce_threshold=0.03):
    """Detect LOD bounces"""
    df = track_hod_lod(df)
    
    # LOD test
    df['lod_test'] = (
        (df['low'] <= df['lod'] * 1.001) &  # Near LOD
        (df['close'] > df['lod'] * 1.01)    # Closes 1% above LOD
    )
    
    # Bounce strength
    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) &  # At least 30 min since last LOD
        (abs(df['low'] - df['lod']) / df['lod'] < 0.005)  # Within 0.5%
    )
    
    # Hammer/Doji at LOD
    df['hammer_at_lod'] = (
        df['lod_test'] &
        ((df['close'] - df['low']) / (df['high'] - df['low']) > 0.7) &  # Close in top 30%
        ((df['high'] - df['low']) / df['open'] > 0.02)  # Minimum 2% range
    )
    
    return df

Multi-Day HOD/LOD Levels

def multi_day_levels(ticker, lookback_days=5):
    """Get HOD/LOD levels from multiple days"""
    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
    
    # Create levels DataFrame
    levels_df = pd.DataFrame(levels).T
    
    # Identify key levels
    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):
    """Analyze when HOD/LOD typically occur"""
    df = track_hod_lod(df)
    
    # Add timestamp info
    df['hour'] = df.index.hour
    df['minute'] = df.index.minute
    df['time_of_day'] = df.index.time
    
    # HOD frequency by hour
    hod_times = df[df['is_new_hod']]['hour'].value_counts().sort_index()
    lod_times = df[df['is_new_lod']]['hour'].value_counts().sort_index()
    
    # HOD/LOD probability by period
    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):
    """Analyze failed HOD/LOD breaks"""
    df = track_hod_lod(df)
    
    # Identify initial breaks
    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]):
    """Progressive entry strategy on HOD breaks"""
    df = hod_spike_setup(df)
    
    # Different confirmation levels
    df['hod_level_1'] = df['hod_break']  # Initial break
    df['hod_level_2'] = (  # Break with volume
        df['hod_break'] & 
        (df['rvol'] > 3)
    )
    df['hod_level_3'] = (  # Strong break with continuation
        df['hod_continuation'] &
        (df['hod_break_strength'] > 2)
    )
    
    # Backtesting with progressive entries
    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):
    """Combine gap analysis with HOD breaks"""
    # Calculate 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) &  # Significant gap up
        (df['close'] > df['open']) &       # Holding 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 with new bar"""
        # Update 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):
        """Current status"""
        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
        }

HOD/LOD Alerts

def hod_lod_alerts(df, ticker):
    """Generate HOD/LOD alerts"""
    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

Next Step

Let’s wrap up the indicators with Gap % and Float, fundamental for small cap screening.