DEV Community

Propfirmkey
Propfirmkey

Posted on

Position Sizing Algorithms for Futures Trading

Position sizing is where math meets money management. In this article, we implement five battle-tested position sizing algorithms in Python and compare their risk-reward profiles through Monte Carlo simulation.

The Five Algorithms

import numpy as np
from abc import ABC, abstractmethod
from typing import Optional

class PositionSizer(ABC):
    @abstractmethod
    def calculate(self, account_balance: float, risk_per_trade: float,
                  stop_distance: float, tick_value: float) -> int:
        pass

class FixedFractional(PositionSizer):
    """Classic fixed percentage risk per trade."""
    def __init__(self, fraction: float = 0.02):
        self.fraction = fraction

    def calculate(self, account_balance, risk_per_trade, stop_distance, tick_value):
        risk_dollars = account_balance * self.fraction
        risk_per_contract = stop_distance * tick_value
        return max(1, int(risk_dollars / risk_per_contract))

class KellyCriterion(PositionSizer):
    """Kelly Criterion — optimal growth rate sizing."""
    def __init__(self, win_rate: float, avg_win: float, avg_loss: float, kelly_fraction: float = 0.5):
        self.win_rate = win_rate
        self.avg_win = avg_win
        self.avg_loss = abs(avg_loss)
        self.kelly_fraction = kelly_fraction  # Half-Kelly is safer

    def calculate(self, account_balance, risk_per_trade, stop_distance, tick_value):
        # Kelly formula: f* = (bp - q) / b
        # b = avg_win / avg_loss, p = win_rate, q = 1 - p
        b = self.avg_win / self.avg_loss if self.avg_loss > 0 else 1
        f_star = (b * self.win_rate - (1 - self.win_rate)) / b
        f_star = max(0, f_star * self.kelly_fraction)

        risk_dollars = account_balance * f_star
        risk_per_contract = stop_distance * tick_value
        return max(1, int(risk_dollars / risk_per_contract))

class FixedRatio(PositionSizer):
    """Ryan Jones' Fixed Ratio — delta-based scaling."""
    def __init__(self, delta: float = 5000):
        self.delta = delta

    def calculate(self, account_balance, risk_per_trade, stop_distance, tick_value):
        # n(n+1)/2 * delta = profit needed
        # Solve for n: n = (-1 + sqrt(1 + 8*P/delta)) / 2
        profit = max(0, account_balance - 50000)  # Assume starting balance
        n = (-1 + np.sqrt(1 + 8 * profit / self.delta)) / 2
        return max(1, int(n) + 1)

class VolatilityBased(PositionSizer):
    """Size based on recent volatility (ATR-based)."""
    def __init__(self, atr_multiplier: float = 2.0, risk_pct: float = 0.01):
        self.atr_multiplier = atr_multiplier
        self.risk_pct = risk_pct

    def calculate(self, account_balance, risk_per_trade, stop_distance, tick_value):
        risk_dollars = account_balance * self.risk_pct
        volatility_stop = stop_distance * self.atr_multiplier * tick_value
        return max(1, int(risk_dollars / volatility_stop))

class AntiMartingale(PositionSizer):
    """Increase size after wins, decrease after losses."""
    def __init__(self, base_contracts: int = 1, scale_factor: float = 1.5):
        self.base_contracts = base_contracts
        self.scale_factor = scale_factor
        self.consecutive_wins = 0

    def update(self, is_win: bool):
        if is_win:
            self.consecutive_wins += 1
        else:
            self.consecutive_wins = 0

    def calculate(self, account_balance, risk_per_trade, stop_distance, tick_value):
        extra = int(self.consecutive_wins * self.scale_factor)
        return self.base_contracts + extra
Enter fullscreen mode Exit fullscreen mode

Monte Carlo Comparison

def monte_carlo_simulation(sizer: PositionSizer, n_trades: int = 500,
                           n_sims: int = 1000, starting_balance: float = 50000,
                           win_rate: float = 0.55, avg_win: float = 400,
                           avg_loss: float = 300) -> dict:
    final_balances = []
    max_drawdowns = []
    blown = 0

    for _ in range(n_sims):
        balance = starting_balance
        peak = starting_balance
        max_dd = 0

        for _ in range(n_trades):
            contracts = sizer.calculate(balance, 0, 10, 12.50)  # ES-like

            if np.random.random() < win_rate:
                pnl = avg_win * contracts
                if hasattr(sizer, 'update'):
                    sizer.update(True)
            else:
                pnl = -avg_loss * contracts
                if hasattr(sizer, 'update'):
                    sizer.update(False)

            balance += pnl
            peak = max(peak, balance)
            dd = (peak - balance) / peak * 100
            max_dd = max(max_dd, dd)

            if balance <= 0:
                blown += 1
                break

        final_balances.append(balance)
        max_drawdowns.append(max_dd)

    return {
        "median_final": np.median(final_balances),
        "mean_final": np.mean(final_balances),
        "std_final": np.std(final_balances),
        "avg_max_dd": np.mean(max_drawdowns),
        "blown_pct": blown / n_sims * 100,
        "p90_final": np.percentile(final_balances, 90),
        "p10_final": np.percentile(final_balances, 10),
    }

# Compare all five
sizers = {
    "Fixed 2%": FixedFractional(0.02),
    "Half Kelly": KellyCriterion(0.55, 400, 300, 0.5),
    "Fixed Ratio": FixedRatio(5000),
    "Volatility": VolatilityBased(2.0, 0.01),
    "Anti-Martingale": AntiMartingale(1, 1.5),
}

print(f"{'Method':<18} {'Median':>10} {'Mean':>10} {'Blown%':>8} {'Avg MaxDD':>10}")
print("-" * 60)

for name, sizer in sizers.items():
    results = monte_carlo_simulation(sizer)
    print(f"{name:<18} ${results['median_final']:>9,.0f} ${results['mean_final']:>9,.0f} "
          f"{results['blown_pct']:>7.1f}% {results['avg_max_dd']:>9.1f}%")
Enter fullscreen mode Exit fullscreen mode

Key Findings

  1. Fixed Fractional offers the best risk-adjusted returns for most traders
  2. Kelly Criterion maximizes long-run growth but has brutal drawdowns at full Kelly — always use half or quarter Kelly
  3. Fixed Ratio scales conservatively, ideal for building confidence
  4. Volatility-based adapts to market conditions automatically
  5. Anti-Martingale can amplify winning streaks but can also give back gains quickly

Practical Recommendation

For most funded account traders, Fixed Fractional at 1% is the safest choice. As you build a track record, graduate to Half Kelly for optimal growth.

The right position sizing depends partly on the drawdown rules your prop firm enforces. For a comprehensive look at how different firms structure their rules, PropFirmKey provides side-by-side comparisons — firms like Alpha Futures that use EOD trailing drawdown allow more aggressive sizing than real-time trailing firms.

Top comments (0)