First Green Day & First Red Day Patterns

Concepto Base

Las estrategias First Green Day y First Red Day se basan en los cambios de momentum direccional después de tendencias sostenidas. Estas reversiones suelen marcar puntos de inflexión importantes en small caps debido a la psicología retail y el flujo institucional.

First Green Day Strategy

Fundamentos

Después de una serie de días rojos consecutivos, el primer día verde suele atraer buying interest renovado, especialmente si viene acompañado de volumen.

class FirstGreenDayPattern:
    def __init__(self):
        self.pattern_type = "trend_reversal"
        self.min_red_days = 3
        self.max_red_days = 8  # Más de 8 días puede indicar problemas fundamentales
        
    def identify_pattern(self, price_history):
        """Identificar patrón de First Green Day"""
        
        # Verificar días rojos consecutivos
        consecutive_red = self.count_consecutive_red_days(price_history)
        
        if not (self.min_red_days <= consecutive_red <= self.max_red_days):
            return None
        
        # Verificar que hoy sea green
        today = price_history.iloc[-1]
        if today['close'] <= today['open']:
            return None
        
        # Calcular métricas del patrón
        pattern_metrics = self.calculate_pattern_metrics(price_history, consecutive_red)
        
        return {
            'consecutive_red_days': consecutive_red,
            'pattern_strength': pattern_metrics['strength'],
            'oversold_level': pattern_metrics['oversold'],
            'volume_confirmation': pattern_metrics['volume_confirm'],
            'setup_quality': pattern_metrics['quality_score']
        }
    
    def calculate_pattern_metrics(self, price_history, red_days):
        """Calcular métricas del patrón"""
        
        # Decline magnitude durante red days
        start_price = price_history.iloc[-(red_days + 1)]['close']
        low_price = price_history.tail(red_days)['low'].min()
        total_decline = (start_price - low_price) / start_price
        
        # Volume durante decline vs recovery
        decline_avg_volume = price_history.tail(red_days)['volume'].mean()
        today_volume = price_history.iloc[-1]['volume']
        volume_ratio = today_volume / decline_avg_volume if decline_avg_volume > 0 else 1
        
        # RSI oversold
        rsi = calculate_rsi(price_history['close'], 14)
        current_rsi = rsi.iloc[-1]
        
        # Pattern strength
        strength_score = min(100, (total_decline * 200) + (red_days * 5))
        
        # Quality score
        quality_components = [
            min(30, total_decline * 100),  # Decline severity (max 30)
            min(25, red_days * 3),         # Duration (max 25)
            min(25, (volume_ratio - 1) * 12.5),  # Volume (max 25)
            min(20, max(0, (30 - current_rsi)))  # RSI oversold (max 20)
        ]
        
        quality_score = sum(quality_components)
        
        return {
            'strength': strength_score,
            'oversold': current_rsi < 35,
            'volume_confirm': volume_ratio > 1.5,
            'quality_score': quality_score
        }

def count_consecutive_red_days(price_data):
    """Contar días rojos consecutivos"""
    consecutive = 0
    
    for i in range(len(price_data) - 2, -1, -1):  # Start from yesterday
        day = price_data.iloc[i]
        if day['close'] < day['open']:
            consecutive += 1
        else:
            break
    
    return consecutive

Entry Strategy

class FirstGreenDayEntry:
    def __init__(self, risk_tolerance='medium'):
        self.risk_tolerance = risk_tolerance
        self.entry_strategies = {
            'conservative': self.conservative_entry,
            'aggressive': self.aggressive_entry,
            'scalp': self.scalp_entry
        }
    
    def calculate_entry_plan(self, stock_data, pattern_data):
        """Calcular plan de entrada"""
        
        strategy_func = self.entry_strategies.get(self.risk_tolerance, self.conservative_entry)
        return strategy_func(stock_data, pattern_data)
    
    def conservative_entry(self, stock_data, pattern_data):
        """Entrada conservadora - esperar confirmación"""
        
        current_price = stock_data.price
        today_open = stock_data.open
        yesterday_high = stock_data.previous_day_high
        
        return {
            'primary_entry': {
                'price': max(yesterday_high * 1.01, current_price * 1.02),
                'trigger': 'break_previous_day_high',
                'size_pct': 0.60,
                'confirmation_required': True
            },
            'secondary_entry': {
                'price': current_price * 1.05,
                'trigger': 'sustained_momentum',
                'size_pct': 0.40,
                'time_limit': '11:30'  # Only if momentum continues
            },
            'stop_loss': min(
                today_open * 0.97,  # 3% below today's open
                stock_data.support_level * 0.98
            ),
            'targets': {
                'target_1': current_price * 1.08,    # 8% quick target
                'target_2': current_price * 1.15,    # 15% extended
                'target_3': stock_data.previous_resistance * 1.02
            },
            'risk_reward': 'conservative',
            'max_hold_time': 'same_day'
        }
    
    def aggressive_entry(self, stock_data, pattern_data):
        """Entrada agresiva - temprano en el momentum"""
        
        current_price = stock_data.price
        today_open = stock_data.open
        
        return {
            'immediate_entry': {
                'price': current_price * 1.005,  # Slight premium
                'trigger': 'pattern_confirmation',
                'size_pct': 0.75,
                'market_order': True
            },
            'add_on_strength': {
                'price': current_price * 1.03,
                'trigger': 'momentum_acceleration',
                'size_pct': 0.25,
                'volume_requirement': '2x_average'
            },
            'stop_loss': today_open * 0.95,  # 5% below open
            'targets': {
                'quick_target': current_price * 1.06,
                'momentum_target': current_price * 1.12
            },
            'risk_reward': 'aggressive',
            'time_management': {
                'profit_take_time': '14:00',
                'force_exit_time': '15:45'
            }
        }

First Red Day Strategy (Short Bias)

Fundamentos

Después de una serie de días verdes consecutivos, el primer día rojo suele indicar que el momentum alcista se está agotando y puede ser el inicio de una corrección.

class FirstRedDayPattern:
    def __init__(self):
        self.pattern_type = "momentum_exhaustion"
        self.min_green_days = 3
        self.max_green_days = 10
        
    def identify_short_setup(self, price_history):
        """Identificar setup para short en First Red Day"""
        
        consecutive_green = self.count_consecutive_green_days(price_history)
        
        if not (self.min_green_days <= consecutive_green <= self.max_green_days):
            return None
        
        # Verificar que hoy sea red day
        today = price_history.iloc[-1]
        if today['close'] >= today['open']:
            return None
        
        # Analizar calidad del setup
        setup_analysis = self.analyze_short_setup_quality(price_history, consecutive_green)
        
        return {
            'consecutive_green_days': consecutive_green,
            'momentum_exhaustion_score': setup_analysis['exhaustion_score'],
            'overbought_level': setup_analysis['overbought'],
            'distribution_signs': setup_analysis['distribution'],
            'short_setup_quality': setup_analysis['quality_score'],
            'entry_recommendation': setup_analysis['recommendation']
        }
    
    def analyze_short_setup_quality(self, price_history, green_days):
        """Analizar calidad para short setup"""
        
        # Extensión del rally
        rally_start = price_history.iloc[-(green_days + 1)]['close']
        rally_high = price_history.tail(green_days)['high'].max()
        rally_magnitude = (rally_high - rally_start) / rally_start
        
        # Volume analysis durante rally
        rally_volume = price_history.tail(green_days)['volume'].mean()
        today_volume = price_history.iloc[-1]['volume']
        volume_ratio = today_volume / rally_volume if rally_volume > 0 else 1
        
        # RSI overbought
        rsi = calculate_rsi(price_history['close'], 14)
        current_rsi = rsi.iloc[-1]
        
        # Distribution signs
        distribution_score = self.detect_distribution_signs(price_history.tail(green_days + 1))
        
        # Exhaustion score
        exhaustion_components = [
            min(40, rally_magnitude * 100),  # Rally magnitude
            min(30, green_days * 3),         # Duration
            min(20, max(0, current_rsi - 70)), # Overbought
            min(10, distribution_score)      # Distribution
        ]
        
        exhaustion_score = sum(exhaustion_components)
        
        # Overall quality
        quality_score = min(100, exhaustion_score + (volume_ratio * 10))
        
        recommendation = 'strong_short' if quality_score > 80 else \
                        'moderate_short' if quality_score > 60 else \
                        'weak_short'
        
        return {
            'exhaustion_score': exhaustion_score,
            'overbought': current_rsi > 75,
            'distribution': distribution_score > 5,
            'quality_score': quality_score,
            'recommendation': recommendation
        }
    
    def detect_distribution_signs(self, recent_data):
        """Detectar signos de distribución"""
        score = 0
        
        # Lower highs en días recientes
        highs = recent_data['high'].values
        if len(highs) >= 3:
            if highs[-1] < highs[-2] < highs[-3]:
                score += 30
        
        # Volume en red days vs green days
        green_days = recent_data[recent_data['close'] > recent_data['open']]
        red_days = recent_data[recent_data['close'] < recent_data['open']]
        
        if len(red_days) > 0 and len(green_days) > 0:
            red_avg_vol = red_days['volume'].mean()
            green_avg_vol = green_days['volume'].mean()
            
            if red_avg_vol > green_avg_vol * 1.2:
                score += 25
        
        # Failed breakouts
        resistance_breaks = self.count_failed_breakouts(recent_data)
        score += min(20, resistance_breaks * 10)
        
        return score

def count_consecutive_green_days(price_data):
    """Contar días verdes consecutivos"""
    consecutive = 0
    
    for i in range(len(price_data) - 2, -1, -1):  # Start from yesterday
        day = price_data.iloc[i]
        if day['close'] > day['open']:
            consecutive += 1
        else:
            break
    
    return consecutive

Short Entry Strategy

class FirstRedDayShortEntry:
    def __init__(self):
        self.short_strategies = {
            'gap_down': self.gap_down_entry,
            'breakdown': self.breakdown_entry,
            'fade_rally': self.fade_rally_entry
        }
    
    def calculate_short_entry_plan(self, stock_data, pattern_data):
        """Calcular plan de entrada para short"""
        
        # Determinar mejor estrategia basada en price action
        if stock_data.gap_percent < -0.03:  # Gap down >3%
            strategy = 'gap_down'
        elif stock_data.price < stock_data.previous_support:
            strategy = 'breakdown'
        else:
            strategy = 'fade_rally'
        
        return self.short_strategies[strategy](stock_data, pattern_data)
    
    def gap_down_entry(self, stock_data, pattern_data):
        """Short en gap down después de rally"""
        
        gap_size = abs(stock_data.gap_percent)
        premarket_high = stock_data.premarket_high
        
        return {
            'primary_short_entry': {
                'price': premarket_high * 0.99,  # Below PM high
                'size_pct': 0.50,
                'trigger': 'failed_gap_fill',
                'confirmation': 'volume_on_weakness'
            },
            'aggressive_short_entry': {
                'price': stock_data.price * 0.98,
                'size_pct': 0.50,
                'trigger': 'continued_weakness',
                'time_window': '10:00-11:00'
            },
            'stop_loss': max(
                premarket_high * 1.03,
                stock_data.previous_day_high * 1.02
            ),
            'targets': {
                'target_1': stock_data.price * 0.92,  # 8% down
                'target_2': stock_data.price * 0.85,  # 15% down
                'gap_fill_target': stock_data.previous_close
            },
            'risk_management': {
                'max_loss_pct': 0.08,
                'time_stop': '15:30',
                'cover_on_squeeze': True
            }
        }
    
    def breakdown_entry(self, stock_data, pattern_data):
        """Short en breakdown de soporte"""
        
        support_level = stock_data.support_level
        
        return {
            'breakdown_short': {
                'price': support_level * 0.995,  # Just below support
                'size_pct': 0.75,
                'trigger': 'confirmed_breakdown',
                'volume_requirement': '1.5x_average'
            },
            'retest_short': {
                'price': support_level * 1.005,  # Failed retest
                'size_pct': 0.25,
                'trigger': 'failed_retest',
                'timeout': '30_minutes'
            },
            'stop_loss': support_level * 1.05,  # 5% above broken support
            'targets': {
                'measured_move': support_level * 0.90,
                'next_support': stock_data.next_support_level
            }
        }

Risk Management Específico

Position Sizing Dinámico

class PatternPositionSizer:
    def __init__(self, account_size, base_risk=0.015):
        self.account_size = account_size
        self.base_risk = base_risk
        
    def calculate_pattern_position_size(self, stock_data, pattern_data, entry_plan):
        """Calcular tamaño basado en patrón específico"""
        
        # Base risk amount
        base_risk_amount = self.account_size * self.base_risk
        
        # Pattern-specific adjustments
        pattern_adjustments = self.get_pattern_adjustments(pattern_data)
        
        # Market environment adjustments
        market_adjustments = self.get_market_environment_adjustments()
        
        # Final risk calculation
        adjusted_risk = base_risk_amount * pattern_adjustments * market_adjustments
        
        # Calculate position size
        entry_price = entry_plan['price']
        stop_price = entry_plan['stop_loss']
        risk_per_share = abs(entry_price - stop_price)
        
        shares = int(adjusted_risk / risk_per_share) if risk_per_share > 0 else 0
        
        return {
            'shares': shares,
            'dollar_risk': adjusted_risk,
            'entry_price': entry_price,
            'stop_price': stop_price,
            'pattern_adjustment': pattern_adjustments,
            'market_adjustment': market_adjustments
        }
    
    def get_pattern_adjustments(self, pattern_data):
        """Ajustes específicos del patrón"""
        
        base_multiplier = 1.0
        
        # Quality score adjustment
        quality_score = pattern_data.get('setup_quality', 50)
        if quality_score > 80:
            base_multiplier *= 1.3
        elif quality_score > 60:
            base_multiplier *= 1.1
        elif quality_score < 40:
            base_multiplier *= 0.7
        
        # Pattern strength
        if pattern_data.get('pattern_strength', 0) > 75:
            base_multiplier *= 1.2
        
        # Volume confirmation
        if pattern_data.get('volume_confirmation', False):
            base_multiplier *= 1.1
        
        return min(2.0, max(0.5, base_multiplier))

Time-Based Exit Rules

class PatternTimeManager:
    def __init__(self):
        self.time_rules = {
            'first_green_day': self.green_day_time_rules,
            'first_red_day': self.red_day_time_rules
        }
    
    def green_day_time_rules(self, entry_time, current_time, pnl):
        """Reglas de tiempo para First Green Day"""
        
        time_in_trade = (current_time - entry_time).seconds / 60  # minutes
        
        rules = []
        
        # Early momentum check
        if time_in_trade > 30 and pnl < 0.02:  # 30 min, less than 2% profit
            rules.append({
                'action': 'consider_exit',
                'reason': 'lack_of_early_momentum',
                'urgency': 'medium'
            })
        
        # Lunch time warning
        if current_time.hour == 12:
            rules.append({
                'action': 'reduce_position',
                'reason': 'lunch_time_low_volume',
                'urgency': 'low'
            })
        
        # Late day exit
        if current_time.hour >= 15:
            rules.append({
                'action': 'exit_position',
                'reason': 'late_day_exit',
                'urgency': 'high'
            })
        
        return rules
    
    def red_day_time_rules(self, entry_time, current_time, pnl):
        """Reglas de tiempo para First Red Day shorts"""
        
        time_in_trade = (current_time - entry_time).seconds / 60
        
        rules = []
        
        # Quick profit taking on shorts
        if time_in_trade > 60 and pnl > 0.10:  # 1 hour, 10% profit
            rules.append({
                'action': 'take_partial_profit',
                'reason': 'quick_short_profit',
                'urgency': 'medium'
            })
        
        # Cover before close (shorts risky overnight)
        if current_time.hour >= 15 and current_time.minute >= 30:
            rules.append({
                'action': 'cover_short',
                'reason': 'avoid_overnight_short_risk',
                'urgency': 'high'
            })
        
        return rules

Performance Analysis

Pattern-Specific Metrics

class PatternPerformanceAnalyzer:
    def __init__(self):
        self.pattern_metrics = {}
        
    def analyze_pattern_performance(self, trades_df, pattern_type):
        """Analizar performance específica del patrón"""
        
        pattern_trades = trades_df[trades_df['pattern_type'] == pattern_type]
        
        if len(pattern_trades) == 0:
            return {'error': f'No trades found for pattern {pattern_type}'}
        
        # Basic metrics
        total_trades = len(pattern_trades)
        wins = len(pattern_trades[pattern_trades['pnl'] > 0])
        win_rate = wins / total_trades
        
        # Pattern-specific analysis
        if pattern_type == 'first_green_day':
            specific_analysis = self.analyze_green_day_performance(pattern_trades)
        else:  # first_red_day
            specific_analysis = self.analyze_red_day_performance(pattern_trades)
        
        return {
            'basic_metrics': {
                'total_trades': total_trades,
                'win_rate': win_rate,
                'avg_pnl': pattern_trades['pnl'].mean(),
                'total_pnl': pattern_trades['pnl'].sum()
            },
            'pattern_specific': specific_analysis,
            'best_setups': self.identify_best_setups(pattern_trades),
            'worst_setups': self.identify_worst_setups(pattern_trades)
        }
    
    def analyze_green_day_performance(self, green_trades):
        """Análisis específico para First Green Day"""
        
        # Performance by consecutive red days
        red_days_performance = green_trades.groupby('consecutive_red_days')['pnl'].agg(['mean', 'count', 'sum'])
        
        # Performance by setup quality
        quality_bins = pd.cut(green_trades['setup_quality'], bins=[0, 50, 70, 85, 100])
        quality_performance = green_trades.groupby(quality_bins)['pnl'].agg(['mean', 'count'])
        
        # Time of entry analysis
        entry_hour_performance = green_trades.groupby(green_trades['entry_time'].dt.hour)['pnl'].mean()
        
        return {
            'consecutive_red_days_impact': red_days_performance.to_dict(),
            'setup_quality_performance': quality_performance.to_dict(),
            'best_entry_hours': entry_hour_performance.sort_values(ascending=False).head(3).to_dict(),
            'optimal_red_days': red_days_performance['mean'].idxmax()
        }

Esta documentación proporciona un framework completo para implementar estrategias First Green Day y First Red Day, con énfasis en la gestión de riesgo y análisis de performance específico para cada patrón.