PortfolioOptimizer (v1)
Constraint-aware Kelly optimizer with Ledoit-Wolf covariance shrinkage
The v1 PortfolioOptimizer is the real math behind Horizon’s KellyOptimizer. When horizon.fund._portfolio_optimizer is importable, Horizon’s sizer delegates to it. Otherwise the standalone fallback path runs.
Import
from horizon.fund._portfolio_optimizer import (
PortfolioOptimizer,
OptimizationConstraints,
OptimizationResult,
RebalanceTrigger,
)
What it does
- Constraint-aware Kelly sizing across a basket of strategies
- Ledoit-Wolf covariance shrinkage for robust correlation estimation
- Projected gradient descent to respect per-market, per-exchange, turnover, and position-count constraints
- Rebalance trigger tracking (drift, VaR breach, correlation change)
Constraints
@dataclass(frozen=True)
class OptimizationConstraints:
max_per_market: float = 0.10 # max 10% per single market
max_per_exchange: float = 0.40 # max 40% on any venue
max_per_sector: float = 0.30 # max 30% per sector
max_turnover: float = 0.20 # max 20% of book rebalanced per cycle
max_positions: int = 50 # hard cap on number of positions
All constraints are expressed as fractions of total capital. Per-market, per-exchange, and per-sector limits bound concentration. Turnover limits prevent whipsaw. Position-count keeps the book manageable.
Core optimization
Update return history
Estimate covariance
Estimate expected returns
Compute raw Kelly weights
w = λ · Σ⁻¹ · μ where Σ⁻¹ is the inverse covariance and μ is the expected return vector. λ is the Kelly fraction (risk aversion scalar).Project onto constraints
Detect rebalance triggers
drift)
- Predicted VaR breached an absolute limit (var_breach)
- Correlation structure changed meaningfully (correlation_change) Each triggers a different rebalance urgency.
API
class PortfolioOptimizer:
def __init__(
self,
constraints: OptimizationConstraints | None = None,
total_capital: float = 100_000,
) -> None:
...
def update_returns(self, strategy_name: str, ret: float) -> None:
"""Append a realized return to the strategy's history."""
def set_exchange(self, strategy_name: str, exchange: str) -> None:
"""Register which exchange a strategy trades on."""
def set_current_weights(self, weights: dict[str, float]) -> None:
"""Update the current allocation snapshot."""
def optimize(
self,
strategy_names: list[str],
engines: dict[str, Any] | None = None,
probs: list[float] | None = None,
prices: list[float] | None = None,
) -> OptimizationResult:
"""Run constrained optimization and return target weights + diagnostics."""
Result
@dataclass(frozen=True)
class OptimizationResult:
target_weights: dict[str, float] # normalized per-strategy weights
expected_return: float
expected_risk: float # predicted portfolio volatility
turnover: float # L1 distance from current weights
violations: list[str] # any constraint violations (should be empty)
rebalance_triggers: list[RebalanceTrigger]
Usage
from horizon.fund._portfolio_optimizer import (
PortfolioOptimizer,
OptimizationConstraints,
)
optimizer = PortfolioOptimizer(
constraints=OptimizationConstraints(
max_per_market=0.08,
max_per_exchange=0.35,
max_turnover=0.15,
max_positions=30,
),
total_capital=500_000,
)
# Register exchanges
optimizer.set_exchange("momentum_tech", "alpaca")
optimizer.set_exchange("meanrev_tech", "alpaca")
optimizer.set_exchange("pred_politics", "polymarket")
# Feed returns as they accumulate
optimizer.update_returns("momentum_tech", 0.012)
optimizer.update_returns("meanrev_tech", -0.005)
optimizer.update_returns("pred_politics", 0.034)
# Update current allocation
optimizer.set_current_weights({
"momentum_tech": 0.30,
"meanrev_tech": 0.30,
"pred_politics": 0.15,
})
# Optimize
result = optimizer.optimize(
strategy_names=["momentum_tech", "meanrev_tech", "pred_politics"],
)
print(f"New weights: {result.target_weights}")
print(f"Expected return: {result.expected_return:+.3%}")
print(f"Expected risk: {result.expected_risk:.3%}")
print(f"Turnover: {result.turnover:.2%}")
print(f"Violations: {result.violations}")
for trigger in result.rebalance_triggers:
print(f" Trigger: {trigger.trigger_type}. {trigger.details}")
Delegation from Horizon
Horizon’s KellyOptimizer tries to import this module at construction time:
# horizon/portfolio/kelly.py
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
At runtime, when KellyOptimizer.optimize() is called:
def optimize(self, signals, current_positions, cash, cov, constraints):
if self._fund_optimizer is not None:
try:
return self._delegate_to_fund_optimizer(...)
except Exception:
pass
return self._standalone(signals, current_positions, cash, constraints)
So in the default install (with horizon.fund available), you’re getting the real Ledoit-Wolf-shrunk constraint-aware math. In a standalone install, you get a simpler but still-correct fallback.
Pitfalls
Source
python/horizon/fund/_portfolio_optimizer.py. ~500 lines. Thread-safe (internal threading.Lock).