Skip to main content

Portfolio Management

The Portfolio class provides position tracking, weight optimization, risk analytics, and rebalancing for prediction market portfolios. All heavy computation (Monte Carlo, Greeks, Ledoit-Wolf) runs in Rust.

4 Optimizers

Kelly, equal-weight, risk parity, and minimum variance allocation.

Monte Carlo Risk

VaR, CVaR, win probability, and Greeks via Rust simulation engine.

Auto-Rebalance

Generate rebalancing orders to move from current to target weights.

Live Engine Sync

Build a Portfolio from a running Engine with Portfolio.from_engine().

Quick Start

import horizon as hz

# Create portfolio
portfolio = hz.Portfolio(name="my_portfolio", capital=10000.0)

# Add positions
portfolio.add_position("trump-win", "yes", 200.0, 0.45, 0.55)
portfolio.add_position("fed-cut", "yes", 100.0, 0.60, 0.65)
portfolio.add_position("btc-100k", "no", 150.0, 0.30, 0.25)

# Check current state
print(portfolio.weights())       # {"trump-win": 0.63, "fed-cut": 0.22, "btc-100k": 0.14}
print(portfolio.pnl())           # Total unrealised PnL
print(portfolio.pnl_pct())       # PnL as percentage of cost basis
print(portfolio.concentration()) # HHI concentration index
print(portfolio.summary())       # Human-readable summary

Constructor

portfolio = hz.Portfolio(
    name="my_portfolio",   # Portfolio name (default: "portfolio")
    capital=10000.0,       # Total capital budget (default: 10000.0)
)
ParameterTypeDefaultDescription
namestr"portfolio"Portfolio identifier
capitalfloat10000.0Total capital for weight calculations

Managing Positions

add_position

Add or replace a position in the portfolio.
portfolio.add_position(
    market_id="trump-win",   # Market identifier
    side="yes",              # "yes" or "no"
    size=200.0,              # Position size in contracts
    entry_price=0.45,        # Average entry price
    current_price=0.55,      # Current market probability
)

update_price

Update the current market price for an existing position.
portfolio.update_price("trump-win", 0.58)

remove_position

Remove a position from the portfolio.
portfolio.remove_position("trump-win")

from_engine

Build a Portfolio from a running Engine’s live positions and feed data.
engine = hz.Engine(risk_config=hz.RiskConfig(max_position_per_market=500))
# ... trade for a while ...

portfolio = hz.Portfolio.from_engine(engine, capital=10000.0)
print(portfolio.summary())

Portfolio Analytics

weights

Current weight per market (position value / total portfolio value).
w = portfolio.weights()
# {"trump-win": 0.63, "fed-cut": 0.22, "btc-100k": 0.14}

pnl / pnl_pct

print(portfolio.pnl())      # Total unrealised PnL in dollars
print(portfolio.pnl_pct())  # PnL as percentage of cost basis

concentration

Herfindahl-Hirschman Index (HHI): sum of squared weights. Lower is more diversified. 1/N = perfectly equal.
hhi = portfolio.concentration()
# 0.33 for 3 equal positions, 1.0 for single position

summary

Human-readable portfolio overview.
print(portfolio.summary())
# Portfolio 'my_portfolio' (3 positions)
# Capital: $10,000.00 | PnL: $25.00 (16.7%)
# ...

Weight Optimization

All optimizers return target weights as dict[str, float] mapping market IDs to target allocations.

Kelly Criterion

Compute Kelly-optimal weights given your probability estimates.
kelly_weights = portfolio.optimize_kelly(
    probs={
        "trump-win": 0.60,   # Your estimated probability
        "fed-cut": 0.70,
        "btc-100k": 0.20,   # Probability of YES (you hold NO)
    },
    max_total=1.0,  # Cap total Kelly allocation (default: 1.0)
)
# {"trump-win": 0.37, "fed-cut": 0.28, "btc-100k": 0.13}

Equal Weight

Simple 1/N allocation across all positions.
equal_weights = portfolio.optimize_equal_weight()
# {"trump-win": 0.33, "fed-cut": 0.33, "btc-100k": 0.33}

Risk Parity

Inverse-volatility weighting. Uses Ledoit-Wolf shrinkage covariance if return history is provided.
# Without history (falls back to equal weight)
rp_weights = portfolio.optimize_risk_parity()

# With return history (T x N matrix)
returns_history = [
    [0.01, -0.02, 0.03],   # time step 1: 3 assets
    [0.02, -0.01, 0.04],   # time step 2
    [-0.01, 0.03, -0.02],  # time step 3
    # ... more observations
]
rp_weights = portfolio.optimize_risk_parity(returns_history)

Minimum Variance

Minimum variance allocation using Ledoit-Wolf covariance estimation.
returns_history = [
    [0.01, -0.02, 0.03],
    [0.02, -0.01, 0.04],
    [-0.01, 0.03, -0.02],
]
mv_weights = portfolio.optimize_min_variance(returns_history)

Rebalancing

needs_rebalance

Check whether any position deviates from target weights by more than a threshold.
target = {"trump-win": 0.33, "fed-cut": 0.33, "btc-100k": 0.33}
if portfolio.needs_rebalance(target, threshold=0.05):
    print("Portfolio drift exceeds 5%")

rebalance_orders

Generate the orders needed to move from current to target weights.
target = portfolio.optimize_equal_weight()
orders = portfolio.rebalance_orders(target)

for order in orders:
    print(f"{order['action']} {order['size_delta']:.1f} of {order['market_id']}")
    print(f"  Current weight: {order['current_weight']:.2%}")
    print(f"  Target weight:  {order['target_weight']:.2%}")
Each order dict contains:
KeyTypeDescription
market_idstrMarket identifier
sidestr"yes" or "no"
actionstr"buy" or "sell"
size_deltafloatSize to trade
current_weightfloatCurrent portfolio weight
target_weightfloatTarget portfolio weight

Risk Metrics

metrics

Compute portfolio risk metrics including Monte Carlo simulation.
m = portfolio.metrics(
    n_simulations=10000,  # Monte Carlo scenarios
    seed=42,              # Reproducible results
    t_hours=24.0,         # Hours to expiry for Greeks
    vol=0.2,              # Implied vol for Greeks
)
Returns a PortfolioMetrics object:
FieldTypeDescription
num_positionsintNumber of positions
total_valuefloatTotal portfolio value
total_pnlfloatTotal unrealised PnL
total_pnl_pctfloatPnL as percentage
concentrationfloatHHI index
var_95float95% Value at Risk (Monte Carlo)
cvar_95float95% Conditional VaR
win_probabilityfloatProbability of positive PnL
greeksdict[str, PredictionGreeks]Per-position Greeks (delta, gamma, theta, vega)
correlation_matrix`list[list[float]]None`Ledoit-Wolf covariance if available
shrinkage_intensityfloatShrinkage parameter
m = portfolio.metrics(seed=42)
print(f"VaR 95:   ${m.var_95:.2f}")
print(f"CVaR 95:  ${m.cvar_95:.2f}")
print(f"Win Prob: {m.win_probability:.1%}")
print(f"HHI:      {m.concentration:.3f}")

# Per-position Greeks
for market_id, g in m.greeks.items():
    print(f"{market_id}: delta={g.delta:.2f}, gamma={g.gamma:.2f}")

Full Workflow Example

import horizon as hz

# 1. Build portfolio from live engine
engine = hz.Engine(
    exchange=hz.Paper(),
    risk_config=hz.RiskConfig(max_position_per_market=500),
)
# ... run strategy, accumulate positions ...

portfolio = hz.Portfolio.from_engine(engine, capital=10000.0)

# 2. Analyze risk
metrics = portfolio.metrics(n_simulations=50000, seed=42)
print(f"Portfolio VaR 95: ${metrics.var_95:.2f}")
print(f"Win probability:  {metrics.win_probability:.1%}")

# 3. Optimize weights
kelly_targets = portfolio.optimize_kelly(
    probs={"trump-win": 0.60, "fed-cut": 0.70},
)

# 4. Check if rebalancing is needed
if portfolio.needs_rebalance(kelly_targets, threshold=0.05):
    orders = portfolio.rebalance_orders(kelly_targets)
    for o in orders:
        print(f"{o['action']} {o['size_delta']:.1f} {o['market_id']}")