Tamaño de Posición con Riesgo Fijo

El Arte de No Quebrar

Position sizing es la diferencia entre ser rentable y quebrar. Puedes tener la mejor estrategia del mundo, pero si arriesgas demasiado en cada trade, un streak de perdidas te elimina.

⚠️ Small Caps Warning: La volatilidad extrema requiere position sizing más conservador que blue chips. Nuestra regla: máximo 1-2% de riesgo por trade en small caps.

Filosofía: Risk-First Approach

# ❌ MALO: Pensar en ganancias primero
def bad_position_sizing(account_value, stock_price):
    """Quiero comprar $10,000 de este stock"""
    return 10000 / stock_price

# ✅ BUENO: Pensar en riesgo primero
def good_position_sizing(account_value, entry_price, stop_price, risk_per_trade=0.02):
    """¿Cuánto puedo perder en este trade?"""
    risk_amount = account_value * risk_per_trade
    risk_per_share = entry_price - stop_price
    return risk_amount / risk_per_share if risk_per_share > 0 else 0

Fixed Risk Position Sizing

El Método Base: 1-2% Risk

class FixedRiskSizer:
    def __init__(self, account_value, max_risk_per_trade=0.02):
        self.account_value = account_value
        self.max_risk_per_trade = max_risk_per_trade
        
    def calculate_shares(self, entry_price, stop_loss_price):
        """Calcular shares basado en riesgo fijo"""
        # Validaciones
        if entry_price <= 0 or stop_loss_price <= 0:
            return 0
        
        # Para long positions
        risk_per_share = abs(entry_price - stop_loss_price)
        
        if risk_per_share == 0:
            return 0
        
        # Cantidad de dinero que estoy dispuesto a perder
        total_risk_amount = self.account_value * self.max_risk_per_trade
        
        # Shares que puedo comprar
        shares = int(total_risk_amount / risk_per_share)
        
        # Verificar que no exceda % máximo del portfolio
        max_position_value = self.account_value * 0.20  # 20% max
        max_shares_by_value = int(max_position_value / entry_price)
        
        return min(shares, max_shares_by_value)
    
    def calculate_position_details(self, entry_price, stop_loss_price):
        """Detalles completos de la posición"""
        shares = self.calculate_shares(entry_price, stop_loss_price)
        
        if shares == 0:
            return None
        
        position_value = shares * entry_price
        risk_amount = shares * abs(entry_price - stop_loss_price)
        
        return {
            'shares': shares,
            'entry_price': entry_price,
            'stop_loss_price': stop_loss_price,
            'position_value': position_value,
            'risk_amount': risk_amount,
            'risk_percentage': risk_amount / self.account_value,
            'position_percentage': position_value / self.account_value
        }

# Ejemplo de uso
sizer = FixedRiskSizer(account_value=50000, max_risk_per_trade=0.02)

# Stock a $25, stop loss a $23
position = sizer.calculate_position_details(entry_price=25, stop_loss_price=23)
print(f"Shares to buy: {position['shares']}")
print(f"Risk amount: ${position['risk_amount']:.2f}")
print(f"Risk percentage: {position['risk_percentage']:.2%}")

Position Sizing para Small Caps

Adjustments para Volatilidad

class SmallCapPositionSizer(FixedRiskSizer):
    def __init__(self, account_value, base_risk=0.02):
        super().__init__(account_value, base_risk)
        self.base_risk = base_risk
        
    def adjust_risk_for_volatility(self, ticker, current_price, lookback_days=20):
        """Ajustar riesgo basado en volatilidad del stock"""
        # Obtener datos históricos
        historical_data = get_historical_data(ticker, lookback_days)
        
        if historical_data.empty:
            return self.base_risk
        
        # Calcular volatilidad
        returns = historical_data['close'].pct_change().dropna()
        volatility = returns.std() * np.sqrt(252)  # Annualized
        
        # Ajustar riesgo inversamente a la volatilidad
        if volatility > 0.8:  # High vol (>80% annual)
            risk_multiplier = 0.5  # Reducir risk al 50%
        elif volatility > 0.5:  # Medium vol
            risk_multiplier = 0.75
        elif volatility > 0.3:  # Low vol
            risk_multiplier = 1.0
        else:  # Very low vol
            risk_multiplier = 1.25
        
        adjusted_risk = self.base_risk * risk_multiplier
        
        # Cap en máximo 3%
        return min(adjusted_risk, 0.03)
    
    def adjust_risk_for_float(self, float_shares):
        """Ajustar riesgo basado en float size"""
        if float_shares < 5_000_000:  # Micro float
            return self.base_risk * 0.5  # 50% del riesgo normal
        elif float_shares < 20_000_000:  # Low float
            return self.base_risk * 0.75
        else:
            return self.base_risk
    
    def calculate_smart_position(self, ticker, entry_price, stop_loss_price, 
                               float_shares=None, gap_percent=None):
        """Position sizing inteligente para small caps"""
        
        # Base risk
        risk = self.base_risk
        
        # Adjust for volatility
        risk = self.adjust_risk_for_volatility(ticker, entry_price)
        
        # Adjust for float
        if float_shares:
            float_adjustment = self.adjust_risk_for_float(float_shares)
            risk = min(risk, float_adjustment)
        
        # Adjust for gap size
        if gap_percent and abs(gap_percent) > 20:
            risk *= 0.5  # Reducir riesgo 50% en gaps grandes
        
        # Update risk and calculate
        original_risk = self.max_risk_per_trade
        self.max_risk_per_trade = risk
        
        position = self.calculate_position_details(entry_price, stop_loss_price)
        
        # Restore original risk
        self.max_risk_per_trade = original_risk
        
        if position:
            position['adjusted_risk'] = risk
            position['risk_factors'] = {
                'base_risk': self.base_risk,
                'volatility_adjusted': risk != self.base_risk,
                'float_adjusted': float_shares is not None,
                'gap_adjusted': gap_percent is not None and abs(gap_percent) > 20
            }
        
        return position

Risk Scaling Strategies

1. Kelly Criterion

def kelly_criterion_sizing(win_rate, avg_win, avg_loss):
    """Kelly Criterion para optimal position sizing"""
    if avg_loss == 0:
        return 0
    
    # Kelly formula: f = (bp - q) / b
    # where b = avg_win/avg_loss, p = win_rate, q = 1-win_rate
    b = avg_win / abs(avg_loss)
    p = win_rate
    q = 1 - win_rate
    
    kelly_fraction = (b * p - q) / b
    
    # Kelly es agresivo, usar fracción
    conservative_kelly = kelly_fraction * 0.25  # 25% of full Kelly
    
    # Cap en máximo 5%
    return min(max(conservative_kelly, 0), 0.05)

def apply_kelly_sizing(historical_trades, current_trade):
    """Aplicar Kelly a trade actual"""
    if len(historical_trades) < 20:  # Necesitas historia
        return 0.02  # Default 2%
    
    # Calcular estadísticas históricas
    wins = [t for t in historical_trades if t > 0]
    losses = [t for t in historical_trades if t < 0]
    
    win_rate = len(wins) / len(historical_trades)
    avg_win = np.mean(wins) if wins else 0
    avg_loss = np.mean(losses) if losses else 0
    
    kelly_size = kelly_criterion_sizing(win_rate, avg_win, avg_loss)
    
    return kelly_size

2. Volatility Adjusted Sizing

def volatility_adjusted_sizing(base_risk, current_volatility, target_volatility=0.15):
    """Ajustar position size por volatilidad"""
    if current_volatility <= 0:
        return base_risk
    
    # Escalar inversamente a la volatilidad
    vol_adjustment = target_volatility / current_volatility
    adjusted_risk = base_risk * vol_adjustment
    
    # Límites razonables
    return max(min(adjusted_risk, 0.05), 0.005)  # Entre 0.5% y 5%

def calculate_portfolio_volatility_target(positions, target_vol=0.15):
    """Calcular sizing para mantener volatilidad de portfolio objetivo"""
    # Simplified version - en realidad necesitas correlaciones
    individual_vols = [pos['volatility'] for pos in positions]
    individual_weights = [pos['weight'] for pos in positions]
    
    # Portfolio vol (asumiendo correlación promedio)
    portfolio_vol = np.sqrt(np.sum([(w * v) ** 2 for w, v in zip(individual_weights, individual_vols)]))
    
    # Scale factor para alcanzar target
    if portfolio_vol > 0:
        scale_factor = target_vol / portfolio_vol
        return min(scale_factor, 2.0)  # Max 2x scaling
    
    return 1.0

Dynamic Position Sizing

1. Equity Curve Adjustment

class DynamicRiskManager:
    def __init__(self, base_risk=0.02, lookback_period=20):
        self.base_risk = base_risk
        self.lookback_period = lookback_period
        self.equity_curve = []
        
    def update_equity(self, new_equity_value):
        """Update equity curve"""
        self.equity_curve.append(new_equity_value)
        
        # Mantener solo el lookback period
        if len(self.equity_curve) > self.lookback_period * 2:
            self.equity_curve = self.equity_curve[-self.lookback_period * 2:]
    
    def calculate_current_risk(self):
        """Calcular riesgo actual basado en performance reciente"""
        if len(self.equity_curve) < self.lookback_period:
            return self.base_risk
        
        recent_equity = self.equity_curve[-self.lookback_period:]
        
        # Calcular drawdown actual
        peak = max(recent_equity)
        current = recent_equity[-1]
        current_drawdown = (peak - current) / peak
        
        # Calcular retornos recientes
        returns = [(recent_equity[i] - recent_equity[i-1]) / recent_equity[i-1] 
                  for i in range(1, len(recent_equity))]
        
        # Win rate reciente
        winning_periods = len([r for r in returns if r > 0])
        recent_win_rate = winning_periods / len(returns)
        
        # Adjust risk based on recent performance
        risk_multiplier = 1.0
        
        # Reducir riesgo si en drawdown
        if current_drawdown > 0.1:  # 10% drawdown
            risk_multiplier *= 0.5
        elif current_drawdown > 0.05:  # 5% drawdown
            risk_multiplier *= 0.75
        
        # Ajustar por win rate reciente
        if recent_win_rate < 0.4:  # <40% win rate
            risk_multiplier *= 0.75
        elif recent_win_rate > 0.6:  # >60% win rate
            risk_multiplier *= 1.25
        
        # Cap limits
        risk_multiplier = max(min(risk_multiplier, 2.0), 0.25)
        
        return self.base_risk * risk_multiplier

2. Market Regime Adjustment

def adjust_risk_for_market_regime(base_risk, vix_level=None, market_trend=None):
    """Ajustar riesgo según régimen de mercado"""
    risk_multiplier = 1.0
    
    # VIX adjustment
    if vix_level:
        if vix_level > 30:  # High fear
            risk_multiplier *= 0.5
        elif vix_level > 20:  # Moderate fear
            risk_multiplier *= 0.75
        elif vix_level < 12:  # Complacency
            risk_multiplier *= 0.8  # También reducir en complacencia
    
    # Market trend adjustment
    if market_trend:
        if market_trend == 'strong_downtrend':
            risk_multiplier *= 0.3
        elif market_trend == 'downtrend':
            risk_multiplier *= 0.6
        elif market_trend == 'sideways':
            risk_multiplier *= 0.8
        elif market_trend == 'uptrend':
            risk_multiplier *= 1.0
        elif market_trend == 'strong_uptrend':
            risk_multiplier *= 1.2
    
    return base_risk * risk_multiplier

Portfolio Level Risk Management

1. Correlation Adjustments

def calculate_correlation_adjusted_sizing(positions, new_position, max_correlated_risk=0.1):
    """Ajustar sizing considerando correlaciones"""
    if not positions:
        return new_position['base_size']
    
    # Calcular correlación promedio con posiciones existentes
    correlations = []
    for pos in positions:
        corr = calculate_correlation(pos['ticker'], new_position['ticker'])
        correlations.append(abs(corr))  # Absolute correlation
    
    avg_correlation = np.mean(correlations)
    
    # Si alta correlación, reducir position size
    if avg_correlation > 0.7:
        correlation_multiplier = 0.5
    elif avg_correlation > 0.5:
        correlation_multiplier = 0.75
    else:
        correlation_multiplier = 1.0
    
    # Calcular riesgo total de posiciones correlacionadas
    correlated_risk = sum([pos['risk_amount'] for pos in positions 
                          if calculate_correlation(pos['ticker'], new_position['ticker']) > 0.5])
    
    account_value = sum([pos['account_value'] for pos in positions])
    
    # Si ya hay mucho riesgo correlacionado, reducir más
    if correlated_risk / account_value > max_correlated_risk:
        correlation_multiplier *= 0.5
    
    return new_position['base_size'] * correlation_multiplier

2. Sector Concentration Limits

class SectorRiskManager:
    def __init__(self, max_sector_risk=0.15):
        self.max_sector_risk = max_sector_risk
        self.sector_exposures = {}
        
    def add_position(self, ticker, sector, risk_amount, account_value):
        """Agregar posición y track sector exposure"""
        if sector not in self.sector_exposures:
            self.sector_exposures[sector] = 0
        
        self.sector_exposures[sector] += risk_amount
        
        # Check if exceeding sector limit
        sector_risk_pct = self.sector_exposures[sector] / account_value
        
        if sector_risk_pct > self.max_sector_risk:
            excess = sector_risk_pct - self.max_sector_risk
            recommended_reduction = excess * account_value
            
            return {
                'approved': False,
                'reason': f'Sector {sector} exposure would be {sector_risk_pct:.2%}',
                'recommended_reduction': recommended_reduction
            }
        
        return {'approved': True}
    
    def get_sector_exposures(self, account_value):
        """Get current sector exposures"""
        return {sector: amount / account_value 
                for sector, amount in self.sector_exposures.items()}

Real-Time Position Monitoring

class PositionMonitor:
    def __init__(self):
        self.positions = {}
        self.alerts = []
        
    def add_position(self, ticker, entry_price, shares, stop_loss, max_risk):
        """Add position to monitor"""
        self.positions[ticker] = {
            'entry_price': entry_price,
            'shares': shares,
            'stop_loss': stop_loss,
            'max_risk': max_risk,
            'current_risk': 0,
            'entry_time': pd.Timestamp.now()
        }
    
    def update_position(self, ticker, current_price):
        """Update position with current price"""
        if ticker not in self.positions:
            return
        
        pos = self.positions[ticker]
        
        # Calculate current risk
        current_risk = (pos['entry_price'] - current_price) * pos['shares']
        pos['current_risk'] = current_risk
        
        # Check alerts
        risk_pct = current_risk / pos['max_risk']
        
        if risk_pct > 0.8:  # 80% of max risk
            self.alerts.append(f"⚠️ {ticker}: Approaching max risk ({risk_pct:.1%})")
        
        if current_price <= pos['stop_loss']:
            self.alerts.append(f"🚨 {ticker}: Stop loss hit at ${current_price}")
    
    def get_portfolio_risk(self):
        """Calculate total portfolio risk"""
        total_risk = sum([pos['current_risk'] for pos in self.positions.values() if pos['current_risk'] > 0])
        return total_risk

Mi Setup Personal

# position_sizing_config.py
RISK_CONFIG = {
    'base_risk_per_trade': 0.015,  # 1.5% base risk
    'max_risk_per_trade': 0.03,    # 3% max risk
    'max_portfolio_risk': 0.06,    # 6% total risk
    'max_sector_concentration': 0.20,  # 20% per sector
    'max_single_position': 0.15,   # 15% max position size
    'volatility_target': 0.15,     # 15% portfolio volatility target
    
    # Small cap adjustments
    'micro_float_multiplier': 0.5,  # 50% size for micro floats
    'large_gap_multiplier': 0.5,    # 50% size for >20% gaps
    'high_vol_multiplier': 0.75,    # 75% size for high vol stocks
    
    # Market regime adjustments
    'high_vix_multiplier': 0.5,     # 50% size when VIX >30
    'bear_market_multiplier': 0.6,  # 60% size in bear market
}

def calculate_final_position_size(ticker, entry_price, stop_loss, account_value):
    """Mi función principal de position sizing"""
    
    # 1. Base sizing
    sizer = SmallCapPositionSizer(account_value, RISK_CONFIG['base_risk_per_trade'])
    
    # 2. Get market data
    market_data = get_market_context()
    stock_data = get_stock_context(ticker)
    
    # 3. Calculate smart position
    position = sizer.calculate_smart_position(
        ticker=ticker,
        entry_price=entry_price,
        stop_loss_price=stop_loss,
        float_shares=stock_data.get('float'),
        gap_percent=stock_data.get('gap_percent')
    )
    
    # 4. Apply market regime adjustments
    market_multiplier = adjust_risk_for_market_regime(
        1.0, 
        vix_level=market_data.get('vix'),
        market_trend=market_data.get('trend')
    )
    
    if position:
        position['shares'] = int(position['shares'] * market_multiplier)
        position['final_adjustments'] = {
            'market_multiplier': market_multiplier,
            'vix_level': market_data.get('vix'),
            'market_trend': market_data.get('trend')
        }
    
    return position

Siguiente Paso

Con position sizing dominado, vamos a Límites de Riesgo Diario para proteger el capital a nivel portfolio.