Skip to main content

Overview

The DrawdownTracker class manages bankroll state and enforces progressive risk reduction as losses accumulate. The system uses a 4-level framework (green → yellow → red → critical) based on drawdown from the high-water mark, with additional cold-streak detection to catch model degradation early.

Drawdown Levels

The tracker defines four distinct risk zones:
LevelDrawdown RangeAlpha MultiplierMin EV OverrideTrading Status
Green< yellowPct1.0NoneFull sizing
Yellow≥ yellowPct0.50.10Half sizing
Red≥ redPct0N/ASuspended
Critical≥ criticalPct0N/ASuspended
Default thresholds: yellowPct = 0.10 (10%), redPct = 0.15 (15%), criticalPct = 0.20 (20%)

Drawdown Calculation

Drawdown is computed as the percentage decline from the high-water mark:
drawdown = (highWaterMark - bankroll) / highWaterMark
  • High-water mark: The maximum bankroll value ever achieved
  • Bankroll: Current account balance after all trades
  • Drawdown: Always a value between 0 (at peak) and 1 (total loss)

Level Determination Logic

getLevel() {
  const drawdown = (this._highWaterMark - this._bankroll) / this._highWaterMark

  if (drawdown >= this._criticalPct) return 'critical'
  if (drawdown >= this._redPct) return 'red'
  if (drawdown >= this._yellowPct) return 'yellow'

  // Green zone, but cold streak may force yellow
  if (this._forcedYellow) return 'yellow'

  return 'green'
}
Source: src/risk/drawdown-tracker.js:81
Even in the green zone (drawdown < 10%), the cold-streak circuit breaker can force yellow mode if the model exhibits consecutive high-confidence misses.

Cold-Streak Circuit Breaker

The tracker monitors consecutive high-confidence prediction misses as an early warning signal for model degradation:

Triggering Conditions

  1. Confidence threshold: Prediction confidence ≥ minConfidenceForStreak (default: 0.70)
  2. Consecutive misses: Streak reaches consecutiveMissThreshold (default: 3)
  3. Result: Force yellow mode even if bankroll drawdown is in green zone

Reset Conditions

The cold streak resets to zero and yellow mode is released when:
  • Any prediction is correct (regardless of confidence level)

Implementation

recordOutcome(correct, confidence) {
  if (correct) {
    this._coldStreak = 0
    this._forcedYellow = false
    return
  }

  if (confidence >= this._minConfidenceForStreak) {
    this._coldStreak++
    if (this._coldStreak >= this._consecutiveMissThreshold) {
      this._forcedYellow = true
    }
  }
}
Source: src/risk/drawdown-tracker.js:60
The cold-streak mechanism is independent of whether predictions were traded. It tracks prediction accuracy, not trade outcomes, to detect model issues before they cause significant losses.

Bankroll Updates

Trade Recording

The tracker updates bankroll after each completed trade:
recordTrade(betAmount, won) {
  this._tradeCount++

  if (won) {
    this._winCount++
    this._bankroll += betAmount * 0.97  // 3% Polymarket fee
  } else {
    this._bankroll -= betAmount
  }

  if (this._bankroll > this._highWaterMark) {
    this._highWaterMark = this._bankroll
  }
}
Source: src/risk/drawdown-tracker.js:36

Payout Logic

  • Wins: Add betAmount × 0.97 (after 3% Polymarket fee)
  • Losses: Deduct full betAmount
  • High-water mark: Updated only when bankroll reaches a new peak
The high-water mark is never decreased. This ensures drawdown percentages are always calculated from the absolute peak, not a moving baseline.

Position Sizing Adjustments

The getAdjustments() method returns parameters that modify position sizing behavior:
getAdjustments() {
  const level = this.getLevel()

  switch (level) {
    case 'green':
      return { alphaMultiplier: 1.0, minEVOverride: null, suspend: false }
    case 'yellow':
      return { alphaMultiplier: 0.5, minEVOverride: 0.10, suspend: false }
    case 'red':
      return { alphaMultiplier: 0, minEVOverride: null, suspend: true }
    case 'critical':
      return { alphaMultiplier: 0, minEVOverride: null, suspend: true }
  }
}
Source: src/risk/drawdown-tracker.js:106

Parameter Descriptions

ParameterTypeDescription
alphaMultipliernumberMultiplied by the Brier-tier alpha (see Position Sizing)
minEVOverridenumber | nullOverrides the minimum expected value threshold for yellow mode
suspendbooleanIf true, all trading is halted

Effect on Position Sizing

The alphaMultiplier is applied in the PositionSizer calculation:
let alpha = this.getAlpha(brierScore, predictionCount)
if (adjustments?.alphaMultiplier) {
  alpha *= adjustments.alphaMultiplier
}
Example:
  • Brier Tier 3 → base alpha = 0.25
  • Yellow mode → alphaMultiplier = 0.5
  • Effective alpha = 0.25 × 0.5 = 0.125 (half the normal sizing)

State Snapshot

The getState() method returns a comprehensive view of current risk status:
{
  bankroll: number,           // Current bankroll in USD
  initialBankroll: number,    // Starting bankroll
  highWaterMark: number,      // Maximum bankroll ever achieved
  drawdownPct: number,        // Current drawdown (0-1)
  level: string,              // 'green' | 'yellow' | 'red' | 'critical'
  coldStreak: number,         // Consecutive high-confidence misses
  forcedYellow: boolean,      // True if cold streak forced yellow mode
  tradeCount: number,         // Total trades executed
  winCount: number,           // Total winning trades
  pnl: number                 // Profit/loss from initial bankroll
}

Example Scenarios

Scenario 1: Normal Drawdown Progression

Initial:    bankroll = $10,000, HWM = $10,000, level = green
Trade 1:    Win $485 → bankroll = $10,485, HWM = $10,485, level = green
Trade 2:    Lose $500 → bankroll = $9,985, HWM = $10,485
            drawdown = (10,485 - 9,985) / 10,485 = 4.77% → level = green
Trade 3:    Lose $600 → bankroll = $9,385, HWM = $10,485
            drawdown = (10,485 - 9,385) / 10,485 = 10.49% → level = yellow
            alphaMultiplier = 0.5 (half sizing activated)

Scenario 2: Cold-Streak Circuit Breaker

State:      bankroll = $10,200, HWM = $10,200, level = green
Outcome 1:  Wrong, confidence = 0.75 → coldStreak = 1
Outcome 2:  Wrong, confidence = 0.82 → coldStreak = 2
Outcome 3:  Wrong, confidence = 0.71 → coldStreak = 3
            coldStreak >= threshold (3) → forcedYellow = true
            level = yellow (despite 0% drawdown)
Outcome 4:  Correct → coldStreak = 0, forcedYellow = false
            level = green (restored)

Scenario 3: Red Zone Suspension

State:      bankroll = $8,900, HWM = $10,485
            drawdown = (10,485 - 8,900) / 10,485 = 15.12% → level = red
            suspend = true (no trading allowed)
            
(Trading halts until manual intervention or system reset)

Configuration Parameters

Drawdown behavior is controlled by config.risk:
{
  bankroll: 10000,                  // Initial bankroll in USD
  drawdown: {
    yellowPct: 0.10,                // 10% drawdown triggers yellow
    redPct: 0.15,                   // 15% drawdown triggers red
    criticalPct: 0.20               // 20% drawdown triggers critical
  },
  coldStreak: {
    consecutiveMissThreshold: 3,    // Misses before forcing yellow
    minConfidenceForStreak: 0.70    // Confidence threshold for streak
  }
}

Integration with Position Sizer

The drawdown tracker provides adjustments to the PositionSizer via the adjustments parameter:
const adjustments = drawdownTracker.getAdjustments()
const result = positionSizer.calculateBet({
  p,
  q,
  bankroll,
  brierScore,
  predictionCount,
  adjustments  // Contains alphaMultiplier, minEVOverride, suspend
})
If adjustments.suspend === true, the calling code should skip trade execution entirely.

Build docs developers (and LLMs) love