Gap % y Float

Los Fundamentos del Small Cap Trading

Gap % y Float son los dos criterios más importantes para filtrar small caps con potencial explosivo. Un stock con float bajo y gap alto es dinamita.

Cálculo de Gap %

def calculate_gap_metrics(df):
    """Calcular todas las métricas de gap"""
    # Gap básico
    df['prev_close'] = df['close'].shift(1)
    df['gap_dollar'] = df['open'] - df['prev_close']
    df['gap_pct'] = (df['gap_dollar'] / df['prev_close']) * 100
    
    # Clasificar gaps
    df['gap_type'] = pd.cut(
        df['gap_pct'],
        bins=[-np.inf, -10, -5, -2, 2, 5, 10, 20, np.inf],
        labels=['large_gap_down', 'medium_gap_down', 'small_gap_down', 
                'no_gap', 'small_gap_up', 'medium_gap_up', 
                'large_gap_up', 'massive_gap_up']
    )
    
    # Gap vs average range
    df['avg_range'] = ((df['high'] - df['low']) / df['low']).rolling(20).mean()
    df['gap_vs_range'] = abs(df['gap_pct']) / (df['avg_range'] * 100)
    
    # Gap fill analysis
    df['gap_high'] = np.where(df['gap_pct'] > 0, df['prev_close'], df['open'])
    df['gap_low'] = np.where(df['gap_pct'] > 0, df['open'], df['prev_close'])
    
    # Check gap fill
    df['gap_filled'] = np.where(
        df['gap_pct'] > 0,
        df['low'] <= df['prev_close'],  # Gap up filled
        df['high'] >= df['prev_close']   # Gap down filled
    )
    
    return df

Float Analysis

def get_float_data(ticker):
    """Obtener datos de float y shares outstanding"""
    # Esto requiere conexión a API de fundamentales
    # Ejemplo con yfinance (no siempre confiable para float)
    import yfinance as yf
    
    stock = yf.Ticker(ticker)
    info = stock.info
    
    float_data = {
        'shares_outstanding': info.get('sharesOutstanding', None),
        'float_shares': info.get('floatShares', None),
        'held_by_insiders': info.get('heldPercentInsiders', None),
        'held_by_institutions': info.get('heldPercentInstitutions', None),
        'short_ratio': info.get('shortRatio', None),
        'short_percent': info.get('shortPercentOfFloat', None)
    }
    
    # Calcular float real si no está disponible
    if float_data['float_shares'] is None and float_data['shares_outstanding']:
        insider_held = float_data['held_by_insiders'] or 0
        institutional_held = float_data['held_by_institutions'] or 0
        locked_up = (insider_held + institutional_held) / 100
        float_data['float_shares'] = float_data['shares_outstanding'] * (1 - locked_up)
    
    return float_data

def classify_float_size(float_shares):
    """Clasificar tamaño de float"""
    if float_shares is None:
        return 'unknown'
    elif float_shares < 10_000_000:
        return 'micro_float'
    elif float_shares < 25_000_000:
        return 'low_float'
    elif float_shares < 50_000_000:
        return 'medium_float'
    elif float_shares < 100_000_000:
        return 'large_float'
    else:
        return 'institutional'

Gap & Float Scanner

def gap_float_scanner(universe, gap_threshold=15, max_float=50_000_000):
    """Scanner para gap + low float combo"""
    candidates = []
    
    for ticker in universe:
        try:
            # Obtener datos de precio
            data = get_latest_data(ticker)
            gap_pct = calculate_gap_pct(data)
            
            # Filtros básicos
            if abs(gap_pct) < gap_threshold:
                continue
                
            # Obtener float
            float_data = get_float_data(ticker)
            float_shares = float_data['float_shares']
            
            if float_shares is None or float_shares > max_float:
                continue
            
            # Calcular métricas adicionales
            price = data['close'].iloc[-1]
            volume = data['volume'].iloc[-1]
            dollar_volume = price * volume
            
            # News/catalysts check (simplified)
            has_news = check_recent_news(ticker)
            
            candidate = {
                'ticker': ticker,
                'gap_pct': gap_pct,
                'float_shares': float_shares,
                'float_category': classify_float_size(float_shares),
                'price': price,
                'volume': volume,
                'dollar_volume': dollar_volume,
                'rvol': calculate_current_rvol(data),
                'has_news': has_news,
                'short_interest': float_data['short_percent'],
                'risk_score': calculate_risk_score(gap_pct, float_shares, volume)
            }
            
            candidates.append(candidate)
            
        except Exception as e:
            print(f"Error processing {ticker}: {e}")
            continue
    
    # Ordenar por potencial
    candidates_df = pd.DataFrame(candidates)
    candidates_df['rank_score'] = (
        candidates_df['gap_pct'].abs() * 0.3 +
        (50_000_000 / candidates_df['float_shares']) * 0.4 +
        candidates_df['rvol'] * 0.3
    )
    
    return candidates_df.sort_values('rank_score', ascending=False)

Gap Fill Probability

def gap_fill_analysis(df, lookback_period=252):
    """Analizar probabilidad de que se llene el gap"""
    df = calculate_gap_metrics(df)
    
    # Análisis histórico de gap fills
    gap_fill_stats = {}
    
    for gap_type in df['gap_type'].unique():
        if pd.isna(gap_type):
            continue
            
        gaps = df[df['gap_type'] == gap_type].copy()
        
        # Para cada gap, ver si se llenó en N días
        fill_rates = {}
        for days in [1, 3, 5, 10, 20]:
            fills = 0
            total = 0
            
            for idx in gaps.index:
                if idx + days < len(df):
                    future_data = df.loc[idx:idx+days]
                    gap_row = df.loc[idx]
                    
                    if gap_row['gap_pct'] > 0:  # Gap up
                        filled = future_data['low'].min() <= gap_row['prev_close']
                    else:  # Gap down
                        filled = future_data['high'].max() >= gap_row['prev_close']
                    
                    if filled:
                        fills += 1
                    total += 1
            
            fill_rates[f'{days}_days'] = fills / total if total > 0 else 0
        
        gap_fill_stats[gap_type] = fill_rates
    
    return gap_fill_stats

Float Rotation Analysis

def float_rotation_analysis(df, float_shares):
    """Analizar cuántas veces rota el float"""
    if float_shares is None:
        return None
    
    # Volumen acumulado vs float
    df['daily_rotation'] = df['volume'] / float_shares
    df['cumulative_rotation'] = df['daily_rotation'].cumsum()
    
    # Ventana móvil de rotación
    df['rotation_5d'] = df['daily_rotation'].rolling(5).sum()
    df['rotation_20d'] = df['daily_rotation'].rolling(20).sum()
    
    # Velocidad de rotación
    df['rotation_velocity'] = df['daily_rotation'].rolling(5).mean()
    
    # Alerta de alta rotación
    df['high_rotation'] = df['daily_rotation'] > 0.5  # 50% del float en un día
    df['extreme_rotation'] = df['daily_rotation'] > 1.0  # Float completo
    
    return df

Gap Fade vs Follow Strategy

def gap_strategy_signals(df, float_shares):
    """Determinar si fade o follow el gap"""
    df = calculate_gap_metrics(df)
    
    # Factores para la decisión
    gap_size = abs(df['gap_pct'])
    float_category = classify_float_size(float_shares)
    
    # Reglas generales (simplificadas)
    df['strategy'] = 'hold'
    
    # Gap pequeño en large float = probable fill (fade)
    fade_conditions = (
        (gap_size < 5) & (float_category in ['large_float', 'institutional'])
    )
    
    # Gap grande en low float = probable momentum (follow)
    follow_conditions = (
        (gap_size > 15) & (float_category in ['micro_float', 'low_float'])
    )
    
    df.loc[fade_conditions, 'strategy'] = 'fade'
    df.loc[follow_conditions, 'strategy'] = 'follow'
    
    # Ajustar por volumen
    if 'rvol' in df.columns:
        # Alto volumen favorece follow
        df.loc[(df['strategy'] == 'fade') & (df['rvol'] > 3), 'strategy'] = 'follow'
        # Bajo volumen favorece fade
        df.loc[(df['strategy'] == 'follow') & (df['rvol'] < 1), 'strategy'] = 'fade'
    
    return df

Insider/Institution Impact

def analyze_ownership_impact(ticker, float_data):
    """Analizar impacto de ownership en volatilidad"""
    insider_pct = float_data.get('held_by_insiders', 0)
    institution_pct = float_data.get('held_by_institutions', 0)
    float_shares = float_data.get('float_shares', 0)
    
    # Free float real
    locked_shares = (insider_pct + institution_pct) / 100
    truly_free_float = float_shares * (1 - locked_shares)
    
    # Volatility multiplier basado en ownership
    ownership_factor = 1 + (locked_shares * 2)  # Más locked = más volátil
    
    analysis = {
        'insider_locked_pct': insider_pct,
        'institution_locked_pct': institution_pct,
        'total_locked_pct': insider_pct + institution_pct,
        'truly_free_float': truly_free_float,
        'volatility_multiplier': ownership_factor,
        'risk_level': 'extreme' if locked_shares > 0.8 else 
                     'high' if locked_shares > 0.6 else 
                     'medium' if locked_shares > 0.4 else 'low'
    }
    
    return analysis

Historical Gap Performance

def historical_gap_performance(ticker, lookback_days=252):
    """Performance histórico por tipo de gap"""
    df = get_historical_data(ticker, lookback_days)
    df = calculate_gap_metrics(df)
    
    performance_by_gap = {}
    
    for gap_type in df['gap_type'].unique():
        if pd.isna(gap_type):
            continue
            
        gap_days = df[df['gap_type'] == gap_type].copy()
        
        if len(gap_days) == 0:
            continue
        
        # Performance metrics
        gap_days['day_return'] = (gap_days['close'] - gap_days['open']) / gap_days['open'] * 100
        gap_days['intraday_high'] = (gap_days['high'] - gap_days['open']) / gap_days['open'] * 100
        gap_days['intraday_low'] = (gap_days['low'] - gap_days['open']) / gap_days['open'] * 100
        
        performance_by_gap[gap_type] = {
            'count': len(gap_days),
            'avg_day_return': gap_days['day_return'].mean(),
            'win_rate': (gap_days['day_return'] > 0).mean(),
            'avg_intraday_high': gap_days['intraday_high'].mean(),
            'avg_intraday_low': gap_days['intraday_low'].mean(),
            'max_gain': gap_days['intraday_high'].max(),
            'max_loss': gap_days['intraday_low'].min()
        }
    
    return performance_by_gap

Real-Time Gap Monitor

class GapFloatMonitor:
    def __init__(self, gap_threshold=10, max_float=50_000_000):
        self.gap_threshold = gap_threshold
        self.max_float = max_float
        self.watchlist = []
        
    def scan_premarket(self):
        """Scan pre-market para gaps"""
        candidates = []
        
        # Obtener movers pre-market
        premarket_movers = get_premarket_movers()
        
        for ticker in premarket_movers:
            try:
                # Validar criterios
                gap_pct = calculate_premarket_gap(ticker)
                float_data = get_float_data(ticker)
                
                if (abs(gap_pct) >= self.gap_threshold and 
                    float_data['float_shares'] <= self.max_float):
                    
                    candidates.append({
                        'ticker': ticker,
                        'gap_pct': gap_pct,
                        'float': float_data['float_shares'],
                        'added_time': pd.Timestamp.now()
                    })
                    
            except Exception as e:
                continue
        
        self.watchlist.extend(candidates)
        return candidates
    
    def monitor_intraday(self):
        """Monitorear durante el día"""
        alerts = []
        
        for item in self.watchlist:
            ticker = item['ticker']
            current_data = get_current_data(ticker)
            
            # Check key levels
            if 'gap_filled' in current_data and current_data['gap_filled']:
                alerts.append(f"🔄 {ticker}: Gap filled @ ${current_data['price']:.2f}")
            
            # Check momentum
            if current_data['rvol'] > 5:
                alerts.append(f"🚀 {ticker}: Explosive volume - RVol {current_data['rvol']:.1f}x")
        
        return alerts

Tips de Trading Real

1. Float Categories Strategy

FLOAT_STRATEGIES = {
    'micro_float': {
        'max_position': 0.05,  # 5% max position
        'stop_loss': 0.15,     # 15% stop
        'take_profit': 0.50,   # 50% target
        'time_limit': 30       # 30 min max hold
    },
    'low_float': {
        'max_position': 0.10,
        'stop_loss': 0.10,
        'take_profit': 0.30,
        'time_limit': 60
    },
    'medium_float': {
        'max_position': 0.20,
        'stop_loss': 0.08,
        'take_profit': 0.20,
        'time_limit': 120
    }
}

2. Gap Size Rules

def gap_trading_rules(gap_pct):
    """Reglas según tamaño de gap"""
    if abs(gap_pct) > 50:
        return "AVOID - Too risky"
    elif abs(gap_pct) > 30:
        return "SCALP ONLY - Quick in/out"
    elif abs(gap_pct) > 15:
        return "MOMENTUM PLAY - Follow with tight stops"
    elif abs(gap_pct) > 5:
        return "FADE CANDIDATE - Look for mean reversion"
    else:
        return "NORMAL GAP - No special strategy"

Alertas Críticas

def critical_gap_float_alerts(ticker, gap_pct, float_shares):
    """Alertas para combinaciones extremas"""
    alerts = []
    
    # Micro float + large gap = nuclear
    if float_shares < 5_000_000 and abs(gap_pct) > 25:
        alerts.append(f"☢️ {ticker}: NUCLEAR SETUP - {gap_pct:.1f}% gap on {float_shares/1_000_000:.1f}M float")
    
    # High short interest + gap up
    short_data = get_short_interest(ticker)
    if short_data and short_data > 30 and gap_pct > 15:
        alerts.append(f"🔥 {ticker}: SQUEEZE POTENTIAL - {gap_pct:.1f}% gap + {short_data:.1f}% SI")
    
    return alerts