Kelly is often introduced with one favorable coin toss and one compact betting formula. That example is useful history, but it can leave portfolio builders with the wrong mental model. Real allocation involves several assets, several possible outcomes, and a strict question in every outcome: is the next wealth factor still positive?
This tutorial builds the general finite-scenario version. We will calculate probability-weighted log growth, solve a long-only multi-asset allocation, inspect every scenario wealth factor, and scale full Kelly into explicit cash. We will not call the result safe, and we will not hide zero wealth behind a small numerical epsilon.
Starting toolkit
Read the family’s simplex notation. You also need the identity . Kelly changes the objective to compounded wealth; it does not introduce the distributional ambiguity set used in DRO.
Why logarithmic wealth changes the decision
An arithmetic-return maximizer likes the largest expected payoff. It does not care enough about a rare outcome that destroys the bankroll. Kelly maximizes
Here r_s is the vector of simple returns in scenario s, p_s is its
probability, and w is the portfolio. The factor 1+w^T r_s is wealth after
the scenario divided by wealth before it.
The logarithm converts multiplication across repeated periods into addition and makes zero wealth an absolute boundary. That creates the growth-optimal logic under the supplied model. It does not create a reliable model of the future.
The visual compares full-risky and cash-scaled growth curves. The lab also exposes individual factors, since an average can hide one outcome close to ruin.
The canonical contract
We choose w>=0 with 1^T w=1 and require
for every scenario. The return inputs are decimal simple returns. If a data source supplies log returns, transform the model intentionally; do not insert them into the simple-return wealth equation.
A return of -1 means that asset loses 100%. It is valid input, but a candidate
fully allocated to that asset produces a zero wealth factor and is outside the
log domain. A simple return below -1 is invalid. If every asset is at -1 in
the same scenario, full investment has no feasible solution.
The finite-scenario gradient is
Because log is concave, G is concave on the positive domain. The reference
solver uses projected gradient ascent with backtracking and publishes its
projected-gradient residual.
A symmetric example with an exact answer
Use three synthetic, equally likely scenarios:
| Scenario | Asset A | Asset B |
|---|---|---|
| A leads | 10% | -5% |
| B leads | -5% | 10% |
| Both gain | 2% | 2% |
At equal weights, the factors are [1.025,1.025,1.02]. Therefore
G=(2 log(1.025)+log(1.02))/3=0.02306261749.
At [1,0], the factors are [1.10,0.95,1.02] and
G=0.02127317090. Concentration has a slightly larger best outcome, but the
weaker second factor reduces geometric growth.
We can prove the optimum without trusting code. Write w=[x,1-x]. The first
two factors are 0.95+0.15x and 1.10-0.15x. They are symmetric around
x=0.5, and their sum is fixed. Strict concavity of the log sum makes
x=0.5 the unique optimum.
Fractional Kelly must leave visible cash
Suppose the full Kelly risky weights are [0.5,0.5], but the policy selects a
40% fraction. The correct output is risky weights [0.2,0.2] and cash weight
0.6. If a system scales the risky weights and then renormalizes them back to
[0.5,0.5], it has undone the fraction.
Cash is not an invisible remainder. Its return convention, currency, funding rules, and whether it can be negative all belong in a production contract. The helper here assumes a zero-return cash sleeve only to demonstrate the arithmetic.
Why scenario quality is the real risk
Kelly can be aggressively correct about an incomplete model. If a severe loss scenario is missing or assigned too little probability, the optimizer may take more risk precisely because the logarithmic objective sees no reason not to.
Data lineage is therefore part of the allocation. Before constructing returns, verify security identity, split state, cash dividends, quantity units, quote currency, and FX time. I have encountered old portfolio records where these items were mixed and the apparent return never existed economically. If a cash dividend is paid but only the price drop is recorded, the wealth factor is wrong. If a pre-split price meets a post-split quantity, the factor is wrong. Kelly cannot infer the missing ledger event.
Probabilities also need provenance. Equal weights can describe an empirical sample; they do not become true outcome probabilities merely because the code accepts them. A forecasting or scenario-generation model should record its knowledge cutoff, version, calibration, and uncertainty separately.
What “full Kelly” means in this API
The solver fully allocates among the supplied columns. If those are all risky assets, it solves a risky-only constrained problem. To let the optimizer choose cash, add a column of zeros for zero-return cash. Fractionally scaling a risky-only optimum afterward generally differs from optimizing risky assets and cash jointly.
For example, use two equally likely rows [-0.10,0] and [-0.05,0]. The second column is cash. Every positive risky allocation loses money in both states, so the optimum is [0,1], with log growth zero. A risky-only call with the first column alone must invest in it and produces negative growth. The shared tests verify the cash-inclusive case.
For a less trivial example, risky returns are +100% or −50%, with probabilities 0.6 and 0.4, and the other column is cash. Let be the risky weight:
Solving gives and . This checks a genuinely asymmetric interior optimum rather than only a symmetric starting point.
Log points are not percentage returns
The canonical value is 2.30626174923 log points per modeled period. Its geometric-return equivalent is
Those numbers are close here, but they are different quantities. At a 40% fraction with zero-return cash, the scenario factors become [1.010,1.010,1.008]. Recompute
Do not keep displaying full-risky after changing the fraction. The lab displays deployed factors, deployed log growth, and its geometric equivalent separately. At fraction zero, all factors are one, all wealth stays in cash, and both growth measures are zero.
Repetition, stress rows, and convergence
The long-run interpretation uses a repeated outcome model, conventionally IID draws from the supplied distribution, repeated rebalancing to the chosen weights, and an integrable log-wealth increment. Costs, changing probabilities, and serial dependence require a richer analysis. Busseti, Ryu and Boyd describe the repeated-bet setup, a cash asset, and risk-constrained extensions. One-period expected log growth remains a well-defined objective without claiming that those long-run assumptions hold.
The API treats every supplied row as a hard solvency stress, even if its probability is zero. This is a deliberate stricter contract: a zero-probability all-ruin row is rejected. Remove such a row only if it truly lies outside the modeled support and stress policy. A tiny epsilon in a logarithm would conceal a different objective.
All public helpers validate finite returns, simplex weights and probabilities, and the positive wealth domain. Solver status uses a projected-gradient tolerance with a numerical floor of ; inspect the residual rather than assuming many printed decimals imply exact convergence.
Try it: permute scenario rows together with probabilities. Growth and weights should stay unchanged. Permuting probabilities alone describes a different model and can change the optimum.
Implementation walkthrough
The Python implementation and TypeScript implementation share the same validation, simplex projection, gradient, backtracking, and stopping rule. The result includes the full vector of wealth factors and its minimum, not only the final weights.
The solver starts at equal weight. Because all valid asset returns are at least
-1, equal weight is positive in a scenario whenever at least one asset is
above -1. A scenario where every asset is exactly -1 is rejected as
infeasible before iteration.
Backtracking prevents a step from leaving the positive domain or lowering the
objective. If the projected-gradient residual does not meet tolerance, the
status is nonconverged. A feasible portfolio is not silently upgraded to an
optimum.
Explore full, fractional, and failure states
Open the Kelly guided playground. The canonical state shows all three factors and the equal-weight optimum. Step through factor calculation, logarithms, probability weighting, and the optimizer. Move the fraction control and watch risky weights shrink while cash grows. The failure scenario places all assets at a 100% loss in one outcome and shows why the fully invested problem has no positive-domain solution.
What to notice: the minimum factor is as important as expected log growth. A result that does not expose its weakest scenario is difficult to review.
Tests that protect the definition
The shared fixture verifies the exact equal-weight objective and the
concentrated endpoint. Scenario rows and their probabilities can be permuted
together without changing the solution. Fractional Kelly must preserve the
declared cash weight. Candidate evaluation rejects zero wealth, the optimizer
rejects an all-ruin scenario, and simple returns below -1 fail input validation.
Python and TypeScript reproduce the same result.
These tests prove the calculation, not the probability model. They do not show that the scenarios are exhaustive or that the allocation has acceptable drawdown.
Kelly, drawdown, and suitability
The original Kelly result is about asymptotic growth under its assumptions. Finite-horizon investors can experience deep drawdowns before that asymptotic logic becomes comforting. Risk-constrained Kelly adds an explicit drawdown- probability control. Fractional Kelly is a simpler heuristic. Both are distinct from full Kelly and should be labeled.
The historical-example decision is deferred. A named market example needs licensed scenario returns, defensible probabilities, point-in-time availability, and policies for costs, cash, leverage, drawdown, taxes, and rebalancing. Without those, backfilled Kelly weights would invite hindsight.
What full Kelly does not promise
It does not promise positive realized return, small drawdown, low volatility, liquidity, tax efficiency, or suitability. It assumes the supplied outcome model and one-period mapping are meaningful. Dependence over time, changing probabilities, fat tails outside the scenario set, execution costs, and funding constraints can materially alter the decision.
This tutorial is educational. It is not investment advice, and the synthetic weights are not recommendations.
Run the calculation yourself
From this topic folder, use Python 3.10 or later. No third-party numerical library is required:
python examples/python_example.py
python -m unittest discover -s tests -p "test_*.py"
The key call is shown below. For an interactive Python session, first make the local implementation importable:
import sys
sys.path.insert(0, "implementations/python")
from kelly_allocation import kelly_allocation
print(kelly_allocation([[.1,-.05],[-.05,.1],[.02,.02]],[1/3]*3).to_dict())
The TypeScript example uses the same inputs. See the runnable commands for compilation and execution. Shared cases in the shared regression cases exercise boundary states in both languages; strings NaN and Infinity are test sentinels, not valid numerical inputs.
Summary
An auditable Kelly allocation exposes the complete chain: simple-return scenarios, probabilities, candidate weights, every wealth factor, expected log growth, convergence residual, and explicit cash for any fractional scale. The logarithm makes ruin impossible to hide mathematically; our implementation and interface should not hide it operationally either.
Kelly completes this family by changing the objective. Black-Litterman changes the mean through views, REF measures sampling instability, robust mean-variance protects against a parameter set, and DRO protects against a distribution class.
Choose the family method by the decision you face
| Your real question | Start with | What changes | What it does not solve |
|---|---|---|---|
| How should uncertain views modify a market prior? | Black-Litterman | Expected returns through a prior and views | Long-run growth under supplied scenarios |
| How unstable are optimized weights across plausible samples? | Resampled Efficient Frontier | Rows, replicate moments, and rank-matched frontiers | Ruin avoidance outside observed samples |
| How should a bounded error in the mean affect allocation? | Robust Mean-Variance | Mean vector inside a declared ellipsoid | Log-wealth optimization |
| Which portfolio ranks best under a declared distribution class and loss event? | Distributionally Robust Portfolio | The admissible return distribution | Scenario-specific compounding |
| Which scenario mix maximizes long-run log growth? | Kelly Allocation | Objective and scenario wealth factors | Drawdown comfort, suitability, or scenario quality |
Related concepts and learning handoff
The reviewed The Fintech Builder chapter KKT checks and limits of an optimality claim (17:33–18:43) helps review simplex feasibility and first-order optimality after the log-growth solve. It does not justify the scenario probabilities or prove that full Kelly is suitable for a person.
Rendered from the canonical Mermaid sources linked by this article.
kelly allocation flow
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
KELLY1956 — A New Interpretation of Information Rate
- Organization or authors: J. L. Kelly Jr.
- Source type: Original peer-reviewed Bell System Technical Journal article.
- Publication or effective date: July 1956, 35(4), 917–926.
- Version: DOI
10.1002/j.1538-7305.1956.tb03809.x. - URL or DOI: Wiley record.
- Accessed: 2026-09-15.
- Jurisdiction: None; information-theoretic and gambling framework.
- Supports: Maximum exponential growth-rate interpretation and arbitrary-odds extension.
- Limitations: It is not a portfolio data contract, drawdown prescription, tax model, or profitability forecast.
BRB2016 — Risk-Constrained Kelly Gambling
- Organization or authors: Enzo Busseti, Ernest K. Ryu, and Stephen Boyd.
- Source type: Peer-reviewed Journal of Investing paper and author manuscript.
- Publication or effective date: Fall 2016, 25(3), 118–134.
- Version: Author manuscript dated 19 March 2016.
- URL or DOI: Stanford paper and code page.
- Accessed: 2026-09-15.
- Jurisdiction: None.
- Supports: General finite-outcome Kelly formulation, explicit probabilities and non-negative gross payoffs, and the distinction between drawdown-constrained and fractional approaches.
- Limitations: The risk-constrained extension is a comparison, not part of the canonical objective.
SB2018 — Distributional Robust Kelly Gambling
- Organization or authors: Qingyun Sun and Stephen Boyd.
- Source type: Original research manuscript.
- Publication or effective date: December 2018.
- Version: arXiv
1812.10371. - URL or DOI: Stanford record.
- Accessed: 2026-09-15.
- Jurisdiction: None.
- Supports: Distributionally robust Kelly as a distinct finite-outcome extension when probabilities are uncertain.
- Limitations: That ambiguity-set problem is not implemented here and must not be conflated with ordinary Kelly.
Full dependency-light reference implementations in both supported languages.
export class ContractError extends Error {
}
;
type V = number[];
type M = number[][];
const dot = (a: V, b: V) => a.reduce((s, x, i) => s + x * b[i], 0), norm = (x: V) => Math.sqrt(dot(x, x));
function simplex(v: V): V { const u = [...v].sort((a, b) => b - a); let s = 0, rho = -1; u.forEach((x, i) => { s += x; if (x - (s - 1) / (i + 1) > 0)
rho = i; }); const theta = (u.slice(0, rho + 1).reduce((a, b) => a + b, 0) - 1) / (rho + 1); return v.map(x => Math.max(x - theta, 0)); }
export function logGrowth(weights: V, returns: M, probabilities: V): number { const w = weights.map(Number), rows = returns.map(r => r.map(Number)), p = probabilities.map(Number); if (!w.length || w.some(x => x < 0 || !Number.isFinite(x)) || Math.abs(w.reduce((a, b) => a + b, 0) - 1) > 1e-10)
throw new ContractError("weights must be a finite simplex vector"); if (rows.length !== p.length || !rows.length || rows.some(r => r.length !== w.length))
throw new ContractError("return scenario shape mismatch"); if (rows.flat().some(x => !Number.isFinite(x) || x < -1))
throw new ContractError("returns must be finite and at least -1"); if (p.some(x => !Number.isFinite(x) || x < 0) || Math.abs(p.reduce((a, b) => a + b, 0) - 1) > 1e-10)
throw new ContractError("probabilities must be a finite simplex vector"); const f = rows.map(r => 1 + dot(w, r)); if (Math.min(...f) <= 0)
throw new ContractError("all scenario wealth factors must be strictly positive"); return p.reduce((s, x, i) => s + x * Math.log(f[i]), 0); }
export interface KellyResult {
weights: V;
expectedLogGrowth: number;
wealthFactors: V;
minimumWealthFactor: number;
arithmeticExpectedReturn: number;
iterations: number;
projectedGradientNorm: number;
status: "optimal" | "nonconverged";
}
export function kellyAllocation(returns: M, probabilities?: V, maxIterations = 30000, tolerance = 1e-12): KellyResult { const rows = returns.map(r => r.map(Number)); if (!rows.length || !rows[0]?.length || rows.some(r => r.length !== rows[0].length) || rows.flat().some(x => !Number.isFinite(x)))
throw new ContractError("returns must be a finite rectangular matrix"); if (rows.flat().some(x => x < -1))
throw new ContractError("simple returns below -1 are invalid"); const t = rows.length, n = rows[0].length, p = probabilities?.map(Number) ?? Array(t).fill(1 / t); if (p.length !== t || p.some(x => x < 0 || !Number.isFinite(x)) || Math.abs(p.reduce((a, b) => a + b, 0) - 1) > 1e-10)
throw new ContractError("probabilities must be a finite simplex vector"); if (rows.some(r => Math.max(...r) <= -1))
throw new ContractError("no simplex portfolio has positive wealth in every scenario"); if (!Number.isInteger(maxIterations) || maxIterations < 1 || !Number.isFinite(tolerance) || !(tolerance > 0))
throw new ContractError("iteration controls invalid"); const terms = (w: V) => { const factors = rows.map(r => 1 + dot(w, r)); return { objective: Math.min(...factors) <= 0 ? -Infinity : p.reduce((s, x, i) => s + x * Math.log(factors[i]), 0), factors }; }, grad = (w: V, f: V) => Array.from({ length: n }, (_, j) => p.reduce((sum, x, s) => sum + x * rows[s][j] / f[s], 0)); let w = Array(n).fill(1 / n), z = terms(w), iteration = 0, pg = Infinity; for (iteration = 1; iteration <= maxIterations; iteration++) {
const g = grad(w, z.factors), mapped = simplex(w.map((x, i) => x + g[i]));
pg = norm(mapped.map((x, i) => x - w[i]));
if (pg <= tolerance)
break;
let step = 1, accepted = false;
while (step >= 1e-14) {
const candidate = simplex(w.map((x, i) => x + step * g[i])), nz = terms(candidate);
if (nz.objective >= z.objective + 1e-4 * dot(g, candidate.map((x, i) => x - w[i]))) {
w = candidate;
z = nz;
accepted = true;
break;
}
step *= .5;
}
if (!accepted)
break;
} const g = grad(w, z.factors); pg = norm(simplex(w.map((x, i) => x + g[i])).map((x, i) => x - w[i])); return { weights: w, expectedLogGrowth: z.objective, wealthFactors: z.factors, minimumWealthFactor: Math.min(...z.factors), arithmeticExpectedReturn: p.reduce((s, x, i) => s + x * dot(w, rows[i]), 0), iterations: Math.min(iteration, maxIterations), projectedGradientNorm: pg, status: pg <= Math.max(tolerance, 1e-9) ? "optimal" : "nonconverged" }; }
export function fractionalKelly(fullWeights: V, fraction: number): {
riskyWeights: V;
cashWeight: number;
} { if (!Number.isFinite(fraction) || fraction < 0 || fraction > 1)
throw new ContractError("fraction must lie in [0,1]"); if (!fullWeights.length || fullWeights.some(x => !Number.isFinite(x) || x < 0) || Math.abs(fullWeights.reduce((a, b) => a + b, 0) - 1) > 1e-10)
throw new ContractError("full weights must be a simplex vector"); return { riskyWeights: fullWeights.map(x => fraction * x), cashWeight: 1 - fraction }; }
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.
