Skip to main content

position sizing is killing me - fixing it with code

been up since 3am coding a proper position sizing module. my current approach (fixed 2% risk per trade) is bleeding me out.

down another $8k this week. total drawdown now -$48k since january. at this rate i’ll be broke by august.

the problem with fixed % risk
#

everyone says “risk 2% per trade” like it’s some magic number. bullshit. here’s why it doesn’t work for algo trading:

volatility changes: SPX options in jan 2023 had VIX at 18. now it’s at 22. same 2% position size = different actual risk

correlation: when all my strategies correlate (like during march banking crisis), i’m not risking 2% per trade, i’m risking 10%+ on correlated positions

capital efficiency: 2% on a $360k account is $7.2k risk per trade. but some of my option spreads only need $2k margin. why tie up extra capital?

backtested 4 different approaches
#

spent last 2 nights backtesting different position sizing methods on my 2020-2022 data:

Position Sizing Comparison

Figure 1: Backtest results for 4 position sizing strategies. Kelly Criterion wins on Sharpe.

results:

  • Fixed $10k: Sharpe 0.8, safe but underutilizes capital
  • Fixed 2% Risk: Sharpe 1.2, what i’m using now (clearly not working live)
  • Kelly Criterion: Sharpe 1.6, highest risk-adjusted returns
  • Volatility-Adjusted: Sharpe 1.4, scales position based on VIX

risk-return tradeoff
#

Risk-Return Profile

Figure 2: Risk vs return for each sizing method. Kelly has best return for risk taken.

kelly criterion looks amazing in backtests but everyone says it’s too aggressive for live trading. probably right. the math assumes you know your exact win rate and payoff ratio. i definitely don’t after only 90 days live.

my new approach: volatility-adjusted kelly
#

built a hybrid that takes kelly formula but scales it down based on:

  1. current VIX - when VIX > 20, reduce position size 30%
  2. strategy correlation - if another strategy has open position in correlated asset, reduce size 20%
  3. recent drawdown - if down >5% in past 10 days, reduce size 40%

here’s the core logic (simplified):

def calculate_position_size(strategy, current_vix, active_positions, recent_pnl):
    """
    Volatility-adjusted Kelly position sizing

    Args:
        strategy: Strategy object with win_rate, avg_win, avg_loss
        current_vix: Current VIX level
        active_positions: List of currently open positions
        recent_pnl: P&L from last 10 trading days

    Returns:
        Position size in dollars
    """
    # Base Kelly fraction
    win_rate = strategy.win_rate
    avg_win = strategy.avg_win
    avg_loss = abs(strategy.avg_loss)

    # Kelly formula: f = (p*b - q) / b
    # where p = win rate, q = loss rate, b = win/loss ratio
    b = avg_win / avg_loss
    kelly_fraction = (win_rate * b - (1 - win_rate)) / b

    # Conservative: use 1/4 Kelly (recommended for live trading)
    position_fraction = kelly_fraction * 0.25

    # Adjust for volatility
    vix_adjustment = 1.0
    if current_vix > 20:
        vix_adjustment = 0.7  # Reduce 30% in high vol
    elif current_vix > 25:
        vix_adjustment = 0.5  # Reduce 50% in extreme vol

    position_fraction *= vix_adjustment

    # Check for correlated positions
    correlation_adjustment = 1.0
    for pos in active_positions:
        if is_correlated(strategy.asset, pos.asset, threshold=0.6):
            correlation_adjustment *= 0.8  # Reduce 20% per correlated position

    position_fraction *= correlation_adjustment

    # Drawdown adjustment
    dd_adjustment = 1.0
    if recent_pnl < -0.05:  # Down >5%
        dd_adjustment = 0.6  # Reduce 40% during drawdown

    position_fraction *= dd_adjustment

    # Apply to account equity
    account_equity = get_account_equity()
    position_size = account_equity * position_fraction

    # Sanity checks
    MAX_POSITION = account_equity * 0.10  # Never risk >10% on single trade
    MIN_POSITION = 1000  # Minimum $1k position

    position_size = max(min(position_size, MAX_POSITION), MIN_POSITION)

    return position_size


def is_correlated(asset1, asset2, threshold=0.6):
    """Check if two assets are correlated above threshold"""
    # Fetch 30-day correlation from historical data
    correlation = calculate_correlation(asset1, asset2, days=30)
    return abs(correlation) > threshold


def get_account_equity():
    """Get current account equity from broker API"""
    # Integration with Interactive Brokers API
    return ib_connection.account_summary()['TotalCashValue']

testing it on paper first
#

not deploying this live yet. learned my lesson about jumping straight to live trading.

gonna run this on paper for 2 weeks minimum. need to see:

  • does it actually reduce position size during high vol? ✓
  • does correlation detection work? (testing now)
  • does drawdown protection kick in correctly? ✓
  • what’s the max position size it generates? (need to verify not too aggressive)

the math checks out but…
#

backtests show this could’ve saved me $20k in losses during march. but backtests also showed my original strategies would work, so grain of salt.

main concern: am i over-engineering this? maybe the real problem isn’t position sizing, it’s that my strategies suck.

been reading some algo trading discussions on reddit and seems like everyone struggles with this. some traders just use fixed fractional and call it a day.

but i’m down $48k in 3 months so i need to do SOMETHING different.


4:23am. gonna test this on paper starting monday. if it works i’ll share the full code on github.

-AK

Related

modeling slippage the right way
the slippage problem is worse than i thought # after 2 weeks live trading (10 total trades), my average slippage is $6.40 per spread
first algo went live today
three years of paper trading, finally real money # started learning algo trading april 2020 during COVID lockdown. i was 16, bored af at home, discovered r/algotrading and fell into the rabbit hole
upgraded IV rank filtering
the IV rank problem # my original algo only sells premium when IV rank > 40 IV rank = where current IV sits relative to its 52-week range