Skip to main content

Overview

The PositionSizer class implements a fractional Kelly criterion strategy that dynamically adjusts position sizes based on model performance. The system uses Brier score tiers to map prediction accuracy to a fractional Kelly multiplier (alpha), ensuring conservative sizing during model uncertainty and more aggressive sizing when predictions are highly accurate.

Core Formula

The position sizer uses the fractional Kelly formula:
Full Kelly:       f* = (p - q) / (1 - q)
Fractional Kelly: f_real = α × f*
Bet Size:         bet = f_real × bankroll
Where:
  • p = predicted probability of the outcome
  • q = market implied probability (current price)
  • α = fractional Kelly multiplier (alpha) from Brier tier
  • bankroll = current account balance

Brier-Tiered Alpha Scaling

The system maps model accuracy to five distinct tiers, each with a specific alpha value:
TierConditionAlphaDescription
Tier 0predictionCount < 1000.00Insufficient data - no trading
Tier 1Brier > 0.260.10Low accuracy - minimal sizing
Tier 20.22 ≤ Brier ≤ 0.260.20Moderate accuracy
Tier 30.18 ≤ Brier < 0.220.25Good accuracy
Tier 4Brier < 0.180.40Excellent accuracy - maximum sizing
The alpha values represent fractional Kelly multipliers. Even at Tier 4 (maximum aggression), the system only uses 40% of the full Kelly recommendation to reduce variance.

Implementation Details

getAlpha(brierScore, predictionCount)

Maps the current Brier score and prediction count to a fractional alpha:
getAlpha(brierScore, predictionCount) {
  // Tier 0: insufficient data
  if (predictionCount < 100) return 0

  // Walk tiers from most aggressive (lowest Brier) to least
  for (let i = tiers.length - 1; i >= 1; i--) {
    const tier = tiers[i]
    if (tier.minPredictions !== undefined && predictionCount < tier.minPredictions) continue
    if (brierScore < tier.maxBrier) return tier.alpha
  }

  // Fallback: worst qualifying tier
  return tiers[1].alpha
}
Source: src/risk/position-sizer.js:28

calculateBet(params)

Computes the optimal bet size with the following logic:
  1. Side determination: If p ≥ 0.5, bet YES; otherwise bet NO
  2. Probability inversion: For NO bets, invert both p and q to compute the effective edge
  3. Full Kelly calculation: f* = (pEff - qEff) / (1 - qEff)
  4. Edge check: If f* ≤ 0, no edge exists → return zero bet
  5. Alpha lookup: Retrieve alpha from Brier tier
  6. Drawdown adjustment: Multiply alpha by alphaMultiplier if provided (see Drawdown Tracking)
  7. Fractional sizing: f_real = α × f*
  8. Bet calculation: bet = f_real × bankroll
  9. Cap check: If bet > maxBetPct × bankroll, cap at maximum
  10. Floor check: If bet < minBetUsd, return zero bet
calculateBet({ p, q, bankroll, brierScore, predictionCount, adjustments }) {
  const side = p >= 0.5 ? 'YES' : 'NO'
  const pEff = side === 'YES' ? p : 1 - p
  const qEff = side === 'YES' ? q : 1 - q

  const fullKelly = (pEff - qEff) / (1 - qEff)
  if (fullKelly <= 0) {
    return { bet: 0, fullKelly, alpha: 0, fractionalKelly: 0, capped: false, side }
  }

  let alpha = this.getAlpha(brierScore, predictionCount)
  if (adjustments?.alphaMultiplier) {
    alpha *= adjustments.alphaMultiplier
  }

  const fractionalKelly = alpha * fullKelly
  let bet = fractionalKelly * bankroll
  let capped = false

  const maxBet = this._maxBetPct * bankroll
  if (bet > maxBet) {
    bet = maxBet
    capped = true
  }

  if (bet < this._minBetUsd) {
    return { bet: 0, fullKelly, alpha, fractionalKelly, capped: false, side }
  }

  return { bet, fullKelly, alpha, fractionalKelly, capped, side }
}
Source: src/risk/position-sizer.js:65

Return Values

The calculateBet method returns a detailed object:
{
  bet: number,              // Final bet size in USD (0 if no trade)
  fullKelly: number,        // Full Kelly fraction (before alpha adjustment)
  alpha: number,            // Applied alpha (after drawdown adjustments)
  fractionalKelly: number,  // Fractional Kelly fraction (alpha × fullKelly)
  capped: boolean,          // True if bet was capped at maxBetPct
  side: 'YES' | 'NO'        // Which side of the market to bet
}

Configuration Parameters

Position sizing behavior is controlled by config.risk:
{
  maxBetPct: 0.05,        // Maximum bet as % of bankroll (5%)
  minBetUsd: 10,          // Minimum bet size in USD
  brierTiers: [           // Tier definitions
    { minPredictions: 100, alpha: 0 },    // Tier 0
    { maxBrier: 999, alpha: 0.10 },       // Tier 1
    { maxBrier: 0.26, alpha: 0.20 },      // Tier 2
    { maxBrier: 0.22, alpha: 0.25 },      // Tier 3
    { maxBrier: 0.18, alpha: 0.40 }       // Tier 4
  ]
}

Example Calculation

Given:
  • p = 0.65 (model predicts 65% probability)
  • q = 0.52 (market price is 52¢)
  • bankroll = $10,000
  • brierScore = 0.20 (Tier 3 → α = 0.25)
  • maxBetPct = 0.05
  • No drawdown adjustments
Calculation:
Side:            YES (p ≥ 0.5)
pEff:            0.65
qEff:            0.52
Full Kelly:      (0.65 - 0.52) / (1 - 0.52) = 0.2708
Alpha:           0.25
Fractional Kelly: 0.25 × 0.2708 = 0.0677
Bet:             0.0677 × $10,000 = $677
Max Bet:         0.05 × $10,000 = $500
Final Bet:       $500 (capped)
Result: Bet $500 on YES, capped = true

Integration with Drawdown Tracker

The position sizer accepts optional adjustments from the DrawdownTracker:
  • Green zone: alphaMultiplier = 1.0 (no adjustment)
  • Yellow zone: alphaMultiplier = 0.5 (half sizing)
  • Red/Critical: alphaMultiplier = 0 (trading suspended)
See Drawdown Tracking for details on how drawdown levels affect position sizing.

Build docs developers (and LLMs) love