Sizing positions with drawdown caps
A drawdown-aware Kelly variant that halves position size after a losing streak, with the exact pattern used in poly15m-v6.
The problem with static position sizing
Kelly criterion gives you an optimal fraction of your bankroll per trade assuming your edge estimate is correct. But signal quality varies over time. During choppy markets or regimes your model was not trained on, your actual win rate drops below the estimate, and full Kelly can quickly wipe out 30-50% of your bankroll before you notice the regime change.
Drawdown caps are a mechanical safeguard: reduce size when recent results suggest the edge has degraded, restore it when results normalize.
The trailing loss counter
The simplest version: count the last N trade outcomes. If more than K of them are losses, apply a multiplier less than 1 to your normal Kelly fraction.
interface TradeResult {
won: boolean
timestamp: number
}
function drawdownMultiplier(
recentTrades: TradeResult[],
windowSize = 10,
lossThreshold = 6,
): number {
if (recentTrades.length < windowSize) return 1.0 // not enough data, full size
const window = recentTrades.slice(-windowSize)
const losses = window.filter((t) => !t.won).length
if (losses >= lossThreshold) return 0.5 // halve the size
return 1.0
}This is the pattern used in poly15m-v6: after 6 or more losses in the last 10 trades, every new position is half the normal Kelly size.
The recovery condition
Once the drawdown multiplier kicks in, when do you restore full size? Two common approaches:
- Time-based: after a fixed cooldown period (e.g. 30 minutes), regardless of subsequent results.
- Result-based: once the trailing window returns below the loss threshold.
The result-based approach is more adaptive. Here is both options:
function shouldRestoreFullSize(
recentTrades: TradeResult[],
windowSize = 10,
lossThreshold = 6,
cooldownMs = 30 * 60 * 1000,
lastReductionAt?: number,
): boolean {
// Time-based
if (lastReductionAt && Date.now() - lastReductionAt > cooldownMs) return true
// Result-based
const window = recentTrades.slice(-windowSize)
const losses = window.filter((t) => !t.won).length
return losses < lossThreshold
}Full sizing pipeline with drawdown
Combining Kelly, quarter-Kelly, and the drawdown multiplier:
const KELLY_FRACTION = 0.25
const MIN_BET_USD = 2
function computeSize(
bankroll: number,
estimatedProb: number,
tokenPrice: number,
recentTrades: TradeResult[],
): number {
const kellyFull = kellyWithFee(estimatedProb, tokenPrice)
const ddMultiplier = drawdownMultiplier(recentTrades)
const dollars = bankroll * kellyFull * KELLY_FRACTION * ddMultiplier
return Math.max(dollars, MIN_BET_USD)
}Per-bot balance caps
When running multiple bots on one wallet, you also need a hard per-bot balance cap independent of Kelly. This prevents one bot from consuming the entire wallet in a good streak, leaving nothing for the others.
const BOT_MAX_POSITION_USD = 20 // absolute ceiling
function cappedSize(
kellyDollars: number,
currentExposure: number,
maxExposure = BOT_MAX_POSITION_USD,
): number {
const available = Math.max(0, maxExposure - currentExposure)
return Math.min(kellyDollars, available)
}Logging the multiplier
Always log the drawdown multiplier alongside each trade. When reviewing performance, you want to distinguish "the bot had a signal but reduced size due to drawdown" from "the bot had no signal". Otherwise, the P&L during drawdown looks worse per-trade than it actually is relative to the risk taken.
function logTrade(shares: number, price: number, ddMultiplier: number) {
const tag = ddMultiplier < 1 ? ' [DD-REDUCED]' : ''
console.log(
`[${new Date().toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris' })}] ` +
`BUY ${shares} shares @ $${price.toFixed(2)}${tag}`
)
}Regime detection vs drawdown caps
Drawdown caps are reactive: they respond to losses after the fact. A more proactive approach is to add a chop filter (volume imbalance or ATR threshold) that skips trades when the market regime does not suit the strategy. Drawdown caps and regime filters are complementary -- use both.
Summary
- Count losses in a trailing window of 10 trades; halve size if 6 or more are losses.
- Restore full size either after a 30-minute cooldown or when the window improves.
- Layer the drawdown multiplier on top of fee-adjusted quarter-Kelly.
- Add a per-bot hard cap to protect wallet balance when running multiple strategies.
Related bots
poly15m-v6 — Enhanced orderbook signal BTC scalper
Polymarket book + Chainlink RTDS + CVD + OFI + chop filter + trained meta-model. Needs 3 of 8 confirmation layers before FOK taker at $0.52. Quarter-Kelly with drawdown scaling, holds to resolution.
contrarian — GTC maker bids on underdog outcomes
GTC maker bids on 20-50¢ underdog outcomes across all Polymarket markets. Zero taker fees, 30-min scan loop, multi-layer risk stack. Inspired by @Car's +$22K strategy.