edgekit.allocate
Capital allocation across strategies as a multi-armed bandit — explore the streams you are unsure about, exploit the ones that are working, and measure the price of not knowing in advance. Thompson sampling, UCB, EWMA weighting, and the regret curve that scores them all.
What's inside. Three weighting rules over a panel of strategy returns — thompson (posterior-probability-of-being-best, the Bayesian explorer), ucb (mean plus an optimism bonus, the frequentist one) and ewma_weights (recency-weighted risk-adjusted momentum, the pragmatist) — plus regret, the cumulative gap to the best-in-hindsight arm that tells you whether any of the cleverness paid.
Every function takes a pd.DataFrame of per-period returns with one column per strategy (R-multiples or fractional returns — just be consistent). Weight vectors come back as a pd.Series indexed by column name, non-negative and summing to 1. These are relative allocations: scale the whole book with edgekit.sizing, and remember the weights are only as causal as the window you compute them on — allocate at t using returns through t - 1.
Weighting rules#
Three personalities for the same job — pick by how much history you have and how much you trust it:
| Rule | Explores via | Best when |
|---|---|---|
thompson | posterior uncertainty | short / uneven histories; you want principled exploration |
ucb | optimism bonus (c * se) | you want a deterministic, tunable exploration knob |
ewma_weights | recency (forgetting) | regimes rotate and old evidence should expire |
thompson#
Thompson sampling: put a Normal posterior on each strategy’s mean return (known-variance approximation — the posterior narrows as 1/sqrt(n)), draw from all posteriors many times, and weight each strategy by how often it wins the draw. Short, noisy histories keep wide posteriors and still get capital; long, mediocre ones get starved. Exploration falls out of the arithmetic — no epsilon knob to tune.
thompson(returns: pd.DataFrame, n_draws: int = 2000, rng=None) -> pd.Series| Param | Type | Default | Meaning |
|---|---|---|---|
returns | pd.DataFrame | — | Per-period returns, one column per strategy. |
n_draws | int | 2000 | Posterior draws used to estimate P(best). |
rng | np.random.Generator | None | Seeded generator; defaults to bootstrap_rng(). |
Returns: a pd.Seriesof weights (each strategy’s probability of being the best arm), non-negative, summing to 1. Deterministic given a seeded rng.
import edgekit as ek
from edgekit.core import bootstrap_rng
w = ek.allocate.thompson(strat_rets, rng=bootstrap_rng())
w.sort_values(ascending=False) # P(each strategy is the best one)
ucb#
Upper Confidence Bound: score each strategy at roughly mean + c * se — its mean return plus an optimism bonus of c standard errors — then floor negative scores at zero and normalise. “Optimism in the face of uncertainty”: unproven strategies ride their upside until the data shrinks it. Larger c explores harder; c = 0 is pure greedy exploitation.
ucb(returns: pd.DataFrame, c: float = 2.0) -> pd.Series| Param | Type | Default | Meaning |
|---|---|---|---|
returns | pd.DataFrame | — | Per-period returns, one column per strategy. |
c | float | 2.0 | Exploration coefficient — standard errors of optimism. |
Returns: a pd.Series of normalised non-negative weights (strategies whose upper bound is still negative get exactly 0).
w = ek.allocate.ucb(strat_rets, c=2.0)
(w == 0).sum() # arms whose optimistic case is still a loserewma_weights#
The pragmatist’s rule: exponentially-weighted mean return over EWMA volatility — a recency-weighted Sharpe — floored at zero and normalised. No posterior, no bonus term, just “give capital to what has recently worked, per unit of recent risk”. The halflife is the whole personality of the rule: 63 days forgets a quarter ago; shorter chases, longer forgives.
ewma_weights(returns: pd.DataFrame, halflife: int = 63) -> pd.Series| Param | Type | Default | Meaning |
|---|---|---|---|
returns | pd.DataFrame | — | Per-period returns, one column per strategy. |
halflife | int | 63 | EWMA halflife in periods (63 ~ a quarter of daily data). |
Returns: a pd.Series of weights proportional to max(ewma_mean, 0) / ewma_vol, summing to 1 (strategies with negative recent mean get 0).
w = ek.allocate.ewma_weights(strat_rets, halflife=63)
# re-run on a schedule; weights drift as regimes rotate
weights_by_month = {m: ek.allocate.ewma_weights(strat_rets.loc[:m]) for m in month_ends}Scoring the allocator#
regret#
Regret is the honest scoreboard for any allocation rule: the cumulative return of the single best strategy in hindsightminus what your weights actually realised. Every allocator pays some regret — hindsight is unfair — but a good one’s regret curve flattens as it learns, while a bad one’s keeps climbing at a constant slope. Pass no weights and it scores the equal-weight portfolio, the benchmark any clever rule must beat.
regret(returns: pd.DataFrame, weights: pd.DataFrame | None = None) -> pd.Series| Param | Type | Default | Meaning |
|---|---|---|---|
returns | pd.DataFrame | — | Per-period returns, one column per strategy. |
weights | pd.DataFrame | None | None | Per-period weights (same shape); equal-weight if None. |
Returns: a pd.Series — the cumulative regret curve (best-in-hindsight cumulative return minus realised), indexed like the input.
reg_eq = ek.allocate.regret(strat_rets) # equal-weight baseline
reg_th = ek.allocate.regret(strat_rets, weights=W_th) # your Thompson weights
# flattening curve = the allocator locked onto the right arm
(reg_th.diff().tail(63).mean(), reg_eq.diff().tail(63).mean())
These rules divide capital among streams that have already survived the gauntlet. Feeding an allocator a panel of unvetted backtests just concentrates capital in whichever one is most overfit — the bandit machinery will happily “learn” noise, and its regret will look great right up until the chosen arm reverts. Validate first, allocate second, and monitor with edgekit.monitor after.
See also#
- Bandits & allocation — the chapter behind this module.
- edgekit.sizing — scale the whole book once the relative weights are set.
- edgekit.monitor — detect when a favoured arm dies.
- edgekit.optimize — covariance-aware weighting when correlations matter more than exploration.

