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

python
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

python
@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

Per strategy, maintain a rolling deque of realized returns (default max 100).

Estimate covariance

Use Ledoit-Wolf shrinkage: blends the sample covariance with a structured target (diagonal or constant-correlation) to produce a more stable estimate, especially with few observations.

Estimate expected returns

Rolling mean of per-strategy returns. For very-short history, falls back to uniform.

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

Projected gradient descent iteratively enforces: - Each weight in [0, max_per_market] - Sum of weights ≤ 1 - Sum per exchange ≤ max_per_exchange - Non-zero weights ≤ max_positions - Turnover vs current weights ≤ max_turnover

Detect rebalance triggers

Track whether: - Weights drifted past a threshold (drift) - Predicted VaR breached an absolute limit (var_breach) - Correlation structure changed meaningfully (correlation_change)

Each triggers a different rebalance urgency.

API

python
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

python
@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

python
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:

python
# 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:

python
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).

Next