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_usd are 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.

Next