Portfolio Constraints
Per-market, per-venue, per-asset-class, and portfolio-wide caps
PortfolioConstraints is the dataclass that every sizer respects when producing target positions. It lives in horizon.portfolio.base.
Full dataclass
python
@dataclass
class PortfolioConstraints:
# Global
max_gross_notional_usd: float = 0.0
max_net_notional_usd: float = 0.0
max_gross_leverage: float = 2.0
# Per venue
venue_budgets: dict[str, float] = field(default_factory=dict)
max_venue_utilization: float = 0.9
# Per asset class
per_asset_class: dict[AssetClass, AssetClassLimits] = field(default_factory=dict)
# Per market
max_notional_per_market_usd: float = 0.0
max_capital_per_market_usd: float = 0.0
# Sector / correlation
max_sector_exposure: dict[str, float] = field(default_factory=dict)
max_cluster_exposure: float | None = None
Field reference
AssetClassLimits
Per-class limits:
python
@dataclass
class AssetClassLimits:
max_notional_usd: float | None = None
max_capital_usd: float | None = None
max_positions: int | None = None
# Options-specific
max_portfolio_delta: float | None = None
max_portfolio_gamma: float | None = None
max_portfolio_vega: float | None = None
max_theta_burn_per_day_usd: float | None = None
# Perps-specific
max_leverage: float | None = None
max_funding_exposure_per_day_usd: float | None = None
liquidation_buffer_pct: float | None = None
# Prediction-market-specific
max_per_event_usd: float | None = None
min_days_to_resolution: int | None = None
Usage
Most users never construct PortfolioConstraints directly. it’s built from RiskConfig by the run loop. But you can pass one explicitly if you’re calling the sizer outside the run loop:
python
from horizon.portfolio.base import PortfolioConstraints
constraints = PortfolioConstraints(
max_gross_notional_usd=200_000,
max_gross_leverage=1.5,
max_notional_per_market_usd=25_000,
)
targets = sizer.optimize(
signals=my_signals,
current_positions={},
cash=cash_snapshot,
cov=None,
constraints=constraints,
)
How constraints interact with sizers
- Hard constraints: sizers MUST respect these. If a sizer would produce a target that violates
max_notional_per_market_usd, it clamps to the limit. - Soft hints: things like
Signal.preferred_notional_usdare hints, not constraints. Sizers may honor or ignore them. - Constraint priority: when multiple constraints apply, the most restrictive wins.
Validation
The dataclass doesn’t validate at construction (fields can be zero or negative without raising). Sizers that read them treat 0.0 as “no limit” for most caps. Set them explicitly to float('inf') if you want to be paranoid.