Portfolio Sizing Overview

Kelly, Carver, EqualWeight: pluggable sizers converting signals to target positions

The portfolio layer is where signals become target positions in USD notional. It sits between signal generation and execution.

How do I control the portfolio?

You have three levels of control:

  1. Pick a sizer in hz.run() - the simplest approach. The sizer reads your signals and decides position sizes automatically.
python
result = hz.run(
    strategies=[MyStrategy()],
    portfolio=KellyOptimizer(kelly_fraction=0.25, max_gross_leverage=1.0),
    ...
)
  1. Influence sizing from your strategy - set preferred_notional_usd or max_notional_usd on your signals to give hints or hard caps to the sizer.
python
Signal.increase(market, confidence=0.8, edge_bps=50, expected_vol_bps=100, horizon="3d",
    preferred_notional_usd=5000,   # soft hint: I'd like ~$5k
    max_notional_usd=10000,        # hard cap: never more than $10k
)
  1. Bypass the pipeline entirely - use hz.connect() to place orders manually with full control over every parameter.
python
with hz.connect("paper", initial_cash_usd=100_000) as ex:
    ex.buy("AAPL", qty=10, limit=185.0)
    ex.flatten("MSFT")

Most users start with option 1. See the Portfolio Control code-along for a hands-on comparison of all three sizers.

Does the portfolio rebalance automatically?

Yes. Every tick, the sizer runs again on all active signals and produces new target positions. If a signal changes (e.g., z-score drops from -2.5 to -1.0, below your threshold), the signal expires and the sizer no longer targets that position. The executor will close or reduce it on the next tick.

You don’t call “rebalance” manually. The pipeline does it every tick as part of the normal loop.

Market-neutral and delta-neutral portfolios

Horizon supports neutral portfolios through constraints and strategy logic:

Net-exposure constraint

Cap the net dollar exposure to force approximate market neutrality:

python
from horizon.portfolio import PortfolioConstraints

constraints = PortfolioConstraints(
    max_net_notional_usd=5000,   # net long/short exposure stays within ±$5k
    max_gross_leverage=2.0,       # can be 2× levered, but balanced long/short
)

Strategy-level neutrality

Your strategy can read the current book and emit balancing signals:

python
def evaluate(self, f, universe, ctx):
    signals = []
    # ... your normal signal logic ...

    # Check net exposure and emit offsetting signal if needed
    if ctx.portfolio.net_notional > 10_000:
        # too long - find something to short
        ...
    elif ctx.portfolio.net_notional < -10_000:
        # too short - find something to buy
        ...

    return signals

Delta-neutral (options)

For options portfolios, use the portfolio Greeks available via ctx:

python
def evaluate(self, f, universe, ctx):
    # Read portfolio Greeks
    delta = ctx.portfolio.portfolio_delta        # total delta
    dollar_delta = ctx.portfolio.portfolio_dollar_delta  # dollar delta
    gamma = ctx.portfolio.portfolio_gamma
    vega = ctx.portfolio.portfolio_vega
    theta = ctx.portfolio.portfolio_theta

    # If delta is too large, hedge it
    if dollar_delta is not None and abs(dollar_delta) > 5000:
        # emit a signal to reduce delta exposure
        ...

Correlation awareness

Check whether a new position would increase portfolio concentration:

python
def evaluate(self, f, universe, ctx):
    for m in universe:
        # Skip if highly correlated with existing book
        corr = ctx.portfolio.correlation_with(m.id)
        if corr > 0.8:
            continue

        # Skip if sector is overweight
        if ctx.portfolio.exposure_to_sector("tech") > 0.3:
            continue

        # ... signal logic ...

Available sizers

Plus:

The Sizer protocol

python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Sizer(Protocol):
    name: str

    def optimize(
        self,
        signals: list[Signal],
        current_positions: dict,
        cash: CashSnapshot,
        cov: CovarianceModel | None,
        constraints: PortfolioConstraints,
    ) -> list[TargetPosition]:
        ...

Every sizer has one method that takes all the context and returns a list of TargetPosition objects.

Quick comparison

PropertyEqualWeightKellyCarver
Uses edge estimateNoYesYes (via forecast)
Uses vol estimateNoYesYes (for sizing)
Uses correlationNoYesVia FDM
Vol targetingNoNoYes
BufferingNoNoYes
ComplexityTrivialMediumHigh
When to useBaselineResearch + liveMulti-instrument vol-targeted

Swapping sizers

Change the portfolio= argument to try a different sizer:

python
# Start with EqualWeight
result1 = hz.run(portfolio=EqualWeight(), ...)

# Switch to Kelly
result2 = hz.run(portfolio=KellyOptimizer(kelly_fraction=0.25), ...)

# Switch to Carver
result3 = hz.run(portfolio=CarverSystematic(annual_vol_target=0.15), ...)

Same strategy, same data, same risk: only the sizer changes. Compare the results.