Stop Loss y Trailing Stops

Tu Póliza de Seguro en Cada Trade

Los stops son la diferencia entre una pérdida controlada y un desastre. En small caps, donde los movimientos pueden ser violentos, tener stops automáticos no es opcional - es supervivencia.

Tipos de Stop Loss

1. Fixed Percentage Stop

class FixedPercentageStop:
    def __init__(self, stop_loss_pct=0.03):
        self.stop_loss_pct = stop_loss_pct
        
    def calculate_stop_price(self, entry_price, position_type='long'):
        """Calcular precio de stop loss"""
        if position_type == 'long':
            stop_price = entry_price * (1 - self.stop_loss_pct)
        else:  # short
            stop_price = entry_price * (1 + self.stop_loss_pct)
        
        return round(stop_price, 2)
    
    def is_stopped_out(self, current_price, stop_price, position_type='long'):
        """¿Se activó el stop?"""
        if position_type == 'long':
            return current_price <= stop_price
        else:  # short
            return current_price >= stop_price

# Ejemplo
stop_manager = FixedPercentageStop(stop_loss_pct=0.03)  # 3% stop
entry_price = 25.00
stop_price = stop_manager.calculate_stop_price(entry_price)
print(f"Entry: ${entry_price}, Stop: ${stop_price}")  # Entry: $25.00, Stop: $24.25

2. ATR-Based Stop

def calculate_atr_stop(df, entry_price, atr_multiplier=1.5, position_type='long'):
    """Stop basado en Average True Range"""
    # Calcular ATR
    df['tr1'] = df['high'] - df['low']
    df['tr2'] = abs(df['high'] - df['close'].shift(1))
    df['tr3'] = abs(df['low'] - df['close'].shift(1))
    df['true_range'] = df[['tr1', 'tr2', 'tr3']].max(axis=1)
    df['atr'] = df['true_range'].rolling(14).mean()
    
    current_atr = df['atr'].iloc[-1]
    
    if position_type == 'long':
        stop_price = entry_price - (current_atr * atr_multiplier)
    else:  # short
        stop_price = entry_price + (current_atr * atr_multiplier)
    
    return round(stop_price, 2)

# Ejemplo de uso
def smart_stop_calculation(ticker, entry_price, position_type='long'):
    """Calcular stop inteligente basado en volatilidad"""
    # Obtener datos históricos
    data = get_historical_data(ticker, period='3mo')
    
    # Método 1: Fixed percentage (3%)
    fixed_stop = entry_price * (0.97 if position_type == 'long' else 1.03)
    
    # Método 2: ATR-based
    atr_stop = calculate_atr_stop(data, entry_price, 1.5, position_type)
    
    # Método 3: Support/Resistance
    if position_type == 'long':
        support_level = find_nearest_support(data, entry_price)
        sr_stop = support_level * 0.99  # 1% below support
    else:
        resistance_level = find_nearest_resistance(data, entry_price)
        sr_stop = resistance_level * 1.01  # 1% above resistance
    
    # Usar el más conservador (closer to entry)
    if position_type == 'long':
        final_stop = max(fixed_stop, atr_stop, sr_stop)  # Highest stop for long
    else:
        final_stop = min(fixed_stop, atr_stop, sr_stop)  # Lowest stop for short
    
    return {
        'final_stop': round(final_stop, 2),
        'fixed_stop': round(fixed_stop, 2),
        'atr_stop': round(atr_stop, 2),
        'sr_stop': round(sr_stop, 2),
        'method_used': 'conservative_composite'
    }

3. VWAP-Based Stop

class VWAPStopManager:
    def __init__(self, buffer_pct=0.01):
        self.buffer_pct = buffer_pct  # 1% buffer below VWAP
        
    def calculate_vwap_stop(self, df, position_type='long'):
        """Stop basado en VWAP con buffer"""
        # Calcular VWAP
        df['vwap'] = (df['close'] * df['volume']).cumsum() / df['volume'].cumsum()
        current_vwap = df['vwap'].iloc[-1]
        
        if position_type == 'long':
            stop_price = current_vwap * (1 - self.buffer_pct)
        else:  # short
            stop_price = current_vwap * (1 + self.buffer_pct)
        
        return round(stop_price, 2)
    
    def is_vwap_intact(self, current_price, current_vwap, position_type='long'):
        """¿Está el precio manteniendo VWAP?"""
        if position_type == 'long':
            return current_price > current_vwap
        else:  # short
            return current_price < current_vwap

Trailing Stops

1. Basic Trailing Stop

class TrailingStopManager:
    def __init__(self, initial_stop_pct=0.03, trailing_pct=0.02):
        self.initial_stop_pct = initial_stop_pct
        self.trailing_pct = trailing_pct
        self.positions = {}  # {ticker: position_info}
        
    def initialize_position(self, ticker, entry_price, shares, position_type='long'):
        """Initialize nueva posición con trailing stop"""
        if position_type == 'long':
            initial_stop = entry_price * (1 - self.initial_stop_pct)
        else:  # short
            initial_stop = entry_price * (1 + self.initial_stop_pct)
        
        self.positions[ticker] = {
            'entry_price': entry_price,
            'shares': shares,
            'position_type': position_type,
            'current_stop': initial_stop,
            'highest_price': entry_price,  # For long positions
            'lowest_price': entry_price,   # For short positions
            'unrealized_pnl': 0,
            'max_favorable_excursion': 0
        }
        
        return initial_stop
    
    def update_trailing_stop(self, ticker, current_price):
        """Update trailing stop con nuevo precio"""
        if ticker not in self.positions:
            return None
        
        position = self.positions[ticker]
        
        # Update tracking prices
        if position['position_type'] == 'long':
            if current_price > position['highest_price']:
                position['highest_price'] = current_price
                
                # Calculate new trailing stop
                new_stop = current_price * (1 - self.trailing_pct)
                
                # Only move stop up (never down for longs)
                if new_stop > position['current_stop']:
                    position['current_stop'] = new_stop
        
        else:  # short position
            if current_price < position['lowest_price']:
                position['lowest_price'] = current_price
                
                # Calculate new trailing stop
                new_stop = current_price * (1 + self.trailing_pct)
                
                # Only move stop down (never up for shorts)
                if new_stop < position['current_stop']:
                    position['current_stop'] = new_stop
        
        # Update P&L tracking
        if position['position_type'] == 'long':
            position['unrealized_pnl'] = (current_price - position['entry_price']) * position['shares']
            position['max_favorable_excursion'] = max(
                position['max_favorable_excursion'],
                (position['highest_price'] - position['entry_price']) * position['shares']
            )
        else:  # short
            position['unrealized_pnl'] = (position['entry_price'] - current_price) * position['shares']
            position['max_favorable_excursion'] = max(
                position['max_favorable_excursion'],
                (position['entry_price'] - position['lowest_price']) * position['shares']
            )
        
        return position['current_stop']
    
    def is_stopped_out(self, ticker, current_price):
        """Check si se activó el trailing stop"""
        if ticker not in self.positions:
            return False
        
        position = self.positions[ticker]
        
        if position['position_type'] == 'long':
            return current_price <= position['current_stop']
        else:  # short
            return current_price >= position['current_stop']
    
    def get_position_status(self, ticker):
        """Get status completo de la posición"""
        if ticker not in self.positions:
            return None
        
        return self.positions[ticker].copy()

2. Breakeven Stop

class BreakevenStopManager:
    def __init__(self, breakeven_trigger_pct=0.04, breakeven_buffer_pct=0.005):
        self.breakeven_trigger_pct = breakeven_trigger_pct  # 4% gain to trigger
        self.breakeven_buffer_pct = breakeven_buffer_pct    # 0.5% above entry
        self.positions = {}
        
    def should_move_to_breakeven(self, ticker, current_price):
        """¿Debería mover el stop a breakeven?"""
        if ticker not in self.positions:
            return False
        
        position = self.positions[ticker]
        
        if position['breakeven_set']:
            return False  # Ya está en breakeven
        
        entry_price = position['entry_price']
        
        if position['position_type'] == 'long':
            gain_pct = (current_price - entry_price) / entry_price
            return gain_pct >= self.breakeven_trigger_pct
        else:  # short
            gain_pct = (entry_price - current_price) / entry_price
            return gain_pct >= self.breakeven_trigger_pct
    
    def set_breakeven_stop(self, ticker):
        """Mover stop a breakeven"""
        if ticker not in self.positions:
            return None
        
        position = self.positions[ticker]
        entry_price = position['entry_price']
        
        if position['position_type'] == 'long':
            breakeven_stop = entry_price * (1 + self.breakeven_buffer_pct)
        else:  # short
            breakeven_stop = entry_price * (1 - self.breakeven_buffer_pct)
        
        position['current_stop'] = breakeven_stop
        position['breakeven_set'] = True
        
        return breakeven_stop

3. Parabolic SAR Stop

def calculate_parabolic_sar(df, acceleration=0.02, max_acceleration=0.2):
    """Calcular Parabolic SAR para trailing stops"""
    df = df.copy()
    df['sar'] = df['close'].iloc[0]  # Initialize
    df['ep'] = df['high'].iloc[0]   # Extreme point
    df['af'] = acceleration         # Acceleration factor
    df['trend'] = 1                 # 1 for uptrend, -1 for downtrend
    
    for i in range(1, len(df)):
        prev_sar = df['sar'].iloc[i-1]
        prev_ep = df['ep'].iloc[i-1]
        prev_af = df['af'].iloc[i-1]
        prev_trend = df['trend'].iloc[i-1]
        
        current_high = df['high'].iloc[i]
        current_low = df['low'].iloc[i]
        
        # Calculate new SAR
        new_sar = prev_sar + prev_af * (prev_ep - prev_sar)
        
        # Check for trend reversal
        if prev_trend == 1:  # Uptrend
            if current_low <= new_sar:
                # Trend reversal to downtrend
                df.loc[df.index[i], 'trend'] = -1
                df.loc[df.index[i], 'sar'] = prev_ep
                df.loc[df.index[i], 'ep'] = current_low
                df.loc[df.index[i], 'af'] = acceleration
            else:
                # Continue uptrend
                df.loc[df.index[i], 'trend'] = 1
                df.loc[df.index[i], 'sar'] = new_sar
                
                # Update extreme point and acceleration
                if current_high > prev_ep:
                    df.loc[df.index[i], 'ep'] = current_high
                    df.loc[df.index[i], 'af'] = min(prev_af + acceleration, max_acceleration)
                else:
                    df.loc[df.index[i], 'ep'] = prev_ep
                    df.loc[df.index[i], 'af'] = prev_af
                    
        else:  # Downtrend
            if current_high >= new_sar:
                # Trend reversal to uptrend
                df.loc[df.index[i], 'trend'] = 1
                df.loc[df.index[i], 'sar'] = prev_ep
                df.loc[df.index[i], 'ep'] = current_high
                df.loc[df.index[i], 'af'] = acceleration
            else:
                # Continue downtrend
                df.loc[df.index[i], 'trend'] = -1
                df.loc[df.index[i], 'sar'] = new_sar
                
                # Update extreme point and acceleration
                if current_low < prev_ep:
                    df.loc[df.index[i], 'ep'] = current_low
                    df.loc[df.index[i], 'af'] = min(prev_af + acceleration, max_acceleration)
                else:
                    df.loc[df.index[i], 'ep'] = prev_ep
                    df.loc[df.index[i], 'af'] = prev_af
    
    return df

Stop Loss Avanzado para Small Caps

1. Volatility-Adjusted Stops

class VolatilityAdjustedStop:
    def __init__(self, base_stop_pct=0.03, lookback_days=20):
        self.base_stop_pct = base_stop_pct
        self.lookback_days = lookback_days
        
    def calculate_volatility_multiplier(self, ticker):
        """Calcular multiplicador basado en volatilidad"""
        # Obtener datos históricos
        data = get_historical_data(ticker, self.lookback_days)
        
        # Calcular volatilidad diaria
        returns = data['close'].pct_change().dropna()
        daily_vol = returns.std()
        
        # Clasificar volatilidad
        if daily_vol > 0.08:      # >8% daily vol
            return 1.5            # Wider stops
        elif daily_vol > 0.05:    # 5-8% daily vol
            return 1.25
        elif daily_vol > 0.03:    # 3-5% daily vol
            return 1.0            # Normal stops
        else:                     # <3% daily vol
            return 0.75           # Tighter stops
    
    def calculate_adjusted_stop(self, ticker, entry_price, position_type='long'):
        """Calculate stop ajustado por volatilidad"""
        vol_multiplier = self.calculate_volatility_multiplier(ticker)
        adjusted_stop_pct = self.base_stop_pct * vol_multiplier
        
        if position_type == 'long':
            stop_price = entry_price * (1 - adjusted_stop_pct)
        else:  # short
            stop_price = entry_price * (1 + adjusted_stop_pct)
        
        return {
            'stop_price': round(stop_price, 2),
            'stop_pct': adjusted_stop_pct,
            'vol_multiplier': vol_multiplier,
            'original_stop_pct': self.base_stop_pct
        }

2. Time-Based Stop Adjustments

class TimeBasedStopManager:
    def __init__(self):
        self.time_adjustments = {
            'premarket': 0.5,     # Tighter stops pre-market
            'opening': 1.5,       # Wider stops first hour
            'regular': 1.0,       # Normal stops
            'closing': 0.75,      # Tighter stops last hour
            'after_hours': 0.5    # Very tight stops after hours
        }
    
    def get_time_period(self):
        """Determinar período actual"""
        current_time = pd.Timestamp.now().time()
        
        if pd.Timestamp('04:00').time() <= current_time < pd.Timestamp('09:30').time():
            return 'premarket'
        elif pd.Timestamp('09:30').time() <= current_time < pd.Timestamp('10:30').time():
            return 'opening'
        elif pd.Timestamp('10:30').time() <= current_time < pd.Timestamp('15:30').time():
            return 'regular'
        elif pd.Timestamp('15:30').time() <= current_time < pd.Timestamp('16:00').time():
            return 'closing'
        else:
            return 'after_hours'
    
    def adjust_stop_for_time(self, base_stop_pct):
        """Ajustar stop según hora del día"""
        time_period = self.get_time_period()
        multiplier = self.time_adjustments[time_period]
        
        return {
            'adjusted_stop_pct': base_stop_pct * multiplier,
            'time_period': time_period,
            'multiplier': multiplier
        }

Stop Loss Automation

1. Real-Time Stop Monitoring

class RealTimeStopMonitor:
    def __init__(self):
        self.active_stops = {}  # {ticker: stop_info}
        self.alerts = []
        
    def add_stop_order(self, ticker, stop_price, shares, position_type, order_type='market'):
        """Agregar stop order para monitoring"""
        self.active_stops[ticker] = {
            'stop_price': stop_price,
            'shares': shares,
            'position_type': position_type,
            'order_type': order_type,
            'created_time': pd.Timestamp.now(),
            'triggered': False
        }
    
    def check_stops(self, market_data):
        """Check all active stops contra market data"""
        triggered_stops = []
        
        for ticker, stop_info in self.active_stops.items():
            if stop_info['triggered']:
                continue
                
            if ticker in market_data:
                current_price = market_data[ticker]['price']
                
                # Check if stop triggered
                if self.is_stop_triggered(current_price, stop_info):
                    stop_info['triggered'] = True
                    stop_info['trigger_time'] = pd.Timestamp.now()
                    stop_info['trigger_price'] = current_price
                    
                    triggered_stops.append({
                        'ticker': ticker,
                        'stop_info': stop_info,
                        'current_price': current_price
                    })
                    
                    # Generate alert
                    self.generate_stop_alert(ticker, stop_info, current_price)
        
        return triggered_stops
    
    def is_stop_triggered(self, current_price, stop_info):
        """Check si el stop se activó"""
        stop_price = stop_info['stop_price']
        position_type = stop_info['position_type']
        
        if position_type == 'long':
            return current_price <= stop_price
        else:  # short
            return current_price >= stop_price
    
    def generate_stop_alert(self, ticker, stop_info, trigger_price):
        """Generar alerta de stop activado"""
        alert = f"🚨 STOP TRIGGERED: {ticker} @ ${trigger_price:.2f} (Stop: ${stop_info['stop_price']:.2f})"
        self.alerts.append({
            'timestamp': pd.Timestamp.now(),
            'ticker': ticker,
            'message': alert,
            'trigger_price': trigger_price,
            'stop_price': stop_info['stop_price']
        })
        
        # Send immediate notification
        print(alert)
        # Implement Discord/email notification

2. Bracket Orders

class BracketOrderManager:
    def __init__(self, broker_api):
        self.broker_api = broker_api
        self.active_brackets = {}
        
    def place_bracket_order(self, ticker, entry_price, shares, stop_loss_price, 
                          take_profit_price, position_type='long'):
        """Place bracket order (entry + stop + target)"""
        
        try:
            # Main order
            if position_type == 'long':
                main_order = self.broker_api.place_buy_order(ticker, shares, entry_price)
            else:
                main_order = self.broker_api.place_sell_order(ticker, shares, entry_price)
            
            # Stop loss order (OCO - One Cancels Other)
            stop_order = self.broker_api.place_stop_order(
                ticker, shares, stop_loss_price, 
                order_type='sell' if position_type == 'long' else 'buy'
            )
            
            # Take profit order
            profit_order = self.broker_api.place_limit_order(
                ticker, shares, take_profit_price,
                order_type='sell' if position_type == 'long' else 'buy'
            )
            
            # Link orders
            bracket_id = f"{ticker}_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}"
            
            self.active_brackets[bracket_id] = {
                'ticker': ticker,
                'main_order_id': main_order['order_id'],
                'stop_order_id': stop_order['order_id'],
                'profit_order_id': profit_order['order_id'],
                'entry_price': entry_price,
                'stop_price': stop_loss_price,
                'target_price': take_profit_price,
                'shares': shares,
                'position_type': position_type,
                'status': 'pending'
            }
            
            return bracket_id
            
        except Exception as e:
            print(f"Error placing bracket order: {e}")
            return None
    
    def update_bracket_status(self, bracket_id):
        """Update status del bracket order"""
        if bracket_id not in self.active_brackets:
            return None
        
        bracket = self.active_brackets[bracket_id]
        
        # Check order status
        main_status = self.broker_api.get_order_status(bracket['main_order_id'])
        
        if main_status == 'filled':
            bracket['status'] = 'active'
            # Now monitor stop and profit orders
        elif main_status == 'cancelled':
            bracket['status'] = 'cancelled'
            # Cancel associated orders
        
        return bracket['status']

Stop Loss Psychology

1. Mental Stop vs Hard Stop

class StopLossStrategy:
    def __init__(self, use_hard_stops=True, mental_stop_buffer_pct=0.005):
        self.use_hard_stops = use_hard_stops
        self.mental_stop_buffer_pct = mental_stop_buffer_pct
        
    def set_stop_strategy(self, ticker, entry_price, base_stop_pct):
        """Decidir estrategia de stop"""
        if self.use_hard_stops:
            # Hard stop: orden automática en el broker
            stop_price = entry_price * (1 - base_stop_pct)
            return {
                'type': 'hard_stop',
                'stop_price': stop_price,
                'automatic': True
            }
        else:
            # Mental stop: monitoring manual
            mental_stop = entry_price * (1 - base_stop_pct)
            alert_level = mental_stop * (1 + self.mental_stop_buffer_pct)
            
            return {
                'type': 'mental_stop',
                'stop_price': mental_stop,
                'alert_price': alert_level,
                'automatic': False
            }
    
    def should_use_hard_stop(self, ticker, volatility, float_size):
        """Determinar si usar hard stop o mental"""
        # Usar hard stops para:
        # - Stocks muy volátiles
        # - Low float (pueden gapear)
        # - When can't monitor constantly
        
        if volatility > 0.08:  # >8% daily volatility
            return True
        
        if float_size < 10_000_000:  # Low float
            return True
        
        return self.use_hard_stops  # Default setting

Mi Setup Personal de Stops

# stop_config.py
STOP_CONFIG = {
    # Base stop settings
    'default_stop_pct': 0.025,      # 2.5% default stop
    'max_stop_pct': 0.05,           # 5% max stop (never risk more)
    'min_stop_pct': 0.01,           # 1% min stop (for low vol)
    
    # Trailing stop settings
    'trailing_trigger_pct': 0.04,    # Start trailing at 4% gain
    'trailing_step_pct': 0.02,       # Trail by 2%
    'breakeven_trigger_pct': 0.06,   # Move to breakeven at 6% gain
    
    # Time-based adjustments
    'premarket_multiplier': 0.5,     # Tighter stops pre-market
    'opening_hour_multiplier': 1.5,  # Wider stops first hour
    'closing_multiplier': 0.75,      # Tighter stops last 30 min
    
    # Volatility adjustments
    'high_vol_multiplier': 1.5,      # Wider stops for high vol
    'low_vol_multiplier': 0.8,       # Tighter stops for low vol
    
    # Small cap adjustments
    'micro_float_multiplier': 1.25,  # Wider stops for micro float
    'large_gap_multiplier': 1.5,     # Wider stops on big gaps
}

class MyStopManager:
    def __init__(self, config=STOP_CONFIG):
        self.config = config
        self.trailing_manager = TrailingStopManager()
        self.vol_adjuster = VolatilityAdjustedStop()
        self.time_adjuster = TimeBasedStopManager()
        
    def calculate_optimal_stop(self, ticker, entry_price, stock_info):
        """Mi método principal para calcular stops"""
        
        # 1. Base stop
        base_stop_pct = self.config['default_stop_pct']
        
        # 2. Adjust for volatility
        vol_data = self.vol_adjuster.calculate_adjusted_stop(ticker, entry_price)
        vol_adjusted_pct = vol_data['stop_pct']
        
        # 3. Adjust for time
        time_data = self.time_adjuster.adjust_stop_for_time(vol_adjusted_pct)
        time_adjusted_pct = time_data['adjusted_stop_pct']
        
        # 4. Adjust for stock characteristics
        final_stop_pct = time_adjusted_pct
        
        # Float adjustment
        if stock_info.get('float', float('inf')) < 10_000_000:
            final_stop_pct *= self.config['micro_float_multiplier']
        
        # Gap adjustment
        if abs(stock_info.get('gap_pct', 0)) > 20:
            final_stop_pct *= self.config['large_gap_multiplier']
        
        # Apply limits
        final_stop_pct = max(min(final_stop_pct, self.config['max_stop_pct']), 
                           self.config['min_stop_pct'])
        
        # Calculate final stop price
        stop_price = entry_price * (1 - final_stop_pct)
        
        return {
            'stop_price': round(stop_price, 2),
            'stop_pct': final_stop_pct,
            'adjustments': {
                'base_pct': base_stop_pct,
                'volatility_adjusted': vol_adjusted_pct,
                'time_adjusted': time_adjusted_pct,
                'final_pct': final_stop_pct
            },
            'factors': {
                'volatility_multiplier': vol_data['vol_multiplier'],
                'time_multiplier': time_data['multiplier'],
                'time_period': time_data['time_period']
            }
        }

Siguiente Paso

Con stops implementados, completemos la gestión de riesgo con Riesgo Asimétrico.