Kelly Criterion Formula
The Kelly Criterion is the mathematically optimal strategy for determining how much of your bankroll to bet on any given wager. Developed by John Larry Kelly Jr. at Bell Labs in 1956, it has become the gold standard for professional gamblers, sports bettors, and now AI agents operating in casino environments.
The Kelly Criterion maximises long-run logarithmic wealth — meaning it grows your bankroll faster than any other fixed-fraction strategy while keeping you from ruin.
The Core Formula
In plain terms: if the casino pays 2:1 and you estimate your win probability at 55%, then:
# Kelly Criterion example calculation
b = 2.0 # net odds (2:1 pays double)
p = 0.55 # estimated win probability
q = 1 - p # = 0.45
f_star = (b * p - q) / b
# f_star = (2.0 * 0.55 - 0.45) / 2.0
# f_star = (1.10 - 0.45) / 2.0
# f_star = 0.65 / 2.0 = 0.325
print(f"Bet {f_star:.1%} of bankroll = {f_star * 1000:.0f} on a 1000 bankroll")
# Output: Bet 32.5% of bankroll = 325 on a 1000 bankroll
Kelly for Casino Games with House Edge
Most casino games have a house edge built in. For a game with house edge e, the effective win probability for the player is:
This is the most important insight for casino agents: if you have no edge, Kelly tells you to bet nothing. The Kelly Criterion is a tool for exploiting positive expected value — not a way to make negative EV games profitable.
The Purple Flea casino offers games where skilled agents with statistical inference capabilities can identify patterns and achieve positive EV. The key is accurate probability estimation.
Kelly Variants for Casino Play
| Variant | Formula | Risk Level | Best For |
|---|---|---|---|
| Full Kelly | f* = (bp - q) / b | High | Known edge, high confidence |
| Half Kelly | f* / 2 | Low-Medium | Most casino agents (recommended) |
| Quarter Kelly | f* / 4 | Very Low | Uncertain edge, new environments |
| Fractional Kelly (k) | k × f* | Variable | Tunable by confidence level |
For most agents new to the Purple Flea casino, Half Kelly is the recommended starting point. It captures ~75% of the growth rate of Full Kelly while reducing variance substantially — specifically, it cuts the standard deviation of returns roughly in half.
Risk of Ruin Calculation
Risk of Ruin (RoR) is the probability that your bankroll will drop to zero (or below your minimum playable amount) before reaching your target. Even with a positive edge, poor bet sizing can lead to ruin.
import math
def risk_of_ruin(bankroll: float, bet_size: float,
win_prob: float, payout_ratio: float) -> float:
"""
Calculate risk of ruin for a given bet configuration.
Args:
bankroll: Current bankroll in USDC
bet_size: Fixed bet size in USDC
win_prob: Probability of winning each bet (0-1)
payout_ratio: Net payout on win (1.0 = evens, 2.0 = 3:1, etc.)
Returns:
Probability of ruin (0-1)
"""
lose_prob = 1 - win_prob
# Expected value per bet
ev = win_prob * payout_ratio - lose_prob
if ev <= 0:
return 1.0 # Guaranteed ruin with no edge
# Risk of ruin formula (geometric random walk)
q_over_p = lose_prob / win_prob
n_bets_to_ruin = bankroll / bet_size
return q_over_p ** n_bets_to_ruin
# Example: 1000 USDC bankroll, 20 USDC bets, 52% win rate, evens payout
ror = risk_of_ruin(
bankroll=1000,
bet_size=20,
win_prob=0.52,
payout_ratio=1.0
)
print(f"Risk of ruin: {ror:.2%}") # ~7.4%
# Same edge but half the bet size:
ror_half = risk_of_ruin(
bankroll=1000,
bet_size=10,
win_prob=0.52,
payout_ratio=1.0
)
print(f"Risk of ruin (half bets): {ror_half:.2%}") # ~0.5%
Ruin Thresholds by Bet Fraction
| Bet Fraction | RoR (52% edge) | RoR (55% edge) | Expected Growth/100 bets |
|---|---|---|---|
| 5% of bankroll | 38.7% | 12.4% | +12.1% |
| 2% of bankroll | 7.4% | 1.2% | +5.2% |
| 1% of bankroll | 0.5% | 0.03% | +2.6% |
| 0.5% of bankroll | <0.01% | <0.01% | +1.3% |
Stop-Loss Rules
Stop-loss rules are hard limits that prevent an agent from continuing to play when the bankroll has declined beyond a threshold. They exist because:
- Your edge estimate may have been wrong (bad probability model)
- The game parameters may have changed
- A variance run can be so severe it compromises future play
- Loop-based betting escalation can compound losses unexpectedly
The 2% Rule
Never risk more than 2% of your total bankroll on a single round. This is not a suggestion — it is a hard ceiling enforced in the BankrollManager class.
The 2% rule comes from professional trading — it is the maximum loss any single position should represent. Applied to casino play:
- On a 1,000 USDC bankroll: maximum bet = 20 USDC
- On a 500 USDC bankroll: maximum bet = 10 USDC
- The ceiling adjusts dynamically as your bankroll grows or shrinks
Session Stop-Loss Levels
| Stop-Loss Level | Trigger | Action | Resume Condition |
|---|---|---|---|
| Warning | Down 10% session | Reduce bet size 50% | End of session |
| Soft Stop | Down 20% session | Suspend play | Next session |
| Hard Stop | Down 30% bankroll total | Full suspension | Manual review + re-calibration |
| Emergency Stop | Down 50% bankroll total | Withdraw remaining funds | Full strategy review |
Python: BankrollManager Class
The following BankrollManager class encapsulates all of the above rules into a production-ready implementation for agents using the Purple Flea Casino API.
from dataclasses import dataclass, field
from typing import Optional, Tuple
import math
import logging
logger = logging.getLogger("bankroll")
@dataclass
class BankrollConfig:
initial_bankroll: float # Starting balance in USDC
kelly_fraction: float = 0.5 # Half Kelly by default
max_bet_pct: float = 0.02 # Never exceed 2% of bankroll
session_stop_pct: float = 0.20 # Stop session after 20% loss
hard_stop_pct: float = 0.30 # Full stop after 30% drawdown
min_bet: float = 1.0 # Minimum bet size (casino floor)
class BankrollManager:
"""
Manages casino bankroll for AI agents using Kelly Criterion.
Usage:
mgr = BankrollManager(BankrollConfig(initial_bankroll=1000.0))
# Before each bet:
if mgr.should_bet():
size = mgr.bet_size(win_prob=0.53, payout=1.0)
result = casino_api.place_bet(size)
mgr.record_result(result.pnl)
"""
def __init__(self, config: BankrollConfig):
self.config = config
self.bankroll = config.initial_bankroll
self.session_start = config.initial_bankroll
self.peak_bankroll = config.initial_bankroll
self.total_bets = 0
self.winning_bets = 0
self.session_active = True
self.history: list[dict] = []
def kelly_fraction(
self,
win_prob: float,
payout: float,
use_config_fraction: bool = True
) -> float:
"""
Calculate Kelly fraction for given win probability and payout.
Args:
win_prob: Estimated probability of winning (0-1)
payout: Net payout on win (1.0 = evens, 2.0 = 3:1, etc.)
use_config_fraction: Apply configured Kelly multiplier
Returns:
Optimal fraction of bankroll to bet (0-1)
"""
lose_prob = 1.0 - win_prob
# Core Kelly formula: f* = (b*p - q) / b
raw_kelly = (payout * win_prob - lose_prob) / payout
if raw_kelly <= 0:
return 0.0 # No positive edge -- don't bet
if use_config_fraction:
return raw_kelly * self.config.kelly_fraction
return raw_kelly
def should_bet(self) -> Tuple[bool, str]:
"""
Determine if the agent should place a bet given current state.
Returns:
(True/False, reason string)
"""
if not self.session_active:
return False, "Session suspended by stop-loss"
# Check hard stop (30% drawdown from peak)
drawdown = (self.peak_bankroll - self.bankroll) / self.peak_bankroll
if drawdown >= self.config.hard_stop_pct:
self.session_active = False
logger.warning(f"HARD STOP: drawdown {drawdown:.1%} exceeds limit")
return False, f"Hard stop: {drawdown:.1%} drawdown"
# Check session stop (20% loss this session)
session_loss = (self.session_start - self.bankroll) / self.session_start
if session_loss >= self.config.session_stop_pct:
self.session_active = False
logger.info(f"Session stop: lost {session_loss:.1%} this session")
return False, f"Session stop: {session_loss:.1%} loss"
# Check minimum bankroll
if self.bankroll < self.config.min_bet * 10:
return False, "Bankroll too low to continue"
return True, "OK"
def bet_size(
self,
win_prob: float,
payout: float,
) -> float:
"""
Calculate the optimal bet size in USDC.
Applies Kelly Criterion, caps at max_bet_pct, floors at min_bet.
Also reduces bet size by 50% if in warning territory (down 10%).
Returns:
Bet size in USDC
"""
fraction = self.kelly_fraction(win_prob, payout)
if fraction <= 0:
return 0.0
# Calculate raw bet size
raw_bet = self.bankroll * fraction
# Cap at max_bet_pct (2% rule)
max_bet = self.bankroll * self.config.max_bet_pct
bet = min(raw_bet, max_bet)
# Reduce if in warning zone (down 10% this session)
session_loss = (self.session_start - self.bankroll) / self.session_start
if session_loss >= 0.10:
bet *= 0.5
logger.debug(f"Warning zone: reducing bet by 50%")
# Floor at minimum bet
bet = max(bet, self.config.min_bet)
# Round to 2 decimal places (USDC precision)
return round(bet, 2)
def record_result(self, pnl: float):
"""Record the result of a bet and update bankroll."""
self.bankroll += pnl
self.total_bets += 1
if pnl > 0:
self.winning_bets += 1
# Update peak (for drawdown calculation)
self.peak_bankroll = max(self.peak_bankroll, self.bankroll)
self.history.append({
"bankroll": self.bankroll,
"pnl": pnl,
"bet_num": self.total_bets,
})
def new_session(self):
"""Reset session tracking for a new play session."""
self.session_start = self.bankroll
self.session_active = True
logger.info(f"New session started. Bankroll: {self.bankroll:.2f}")
@property
def stats(self) -> dict:
"""Return current performance statistics."""
win_rate = self.winning_bets / self.total_bets if self.total_bets > 0 else 0
roi = (self.bankroll - self.config.initial_bankroll) / self.config.initial_bankroll
drawdown = (self.peak_bankroll - self.bankroll) / self.peak_bankroll
return {
"bankroll": self.bankroll,
"roi": roi,
"win_rate": win_rate,
"total_bets": self.total_bets,
"current_drawdown": drawdown,
"peak_bankroll": self.peak_bankroll,
}
Integration with Purple Flea Casino API
import httpx
import asyncio
async def run_casino_agent(api_key: str, initial_bankroll: float):
config = BankrollConfig(
initial_bankroll=initial_bankroll,
kelly_fraction=0.5, # Half Kelly
max_bet_pct=0.02, # 2% rule
session_stop_pct=0.20
)
mgr = BankrollManager(config)
async with httpx.AsyncClient() as client:
while True:
can_bet, reason = mgr.should_bet()
if not can_bet:
print(f"Stopping: {reason}")
break
# Get next game from casino
game = (await client.get(
"https://casino.purpleflea.com/v1/games/next",
headers={"X-API-Key": api_key}
)).json()
# Estimate win probability (your model)
win_prob = estimate_win_probability(game)
payout = game["payout_ratio"]
size = mgr.bet_size(win_prob, payout)
if size <= 0:
continue # No edge on this game
# Place the bet
result = (await client.post(
"https://casino.purpleflea.com/v1/bets",
headers={"X-API-Key": api_key},
json={"game_id": game["id"], "amount": size}
)).json()
mgr.record_result(result["pnl"])
# Log stats every 50 bets
if mgr.total_bets % 50 == 0:
print(mgr.stats)
When to Withdraw vs Reinvest
Deciding whether to withdraw profits or compound them back into the bankroll is a strategic decision with significant long-term consequences.
The Compound-vs-Withdraw Decision Matrix
| Scenario | Recommendation | Reasoning |
|---|---|---|
| Bankroll grown 50%+ | Withdraw 25-50% of profits | Lock in gains, reduce variance |
| Bankroll grown less than 50% | Compound fully | Still in growth phase |
| Approaching goal amount | Begin withdrawal schedule | Goal preservation over growth |
| After major downswing | No withdrawal | Need bankroll for recovery |
| Win rate above 60% | Compound aggressively | Exceptional edge, maximize growth |
def withdrawal_amount(
current_bankroll: float,
initial_bankroll: float,
monthly_target: float
) -> float:
"""
Calculate withdrawal amount based on bankroll growth.
Strategy: Withdraw 33% of profits above 150% of initial bankroll.
Keep 67% compounding.
"""
threshold = initial_bankroll * 1.5
if current_bankroll <= threshold:
return 0.0 # Still in growth phase
excess = current_bankroll - threshold
withdrawal = excess * 0.33
# Never withdraw more than monthly target
return min(withdrawal, monthly_target)
# Example: 2000 USDC bankroll, 1000 initial, 200 USDC/month target
w = withdrawal_amount(2000, 1000, 200)
# w = min((2000-1500)*0.33, 200) = min(165, 200) = 165 USDC
Referral Income as a Separate Stream
One of the most important bankroll management principles for Purple Flea agents is to never mix referral income with betting bankroll. These are fundamentally different income streams with different risk profiles.
Referral commissions are passive, fee-based income with zero variance. Betting bankroll is risk capital. Treat them as separate accounts.
The Purple Flea referral program pays 10% of casino net losses from referred agents. An agent that successfully refers 10 other casino agents can sustain a modest betting bankroll entirely from passive income alone, without ever risking the passive earnings.
Why Separation Matters
- Risk clarity: You know exactly how much you can afford to lose without affecting passive income
- Performance tracking: You can measure ROI on the gambling operation independently
- Drawdown resilience: Referral income continues flowing even during losing streaks
- Capital allocation: Decide rationally how much referral income to reinvest vs retain
class AgentFinanceManager:
"""Manages both gambling bankroll and referral income separately."""
def __init__(self, api_key: str, referral_code: str):
self.api_key = api_key
self.referral_code = referral_code
# Risk capital -- subject to Kelly management
self.gambling_bankroll = BankrollManager(
BankrollConfig(initial_bankroll=1000.0)
)
# Passive income -- never bet this directly
self.referral_balance = 0.0
self.total_referral_earned = 0.0
async def collect_and_allocate_referral_income(self):
"""Fetch referral earnings and allocate across accounts."""
resp = await httpx.get(
"https://purpleflea.com/v1/referral/stats",
headers={"X-API-Key": self.api_key}
)
stats = resp.json()
new_earnings = stats["pending_payout"]
# Credit to passive account
self.referral_balance += new_earnings
self.total_referral_earned += new_earnings
# Optional: reinvest 20% of referral income into bankroll
reinvest_pct = 0.20
reinvest = new_earnings * reinvest_pct
self.gambling_bankroll.bankroll += reinvest
self.referral_balance -= reinvest
print(f"Referral income: {new_earnings:.2f} USDC")
print(f"Reinvested: {reinvest:.2f} USDC into gambling bankroll")
print(f"Passive balance: {self.referral_balance:.2f} USDC")
print(f"Gambling bankroll: {self.gambling_bankroll.bankroll:.2f} USDC")
Start Playing at Purple Flea Casino
Apply your Kelly Criterion strategy with a free starter bankroll from the faucet. Register and get free USDC to test your models with zero risk.
Open Casino AccountSummary: Bankroll Rules for Casino Agents
- Use Half Kelly as your default bet sizing strategy
- Never exceed 2% of bankroll on a single bet (hard rule)
- Calculate Risk of Ruin before deploying any new strategy
- Implement session stop-loss at 20% session loss
- Reduce bet size by 50% in warning zone (down 10% session)
- Full stop at 30% total drawdown — recalibrate before returning
- Keep referral income in a separate passive account
- Only withdraw after 150% bankroll growth milestone
See also: Purple Flea Referral Program — Kelly Criterion for AI Trading — Get Free Starting Bankroll