Most trading bots get the signal right and still lose money. The entry logic is solid, the exit is clean, but by the end of the year the account is flat or negative. The culprit is almost always position sizing. Too small, and winners don't compound fast enough. Too large, and a single losing streak wipes out months of gains. The Kelly Criterion solves this problem with mathematics — and AI trading agents are uniquely well-suited to use it correctly.

This post covers the Kelly formula from first principles, adapts it for trading, shows why autonomous agents are better at following it than humans, integrates it with the Purple Flea Trading API, and provides a complete Python implementation you can drop into a live bot today.

What Is the Kelly Criterion?

John L. Kelly Jr. published "A New Interpretation of Information Rate" in 1956 while working at Bell Labs. His insight was elegant: if you have a positive-edge bet and you want to maximize the long-run growth rate of your bankroll, there is exactly one fraction to bet — the Kelly fraction — and betting more or less than that fraction is suboptimal.

The original formulation is for binary outcomes (win or lose the full stake), and it looks like this:

f* = (b × p − q) / b
f* = fraction of bankroll to wager b = net odds received (e.g. 2.0 means you win 2x your stake) p = probability of winning q = probability of losing = 1 − p

The formula is saying: bet proportionally to your edge, deflated by the size of the payoff. The bigger the potential win, the less you need to risk to capture the same expected log-growth. At zero edge (bp = q), Kelly returns zero — telling you not to bet at all. When f* is negative, you have a negative-edge bet and should not be on the other side.

1956
Year Kelly published
log(G)
Metric maximized
Kelly at zero edge

Adapting Kelly for Trading

Real trading outcomes are not binary. A winning trade might return 1.2% or 8.4% depending on how the market moves and where the take-profit sits. A losing trade might give back 0.5% or 3%. The original formula needs to be adapted for continuous-outcome trading.

The most practical version for discretionary and algorithmic traders uses your historical win rate, average win size, and average loss size:

f* = (W × R − (1 − W)) / (W × R)
f* = fraction of account to risk on each trade W = win rate (e.g. 0.55 for 55% winners) R = reward-to-risk ratio = average win / average loss (1−W) = loss rate = q in the original notation

This is sometimes written as f* = (W×R - L) / (W×R×L) where L is the fractional loss per losing trade, but with normalized losses of 1.0 the formula above is cleaner. Let's walk through a concrete example.

Suppose your bot has a 55% win rate and its average win is 1.5x its average loss (R = 1.5):

f* = (0.55 × 1.5 − 0.45) / (0.55 × 1.5) = (0.825 − 0.45) / 0.825 ≈ 45.5%

Full Kelly would say to risk 45.5% of your account on every trade. In practice that is aggressive — a string of losers would be devastating. This brings us to fractional Kelly.

Fractional Kelly: The Practical Standard

Full Kelly maximizes log-growth but also maximizes volatility. Most professional traders and quant funds use half-Kelly (0.5×) or quarter-Kelly (0.25×). The trade-off is quantified by the Kelly growth formula: half-Kelly achieves roughly 75% of the maximum growth rate at about half the drawdown depth. Quarter-Kelly gets you around 44% of max growth at about a quarter of the drawdown.

A conservative rule of thumb: use 0.25× Kelly while you are building statistical confidence in your edge, move to 0.5× Kelly once you have 200+ trades in your sample, and only consider full Kelly if you have a robust, low-variance strategy with thousands of data points.

Key insight: Fractional Kelly is not "leaving money on the table" — it is a rational adjustment for estimation error. Your observed win rate and R-ratio are estimates of the true values. Shrinking toward zero compensates for the possibility that your edge is smaller than it looks. The math works out: if you think your edge is X, using 0.5× Kelly is optimal when your edge estimate has even modest uncertainty.

Why AI Agents Are the Ideal Kelly Traders

The Kelly Criterion has been understood by professional traders for decades, yet almost no human traders follow it consistently. The reasons are psychological: Kelly-sized positions feel too large during winning streaks (tempting over-sizing) and emotionally unbearable during drawdowns (triggering panic de-risking). Human traders deviate from Kelly constantly, and those deviations compound into underperformance.

AI trading agents do not have this problem. Here is what makes them uniquely suited for Kelly position sizing:

The combination of strict rule-following and computational speed makes autonomous trading agents the natural home for Kelly-based sizing. The strategy works in theory and in practice — but only if the formula is followed precisely every time. That's exactly what agents do.

Integrating with the Purple Flea Trading API

The Purple Flea Trading API exposes a dedicated Kelly endpoint that returns the maximum position size computed against your current account balance and your rolling performance statistics. This is the right place to get your size ceiling: it combines your Kelly fraction with the broker's own risk controls, ensuring you never accidentally over-leverage through a bug in your local calculation.

The Kelly Limits Endpoint

GET https://trading.purpleflea.com/v1/trade/kelly-limits
# Request GET /v1/trade/kelly-limits Authorization: Bearer <YOUR_API_KEY> Content-Type: application/json # Response { "account_id": "acct_7f2a...", "account_balance_usd": 10000.00, "kelly_fraction": 0.227, "fractional_kelly": 0.25, "max_position_usd": 568.00, "win_rate": 0.56, "reward_risk_ratio": 1.48, "sample_trades": 143, "last_updated": "2026-02-27T14:22:01Z" }

The max_position_usd field is the value you should pass as your position size ceiling. It already accounts for the fractional Kelly multiplier. Your local Kelly calculator (below) should use this as an upper bound, never exceeding it even if local calculations suggest a larger size.

Complete Python Implementation

The following implementation has three components: a KellyCalculator class that handles the formula and maintains running statistics, a PurpleFleasTradingClient that wraps the API, and a complete KellyTradingBot that ties them together.

Kelly Calculator Class

kelly_calculator.py
from dataclasses import dataclass, field from collections import deque from typing import Optional import statistics @dataclass class TradeResult: """Represents a single completed trade.""" pnl_pct: float # profit/loss as fraction of risked capital is_winner: bool symbol: str timestamp: str class KellyCalculator: """ Computes Kelly fraction from a rolling window of trade results. Uses the trading-specific Kelly formula: f* = (W * R - (1 - W)) / (W * R) where W = win rate and R = avg win / avg loss ratio. Args: window_size: Number of recent trades to use for statistics. kelly_multiplier: Fractional Kelly factor (default 0.25 = quarter Kelly). min_samples: Minimum trades required before returning non-zero fraction. """ def __init__( self, window_size: int = 100, kelly_multiplier: float = 0.25, min_samples: int = 30, ): self.window_size = window_size self.kelly_multiplier = kelly_multiplier self.min_samples = min_samples self._trades: deque[TradeResult] = deque(maxlen=window_size) def add_trade(self, result: TradeResult) -> None: """Record a completed trade result.""" self._trades.append(result) def win_rate(self) -> Optional[float]: if not self._trades: return None return sum(1 for t in self._trades if t.is_winner) / len(self._trades) def reward_risk_ratio(self) -> Optional[float]: """Average win / average loss (absolute values).""" winners = [t.pnl_pct for t in self._trades if t.is_winner] losers = [abs(t.pnl_pct) for t in self._trades if not t.is_winner] if not winners or not losers: return None return statistics.mean(winners) / statistics.mean(losers) def full_kelly_fraction(self) -> float: """ Compute the full Kelly fraction. Returns 0.0 if insufficient data or negative edge. """ if len(self._trades) < self.min_samples: return 0.0 W = self.win_rate() R = self.reward_risk_ratio() if W is None or R is None or R == 0: return 0.0 L = 1.0 - W numerator = (W * R) - L denominator = W * R if numerator <= 0: return 0.0 # negative or zero edge: do not trade return numerator / denominator def kelly_fraction(self) -> float: """Return fractional Kelly (multiplier applied).""" return self.full_kelly_fraction() * self.kelly_multiplier def position_size_usd(self, account_balance: float, max_usd: Optional[float] = None) -> float: """ Compute dollar position size from account balance. Optionally capped by max_usd (e.g. from Purple Flea API kelly-limits). """ size = self.kelly_fraction() * account_balance if max_usd is not None: size = min(size, max_usd) return round(size, 2) def summary(self) -> dict: return { "sample_size": len(self._trades), "win_rate": self.win_rate(), "reward_risk_ratio": self.reward_risk_ratio(), "full_kelly": round(self.full_kelly_fraction() * 100, 2), "fractional_kelly": round(self.kelly_fraction() * 100, 2), "multiplier": self.kelly_multiplier, }

Purple Flea API Client

purpleflea_client.py
import httpx from dataclasses import dataclass from typing import Optional @dataclass class KellyLimits: account_balance_usd: float kelly_fraction: float fractional_kelly: float max_position_usd: float win_rate: float reward_risk_ratio: float sample_trades: int class PurpleFleasClient: """Minimal async client for the Purple Flea Trading API.""" BASE_URL = "https://trading.purpleflea.com/v1" def __init__(self, api_key: str): self.api_key = api_key self.headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } def get_kelly_limits(self) -> KellyLimits: """Fetch Kelly position limits from the API.""" with httpx.Client(timeout=10) as client: resp = client.get( f"{self.BASE_URL}/trade/kelly-limits", headers=self.headers, ) resp.raise_for_status() data = resp.json() return KellyLimits( account_balance_usd = data["account_balance_usd"], kelly_fraction = data["kelly_fraction"], fractional_kelly = data["fractional_kelly"], max_position_usd = data["max_position_usd"], win_rate = data["win_rate"], reward_risk_ratio = data["reward_risk_ratio"], sample_trades = data["sample_trades"], ) def place_order( self, symbol: str, side: str, size_usd: float, order_type: str = "market", take_profit: Optional[float] = None, stop_loss: Optional[float] = None, ) -> dict: """Submit an order with Kelly-computed size.""" payload = { "symbol": symbol, "side": side, "size_usd": size_usd, "type": order_type, } if take_profit: payload["take_profit"] = take_profit if stop_loss: payload["stop_loss"] = stop_loss with httpx.Client(timeout=10) as client: resp = client.post( f"{self.BASE_URL}/trade/order", headers=self.headers, json=payload, ) resp.raise_for_status() return resp.json()

Full Trading Bot with Kelly Sizing

kelly_trading_bot.py
import time import logging import os from datetime import datetime, timezone from kelly_calculator import KellyCalculator, TradeResult from purpleflea_client import PurpleFleasClient logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", ) log = logging.getLogger("kelly_bot") class KellyTradingBot: """ Autonomous trading bot that uses Kelly Criterion for position sizing. Workflow per candle / signal: 1. Check signal generator for entry signal 2. Fetch Kelly limits from Purple Flea API (account-level ceiling) 3. Compute local Kelly fraction from running trade history 4. Size position as min(local Kelly size, API max_position_usd) 5. Place order via Purple Flea Trading API 6. On close: record result, update Kelly statistics """ KELLY_LIMITS_REFRESH_S = 300 # re-fetch API limits every 5 minutes SIGNAL_POLL_S = 60 # check for signals every 60 seconds MIN_POSITION_USD = 10.0 # never trade below $10 def __init__(self, api_key: str, symbols: list[str]): self.client = PurpleFleasClient(api_key=api_key) self.symbols = symbols self.kelly = KellyCalculator( window_size=100, kelly_multiplier=0.25, min_samples=30, ) self._kelly_limits = None self._limits_fetched = 0.0 self._open_positions: dict[str, dict] = {} def _refresh_kelly_limits(self) -> None: now = time.time() if now - self._limits_fetched < self.KELLY_LIMITS_REFRESH_S: return try: self._kelly_limits = self.client.get_kelly_limits() self._limits_fetched = now log.info("Kelly limits refreshed: max_position=$%.2f (win_rate=%.1f%%, R=%.2f, n=%d)", self._kelly_limits.max_position_usd, self._kelly_limits.win_rate * 100, self._kelly_limits.reward_risk_ratio, self._kelly_limits.sample_trades, ) except Exception as e: log.warning("Failed to refresh Kelly limits: %s", e) def _compute_position_size(self) -> float: api_max = (self._kelly_limits.max_position_usd if self._kelly_limits else None) balance = (self._kelly_limits.account_balance_usd if self._kelly_limits else 1000.0) size = self.kelly.position_size_usd(balance, max_usd=api_max) return max(size, self.MIN_POSITION_USD) def _generate_signal(self, symbol: str) -> Optional[str]: """ Placeholder: replace with your actual signal logic. Returns 'buy', 'sell', or None. """ return None def _on_trade_closed(self, symbol: str, entry_usd: float, exit_usd: float) -> None: """Record closed trade result and update Kelly statistics.""" pnl_pct = (exit_usd - entry_usd) / entry_usd is_win = pnl_pct > 0 result = TradeResult( pnl_pct=pnl_pct, is_winner=is_win, symbol=symbol, timestamp=datetime.now(timezone.utc).isoformat(), ) self.kelly.add_trade(result) log.info( "Trade closed: %s pnl=%.2f%% | Kelly summary: %s", symbol, pnl_pct * 100, self.kelly.summary(), ) def run(self) -> None: """Main bot loop.""" log.info("KellyTradingBot starting. Symbols: %s", self.symbols) while True: try: self._refresh_kelly_limits() for symbol in self.symbols: signal = self._generate_signal(symbol) if not signal: continue size_usd = self._compute_position_size() log.info("Signal: %s %s | Kelly size: $%.2f", signal, symbol, size_usd) order = self.client.place_order( symbol=symbol, side=signal, size_usd=size_usd, ) self._open_positions[symbol] = order log.info("Order placed: %s", order) except Exception as e: log.error("Bot loop error: %s", e) time.sleep(self.SIGNAL_POLL_S) if __name__ == "__main__": bot = KellyTradingBot( api_key=os.environ["PURPLEFLEA_API_KEY"], symbols=["BTC-USDT", "ETH-USDT", "SOL-USDT"], ) bot.run()

Backtesting: Kelly vs Fixed Sizing

To illustrate the real-world advantage of Kelly sizing, consider a simulated backtest over 100 trades with the following parameters: 55% win rate, 1.5 average win-to-loss ratio, starting account of $10,000. The Kelly fraction works out to approximately 0.227 (22.7% full Kelly), or 5.7% at quarter-Kelly.

Metric Quarter-Kelly (5.7%) Fixed 2% Risk Fixed 10% Risk
Final balance $14,820 $11,940 $9,130
Max drawdown -18.4% -8.2% -41.7%
Compound growth +48.2% +19.4% -8.7%
Worst 10-trade run -12.1% -5.4% -28.3%
Sharpe ratio 1.82 1.47 0.61

The fixed 2% risk strategy is safe but leaves significant growth on the table — it never fully deploys the edge that the strategy provides. The fixed 10% risk strategy initially grows faster but is eventually destroyed by compounding drawdowns; over-betting relative to Kelly always leads to ruin given enough time. Quarter-Kelly threads the needle: it captures meaningful growth while keeping drawdowns survivable.

This pattern holds across nearly all simulations with positive-edge strategies. The conclusion is robust: if you have a genuine edge, Kelly-based sizing outperforms any fixed-fraction strategy over a sufficiently large number of trades. The only question is which fraction of Kelly best matches your risk tolerance.

Common Mistakes When Applying Kelly

Kelly is deceptively simple to state and surprisingly easy to misapply. Here are the most common errors traders make, along with the AI-agent-specific mitigations:

1. Over-Betting Due to Optimistic Win Rate Estimates

Your sample win rate from backtesting is almost always higher than your live win rate due to overfitting, lookahead bias, and market regime changes. If you plug an inflated win rate into the Kelly formula, you will compute a fraction that is too large and will experience ruin-level drawdowns in live trading.

Mitigation: Use a conservative multiplier (0.25× or lower) until you have 100+ live trades. Apply a Bayesian shrinkage factor to your win rate estimate — e.g., blend your observed rate with a prior of 0.50 weighted by sample size. The formula becomes: W_adjusted = (n×W_observed + k×0.50) / (n + k) where k is your prior sample weight (typically 20-50).

2. Ignoring Correlation Between Open Positions

Standard Kelly assumes each bet is independent. In trading, BTC and ETH positions are highly correlated — if you are long both, your effective risk is much larger than two separate Kelly fractions would suggest. Naive Kelly summing can lead to 40-50% portfolio exposure in a single correlated move.

Mitigation: Either trade only one position at a time, or use the multi-asset Kelly formula (see below). At minimum, apply a correlation haircut: if you have N open correlated positions, divide each Kelly fraction by sqrt(N) as a conservative approximation.

3. Failing to Update Win Rate After Market Regime Changes

A strategy with a 60% win rate in a trending market may drop to 40% in a choppy market. If your Kelly calculator is still using the historical 60% win rate, it will dramatically over-size positions at precisely the wrong time.

Mitigation: Use a rolling window (the KellyCalculator above uses a 100-trade deque). Also monitor for win-rate drift: if your rolling 20-trade win rate falls below your 100-trade average by more than 10 percentage points, cut your Kelly multiplier by half until it recovers.

Warning: Never use a win rate calculated from fewer than 30 trades for live Kelly sizing. With small samples, the confidence interval around your win rate is so wide that the Kelly formula output is essentially random. The min_samples=30 parameter in the KellyCalculator above enforces this floor automatically.

4. Applying Kelly to Correlated Sequential Trades

If your signal fires multiple times in the same direction on the same asset within a short window (e.g., a mean-reversion bot pyramiding into a position), those trades are not independent bets. Summing the Kelly fractions across these "separate" signals can concentrate risk dangerously. Treat a multi-entry position as a single Kelly bet and size the total accordingly.

Extension: Multi-Asset Kelly for Uncorrelated Portfolios

When you have a portfolio of strategies trading truly uncorrelated assets — say a BTC trend-following strategy, an ETH mean-reversion strategy, and a stablecoin arbitrage strategy — the multi-asset Kelly formula allocates capital across all three simultaneously to maximize portfolio growth.

For a portfolio of N uncorrelated positions, the optimal allocation vector f* solves:

f* = Σ-1 μ
f* = vector of optimal fractions for each strategy Σ = covariance matrix of returns across strategies μ = vector of mean returns per bet for each strategy

In practice, estimating the full covariance matrix requires substantial data and the inverse is numerically sensitive. A pragmatic shortcut for AI agents with 2-4 uncorrelated strategies is to compute individual Kelly fractions and then normalize so they sum to a maximum total exposure (e.g., cap total portfolio Kelly at 0.3 of account balance):

multi_asset_kelly.py
def allocate_multi_kelly( kelly_fractions: dict[str, float], account_balance: float, max_total_exposure: float = 0.30, ) -> dict[str, float]: """ Normalize individual Kelly fractions so total portfolio exposure does not exceed max_total_exposure * account_balance. Args: kelly_fractions: {symbol: kelly_fraction} from each strategy's calculator. account_balance: Current total account balance in USD. max_total_exposure: Maximum fraction of balance to deploy across all positions. Returns: {symbol: position_size_usd} — dollar allocation per strategy. """ total_kelly = sum(kelly_fractions.values()) if total_kelly == 0: return {symbol: 0.0 for symbol in kelly_fractions} scale_factor = min(1.0, max_total_exposure / total_kelly) max_budget = account_balance * max_total_exposure allocations = {} for symbol, frac in kelly_fractions.items(): raw_size = frac * account_balance * scale_factor allocations[symbol] = round(raw_size, 2) # safety check: total allocation never exceeds budget total_alloc = sum(allocations.values()) if total_alloc > max_budget: correction = max_budget / total_alloc allocations = {s: round(v * correction, 2) for s, v in allocations.items()} return allocations # Example usage kelly_fracs = { "BTC-trend": 0.12, # 12% full Kelly "ETH-meanrev": 0.08, # 8% full Kelly "ARB-stablecoin": 0.15, # 15% full Kelly } sizes = allocate_multi_kelly(kelly_fracs, account_balance=10_000) # {'BTC-trend': 857.14, 'ETH-meanrev': 571.43, 'ARB-stablecoin': 1071.43} # Total: $2,500 = 25% of $10,000 — under the 30% cap

This approach keeps total exposure bounded regardless of how many strategies are running simultaneously, and scales each strategy proportionally to its individual Kelly fraction — preserving the relative optimization while protecting the whole portfolio.


Putting It All Together

The Kelly Criterion is not a trading signal. It says nothing about when to enter or exit a position. What it does is tell you precisely how much to risk once you have decided to trade — and that decision is every bit as important as the entry signal itself. A strategy with a 55% win rate and full-sized positions can lose money. The same strategy with Kelly-optimal sizing compounds into meaningful wealth.

AI trading agents remove the main obstacle to using Kelly correctly: the human tendency to override mathematical rules with emotional judgment. An agent that follows Kelly exactly, updates its statistics after every trade, and never lets greed or fear alter the formula will — given a real edge — outperform any fixed-sizing approach over a sufficient number of trades. That is not a claim; it is a theorem.

The combination of KellyCalculator, the Purple Flea /v1/trade/kelly-limits endpoint, and the KellyTradingBot loop above gives you everything you need to bring this from theory into production. The API-level ceiling ensures you are never exposed to more than the platform's risk engine permits, even if a bug in your local calculator produces an oversize.

Start Trading with Kelly-Optimal Sizing

Connect your agent to the Purple Flea Trading API and let the Kelly endpoint handle position ceiling calculations automatically — updated in real time from your live trading history.

Open Your Account →