KellyOptimizer

Real Kelly math with cost drag, leverage cap, and fund-module delegation

KellyOptimizer is Horizon’s default sizer. It implements the classical Kelly formula with realistic extensions: transaction cost drag, gross leverage cap, per-market and per-signal notional caps, per-venue budget routing.

When the existing v1 horizon.fund._portfolio_optimizer.PortfolioOptimizer is importable (i.e., when running alongside the v1 codebase), KellyOptimizer delegates to it. using Ledoit-Wolf shrinkage covariance and projected gradient descent. Otherwise the standalone fallback runs.

Import

python
from horizon.portfolio import KellyOptimizer

Signature

python
KellyOptimizer(
    kelly_fraction: float = 0.25,
    max_gross_leverage: float = 2.0,
    transaction_cost_bps: float = 5.0,
    covariance_model: str = "ledoit_wolf",
)
kelly_fractionfloat
Fractional Kelly, the most commonly used risk multiplier. `0.25` = quarter Kelly. Must be in (0, 1].
max_gross_leveragefloat
Hard cap on sum of absolute weights. Must be > 0.
transaction_cost_bpsfloat
Assumed round-trip cost subtracted from edge before sizing. Prevents sizing up positions whose net edge (after costs) is negative.
covariance_modelstr
Covariance estimator name. Forwarded to the fund module wrapper when present; informational in the standalone path.

Validation

The constructor raises ValueError on invalid arguments:

python
KellyOptimizer(kelly_fraction=0)         # ValueError
KellyOptimizer(kelly_fraction=1.5)       # ValueError
KellyOptimizer(max_gross_leverage=0)     # ValueError

The math

For each signal:

edge_net   = (expected_edge_bps - transaction_cost_bps) / 10000
vol        = max(expected_vol_bps, 10) / 10000
kelly_full = edge_net / vol²
kelly_cap  = min(kelly_full, 1.0)        (hard-capped at 100% of equity)
weight     = kelly_cap × kelly_fraction × confidence × direction.sign

Then across all signals:

  1. Gross cap: if Σ |weights| > max_gross_leverage, scale all weights proportionally
  2. Convert to USD: notional_usd = weight × total_equity
  3. Per-market cap: apply constraints.max_notional_per_market_usd
  4. Per-signal cap: apply signal.max_notional_usd if set
  5. Venue routing: apply per-venue budgets for signals prefixed by a venue key (e.g., polymarket:...)

Use it

python
import horizon as hz
from horizon.portfolio import KellyOptimizer

result = hz.run(
    portfolio=KellyOptimizer(
        kelly_fraction=0.25,
        max_gross_leverage=1.5,
        transaction_cost_bps=5,
    ),
    ...
)

Delegation to fund module

python
try:
    from horizon.fund._portfolio_optimizer import PortfolioOptimizer
    self._fund_optimizer = PortfolioOptimizer(
        kelly_fraction=kelly_fraction,
        max_gross_leverage=max_gross_leverage,
        covariance_model=covariance_model,
    )
except Exception:
    self._fund_optimizer = None

When self._fund_optimizer is not None, the v1 math runs with:

  • Ledoit-Wolf covariance shrinkage for robust correlation estimation
  • Projected gradient descent for constraint-aware optimization
  • Rebalance trigger tracking (drift, VaR, correlation change)

When unavailable, the standalone fallback runs the simpler formula above.

See v1 PortfolioOptimizer for the full math of the fund module.

Verified behavior

Hand-calculated cases in tests/test_portfolio.py::TestKellyMath:

InputExpected
edge=100, vol=500, conf=1.0, kelly_fraction=1.0Weight = 1.0 → 100% of equity
Same, kelly_fraction=0.550% of equity
Same, confidence=0.550% of equity (half weight)
edge=5, cost=10 bpsWeight = 0 (net edge negative)
Two strong signals, max_gross_leverage=1.5Each scaled so sum = 1.5 × equity
max_notional_per_market_usd=5000Capped at $5k regardless of edge
signal.max_notional_usd=2000Capped at $2k

8 tests, all passing.

Pitfalls

Fractional Kelly selection

Full Kelly is theoretically optimal but practically too aggressive:

  • Full Kelly (1.0): maximizes expected log wealth but has ~40% probability of 50% drawdown
  • Half Kelly (0.5). 75% of expected growth, much lower drawdown probability
  • Quarter Kelly (0.25). 44% of expected growth, very conservative
  • Eighth Kelly (0.125): very low risk, but growth is only 22% of full

Most practitioners use 0.25 to 0.5. The default is 0.25.

Next