🇪🇸 Leer en Español 🇺🇸 English

Fixed-Risk Position Sizing

The Art of Not Going Broke

Position sizing is the difference between being profitable and going broke. You can have the best strategy in the world, but if you risk too much on each trade, a losing streak will wipe you out.

⚠️ Small Caps Warning: Extreme volatility requires more conservative position sizing than blue chips. Our rule: maximum 1-2% risk per trade in small caps.

Philosophy: Risk-First Approach

# ❌ BAD: Thinking about gains first
def bad_position_sizing(account_value, stock_price):
    """I want to buy $10,000 of this stock"""
    return 10000 / stock_price

# ✅ GOOD: Thinking about risk first
def good_position_sizing(account_value, entry_price, stop_price, risk_per_trade=0.02):
    """How much can I lose on this 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

The Base Method: 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):
        """Calculate shares based on fixed risk"""
        # Validations
        if entry_price <= 0 or stop_loss_price <= 0:
            return 0
        
        # For long positions
        risk_per_share = abs(entry_price - stop_loss_price)
        
        if risk_per_share == 0:
            return 0
        
        # Amount of money I'm willing to lose
        total_risk_amount = self.account_value * self.max_risk_per_trade
        
        # Shares I can buy
        shares = int(total_risk_amount / risk_per_share)
        
        # Verify it doesn't exceed max 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):
        """Complete position details"""
        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
        }

# Usage example
sizer = FixedRiskSizer(account_value=50000, max_risk_per_trade=0.02)

# Stock at $25, stop loss at $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 for Small Caps

Volatility Adjustments

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):
        """Adjust risk based on stock volatility"""
        # Get historical data
        historical_data = get_historical_data(ticker, lookback_days)
        
        if historical_data.empty:
            return self.base_risk
        
        # Calculate volatility
        returns = historical_data['close'].pct_change().dropna()
        volatility = returns.std() * np.sqrt(252)  # Annualized
        
        # Adjust risk inversely to volatility
        if volatility > 0.8:  # High vol (>80% annual)
            risk_multiplier = 0.5  # Reduce risk to 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 at maximum 3%
        return min(adjusted_risk, 0.03)
    
    def adjust_risk_for_float(self, float_shares):
        """Adjust risk based on float size"""
        if float_shares < 5_000_000:  # Micro float
            return self.base_risk * 0.5  # 50% of normal risk
        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):
        """Smart position sizing for 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  # Reduce risk 50% on large gaps
        
        # 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 for 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 is aggressive, use a fraction
    conservative_kelly = kelly_fraction * 0.25  # 25% of full Kelly
    
    # Cap at maximum 5%
    return min(max(conservative_kelly, 0), 0.05)

def apply_kelly_sizing(historical_trades, current_trade):
    """Apply Kelly to current trade"""
    if len(historical_trades) < 20:  # Need history
        return 0.02  # Default 2%
    
    # Calculate historical statistics
    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):
    """Adjust position size by volatility"""
    if current_volatility <= 0:
        return base_risk
    
    # Scale inversely to volatility
    vol_adjustment = target_volatility / current_volatility
    adjusted_risk = base_risk * vol_adjustment
    
    # Reasonable limits
    return max(min(adjusted_risk, 0.05), 0.005)  # Between 0.5% and 5%

def calculate_portfolio_volatility_target(positions, target_vol=0.15):
    """Calculate sizing to maintain target portfolio volatility"""
    # Simplified version - you actually need correlations
    individual_vols = [pos['volatility'] for pos in positions]
    individual_weights = [pos['weight'] for pos in positions]
    
    # Portfolio vol (assuming average correlation)
    portfolio_vol = np.sqrt(np.sum([(w * v) ** 2 for w, v in zip(individual_weights, individual_vols)]))
    
    # Scale factor to reach 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)
        
        # Keep only the 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):
        """Calculate current risk based on recent performance"""
        if len(self.equity_curve) < self.lookback_period:
            return self.base_risk
        
        recent_equity = self.equity_curve[-self.lookback_period:]
        
        # Calculate current drawdown
        peak = max(recent_equity)
        current = recent_equity[-1]
        current_drawdown = (peak - current) / peak
        
        # Calculate recent returns
        returns = [(recent_equity[i] - recent_equity[i-1]) / recent_equity[i-1] 
                  for i in range(1, len(recent_equity))]
        
        # Recent win rate
        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
        
        # Reduce risk if in drawdown
        if current_drawdown > 0.1:  # 10% drawdown
            risk_multiplier *= 0.5
        elif current_drawdown > 0.05:  # 5% drawdown
            risk_multiplier *= 0.75
        
        # Adjust for recent win rate
        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):
    """Adjust risk based on market regime"""
    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  # Also reduce in complacency
    
    # 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):
    """Adjust sizing considering correlations"""
    if not positions:
        return new_position['base_size']
    
    # Calculate average correlation with existing positions
    correlations = []
    for pos in positions:
        corr = calculate_correlation(pos['ticker'], new_position['ticker'])
        correlations.append(abs(corr))  # Absolute correlation
    
    avg_correlation = np.mean(correlations)
    
    # If high correlation, reduce 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
    
    # Calculate total risk of correlated positions
    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])
    
    # If there's already too much correlated risk, reduce further
    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):
        """Add position and 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

My Personal Setup

# 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):
    """My main position sizing function"""
    
    # 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

Next Step

With position sizing mastered, let’s move on to Daily Risk Limits to protect capital at the portfolio level.