The Kelly Criterion With Real Payoffs and Real Fees

The Kelly Criterion With Real Payoffs and Real Fees

The Kelly Criterion is one of those results that gets quoted more often than it gets read. The version you’ll find in most blog posts is

f* = (bp - q) / b

where b is net odds, p is your probability of winning, and q is 1 - p. It’s correct, and it answers a narrower question than most people using it realize, because it has an assumption baked in that real instruments frequently violate.

The hidden assumption

That formula assumes that when you lose, you lose your entire stake.

For a straight sports bet, fine. But plenty of positions do not work that way. A partially collateralized position, an instrument with a rebate on loss, a bet that settles at a fraction, a market with a per-transaction fee on both sides: in all of those, the amount you lose per unit staked is its own parameter, and the classic formula has quietly set it to 1.

This is why keeks, my bankroll management library, has always taken payoff and loss as separate arguments:

from keeks.binary_strategies import KellyCriterion

strategy = KellyCriterion(payoff=1.0, loss=1.0, transaction_cost=0.0)
strategy.evaluate(probability=0.6, current_bankroll=1000)

With payoff=1.0, loss=1.0, transaction_cost=0.0 you’re in textbook territory and the classic formula applies. Change either of the other two and you’re somewhere else.

Deriving the general form

Kelly maximizes the expected logarithm of your bankroll. With separate payoff and loss multipliers, staking fraction f of your bankroll gives

E[log growth] = p * log(1 + f*a) + q * log(1 - f*l)

where a is the payoff multiplier net of costs and l is the loss multiplier including costs. Differentiate with respect to f, set to zero, and you get

p*a / (1 + f*a) = q*l / (1 - f*l)

which solves to

f* = p/l - q/a

That’s the form keeks uses now. Costs enter by adjusting the two multipliers in the natural direction, since a fee makes a win smaller and a loss larger:

adjusted_payoff = self.payoff - self.transaction_cost
adjusted_loss = self.loss + self.transaction_cost

kelly_fraction = probability / adjusted_loss - q / adjusted_payoff

Sanity check it against the classic case. With l = 1 this becomes p - q/a, and since b = a/l = a, that’s exactly (bp - q)/b. The textbook formula is the l = 1 special case, which is what you’d hope.

What actually changes

Here’s the part worth internalizing. The two forms differ by a factor of l:

p/l - q/a   =   (1/l) * (p - q*l/a)

So whenever your loss multiplier plus your fee is not exactly 1, the correct stake differs from the textbook stake by a multiplicative factor. If you lose only half your stake on a loss, the classic formula tells you to bet roughly half of what log-optimal sizing actually wants, which is a substantial amount of foregone growth over many bets. In the other direction, if fees push your effective loss above 1, the classic formula overbets, which is the more dangerous error.

What does not change is the sign. Both forms cross zero at the same place, because both are zero exactly when p*a = q*l, which is the break-even condition. That’s a genuinely useful property: your bet-or-skip decisions are unaffected, only your sizing is. So a strategy that was profitable under the old formula stays profitable, it was just sized wrong.

I verified this numerically rather than trusting the algebra: for a grid of (p, payoff, loss, cost) combinations, compute expected log growth across candidate fractions, find the maximizing fraction by brute force, and confirm the closed form agrees. That kind of check is cheap and it’s the only reason I believe the derivation.

Fees deserve their own paragraph

Transaction costs do something more interesting than shrink your edge. They can invert it.

Consider a bet with a small edge and a fee comparable to the payoff. The gross expectation is positive, the net expectation is negative, and the naive sizing path will happily hand you a positive fraction because it computed the edge before the fee. keeks guards this explicitly:

if adjusted_payoff <= 0 or adjusted_loss <= 0:
    return 0.0

If costs have eaten the entire payoff, there’s no bet at any size. That’s a hard gate, not a small stake, and it belongs before the sizing math rather than after it. The general principle: compute net-of-cost payoffs first, then decide whether there’s an edge, then size. Fee-dominated positions should fall out as no-bets, not as tiny bets.

Layering the safeguards

Full Kelly maximizes growth and is famously unpleasant to live with. The drawdowns are brutal and the sizing is very sensitive to your probability estimate, which in practice is the least reliable input you have. So keeks stacks the usual mitigations on top of the same core:

  • FractionalKellyCriterion(payoff, loss, transaction_cost, fraction) scales the stake by a constant.
  • DrawdownAdjustedKelly(payoff, loss, transaction_cost, max_acceptable_drawdown=0.2) sizes against a drawdown tolerance.
  • A min_probability gate refuses to bet below a confidence floor.
  • A max-safe-bet clamp keeps a single stake from being able to ruin you.

The design property I care about is that every one of those touches size and none of them touches sign. The edge decision and the size decision stay separate, which means you can tune your risk appetite without accidentally changing what you’re willing to bet on at all. Once those two get tangled, a “more conservative” setting starts silently skipping profitable positions and you have no way to see it.

The takeaway

Kelly is an optimization over your payoff structure, not a universal constant. Before you use any published form of it, write down three numbers: what a win pays per unit staked, what a loss costs per unit staked, and what the transaction costs. If your loss is not exactly 1 unit and your fees are not exactly 0, the formula in the blog post is sizing for a different game than the one you’re playing.