---
title: "Multi-Strategy Operations"
description: "Run multiple strategies with isolated risk budgets, position reconciliation, and capital rebalancing."
icon: "layer-group"
---

<Note>
  **Pro Feature.** Requires a Pro or Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)
</Note>

# Multi-Strategy Operations

Run a portfolio of independent strategies, each with its own risk budget and Engine instance. The `StrategyBook` provides aggregate analytics, pairwise correlation tracking, rebalancing recommendations, and intervention alerts. The `reconciliation` module detects discrepancies between engine state and exchange positions.

<CardGroup cols={2}>
  <Card title="Strategy Book" icon="book">
    Manage multiple engines with isolated risk budgets, rolling Sharpe, and drawdown tracking.
  </Card>
  <Card title="Correlation Monitoring" icon="chart-line">
    Pairwise Pearson correlation of PnL changes across strategies.
  </Card>
  <Card title="Capital Rebalancing" icon="scale-balanced">
    Equal, risk-parity, and performance-based budget allocation.
  </Card>
  <Card title="Position Reconciliation" icon="magnifying-glass">
    Compare engine vs exchange positions and flag breaks by severity.
  </Card>
</CardGroup>

---

## Multi-Strategy Book

The `StrategyBook` holds references to multiple Engine instances and provides oversight, analytics, and rebalancing. It does not own engines. Each strategy is responsible for its own execution.

```python
from horizon.multi_strategy import StrategyBook
```

### Quick Start

```python
book = StrategyBook(total_capital=100_000)
book.add_strategy("momentum", engine_a, risk_budget=0.4)
book.add_strategy("mean_revert", engine_b, risk_budget=0.6)

# Each cycle:
book.update()
report = book.report()
print(f"Portfolio Sharpe: {report.portfolio_sharpe:.2f}")
print(f"Total PnL: {report.total_pnl:+.2f}")

# Check for problems:
troubled = book.needs_intervention(max_drawdown=0.10)
if troubled:
    print(f"Strategies needing attention: {troubled}")

# Rebalance:
new_budgets = book.rebalance_capital(method="risk_parity")
for name, budget in new_budgets.items():
    print(f"  {name}: {budget:.1%}")
```

### Constructor

```python
book = StrategyBook(total_capital=100_000.0)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `total_capital` | `float` | `100000.0` | Total capital budget across all strategies. |

---

### add_strategy

Add a strategy to the book.

```python
book.add_strategy(
    name="momentum",
    engine=engine_a,
    risk_budget=0.4,  # 40% of total capital
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | `str` | *required* | Unique strategy name. |
| `engine` | `Engine` | *required* | Reference to the strategy's Engine instance. |
| `risk_budget` | `float or None` | `None` | Fraction of total capital to allocate (0.0 to 1.0). If `None`, budgets are redistributed equally across all strategies. |

<Note>
If `risk_budget` is `None`, all existing strategies' budgets are redistributed equally (including the new one). If a specific budget is provided, the total across all strategies must not exceed 1.0.
</Note>

### remove_strategy

Remove a strategy from the book. Its PnL history and equity curve are also discarded.

```python
book.remove_strategy("momentum")
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | `str` | *required* | Strategy name to remove. |

### update

Record PnL snapshots for each active strategy. Call this once per cycle to build up the equity curves used for correlation and Sharpe computation.

```python
book.update()
```

Queries each strategy's engine via `engine.status()` to read current `total_pnl`. Updates rolling Sharpe, drawdown, and the combined portfolio equity curve.

### report

Generate a multi-strategy report with per-strategy metrics, pairwise PnL correlations, and aggregate portfolio-level analytics.

```python
report = book.report()

# Per-strategy metrics
for s in report.strategies:
    print(f"{s.name}: PnL={s.pnl:+.2f}, Sharpe={s.sharpe:.2f}, "
          f"DD={s.drawdown:.1%}, budget={s.risk_budget:.1%}")

# Pairwise correlations
for c in report.correlations:
    print(f"{c.strategy_a} vs {c.strategy_b}: "
          f"corr={c.correlation:.3f} (n={c.n_observations})")

# Aggregate
print(f"Portfolio Sharpe: {report.portfolio_sharpe:.2f}")
print(f"Total Drawdown: {report.total_drawdown:.1%}")
print(f"Capital Utilization: {report.capital_utilization:.1%}")
```

#### MultiStrategyReport

| Field | Type | Description |
|-------|------|-------------|
| `strategies` | `list[StrategySlot]` | Snapshot of each strategy's current state. |
| `total_pnl` | `float` | Sum of all strategy PnLs. |
| `total_drawdown` | `float` | Portfolio-level drawdown (fraction of peak combined equity). |
| `portfolio_sharpe` | `float` | Sharpe ratio of the combined equity curve. |
| `correlations` | `list[StrategyCorrelation]` | Pairwise PnL correlations between active strategies. |
| `capital_utilization` | `float` | Fraction of total capital allocated to active strategies. |

#### StrategySlot

| Field | Type | Description |
|-------|------|-------------|
| `name` | `str` | Unique identifier for this strategy. |
| `engine` | `Engine` | Reference to the strategy's Engine instance. |
| `risk_budget` | `float` | Fraction of total capital allocated (0.0 to 1.0). |
| `pnl` | `float` | Current unrealized + realized PnL. |
| `sharpe` | `float` | Rolling Sharpe ratio. |
| `drawdown` | `float` | Current drawdown as fraction of peak equity. |
| `is_active` | `bool` | Whether the strategy is currently active. |

#### StrategyCorrelation

| Field | Type | Description |
|-------|------|-------------|
| `strategy_a` | `str` | Name of the first strategy. |
| `strategy_b` | `str` | Name of the second strategy. |
| `correlation` | `float` | Pearson correlation of PnL changes. |
| `n_observations` | `int` | Number of overlapping observations used. |

### rebalance_capital

Compute target risk budgets for each active strategy.

```python
# Equal allocation
budgets = book.rebalance_capital(method="equal")

# Inverse-volatility: lower vol gets more capital
budgets = book.rebalance_capital(method="risk_parity")

# Proportional to Sharpe: higher Sharpe gets more capital
budgets = book.rebalance_capital(method="performance")
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `method` | `str` | `"equal"` | Rebalancing method (see table below). |

#### Rebalancing Methods

| Method | Description |
|--------|-------------|
| `"equal"` | Equal allocation across active strategies. |
| `"risk_parity"` | Allocate inversely to PnL volatility. Lower vol strategies get more capital. Falls back to equal weight if insufficient history. |
| `"performance"` | Allocate proportionally to Sharpe ratio. Uses a shifted softmax to handle negative Sharpe values. |

#### Returns

`dict[str, float]` mapping strategy name to target risk budget (summing to 1.0 across active strategies).

### needs_intervention

Return names of strategies that breach risk thresholds.

```python
troubled = book.needs_intervention(
    max_drawdown=0.10,  # Flag if drawdown > 10%
    min_sharpe=0.0,     # Flag if Sharpe < 0 (with enough history)
)

for name in troubled:
    print(f"Strategy '{name}' needs attention")
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `max_drawdown` | `float` | `0.1` | Maximum allowed drawdown (e.g., 0.1 = 10%). |
| `min_sharpe` | `float` | `0.0` | Minimum acceptable Sharpe ratio. Only checked when 10+ PnL observations exist. |

#### Returns

`list[str]` of strategy names that need attention.

<Warning>
A strategy is flagged if its drawdown exceeds `max_drawdown` OR its Sharpe falls below `min_sharpe` (once enough history has accumulated). Both conditions are checked independently.
</Warning>

### strategy_pipeline

Return a pipeline function that records metrics for the named strategy inside `hz.run()`.

```python
hz.run(
    pipeline=[
        my_model,
        book.strategy_pipeline("momentum"),
        my_quoter,
    ],
    ...
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `strategy_name` | `str` | *required* | Name of the strategy (must already be in the book). |

#### Injected into ctx.params

| Key | Type | Description |
|-----|------|-------------|
| `"strategy_pnl"` | `float` | Current PnL for this strategy. |
| `"strategy_drawdown"` | `float` | Current drawdown fraction. |
| `"strategy_sharpe"` | `float` | Rolling Sharpe ratio. |

---

## Position Reconciliation

Detect discrepancies between the engine's internal position tracking and the exchange's reported positions. Flags missing positions, size mismatches, and side mismatches with severity levels.

```python
from horizon.reconciliation import reconcile, auto_reconcile, PositionBreak, ReconciliationReport
```

### How Reconciliation Works

<Steps>
  <Step title="Snapshot engine positions">
    Queries `engine.positions()` and aggregates by `market_id` (summing sizes for markets with multiple positions).
  </Step>
  <Step title="Snapshot exchange positions">
    Uses the provided `exchange_positions` list or attempts to fetch from `engine.exchange_positions()`.
  </Step>
  <Step title="Compare both sides">
    For every market across both snapshots, checks for: missing positions, side mismatches, and size mismatches.
  </Step>
  <Step title="Assign severity">
    Each break gets a severity level based on the type and magnitude of the discrepancy.
  </Step>
</Steps>

### reconcile

Compare engine positions vs exchange and return all detected breaks.

```python
report = reconcile(
    engine=engine,
    exchange_positions=[
        {"market_id": "btc-100k", "size": 10.0, "side": "yes"},
        {"market_id": "eth-5k", "size": 5.0, "side": "no"},
    ],
)

if not report.is_clean:
    for brk in report.breaks:
        print(f"[{brk.severity.upper()}] {brk.market_id}: {brk.break_type}")
        print(f"  Engine: size={brk.engine_size}, side={brk.engine_side}")
        print(f"  Exchange: size={brk.exchange_size}, side={brk.exchange_side}")
else:
    print(f"All clean: {report.matched} positions matched")
```

#### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `engine` | `Engine` | *required* | Horizon Engine instance. |
| `exchange_positions` | `list[dict] or None` | `None` | List of position dicts from the exchange. If `None`, tries `engine.exchange_positions()` or returns an engine-only report. |

Each exchange position dict should have:

| Key | Type | Description |
|-----|------|-------------|
| `"market_id"` | `str` | Market identifier. |
| `"size"` | `float` | Position size. |
| `"side"` | `str` | `"yes"` or `"no"`. |

#### Returns

`ReconciliationReport` with all detected breaks.

### Break Severity Levels

| Break Type | Condition | Severity |
|------------|-----------|----------|
| `"missing_exchange"` | Position in engine but not on exchange | `"critical"` |
| `"missing_engine"` | Position on exchange but not in engine | `"critical"` |
| `"side_mismatch"` | Engine says YES, exchange says NO (or vice versa) | `"critical"` |
| `"size_mismatch"` | Size difference greater than 10% | `"critical"` |
| `"size_mismatch"` | Size difference between 1% and 10% | `"warning"` |
| (matched) | Size difference within 1%, same side | (no break) |

<Warning>
Side mismatches are always critical. A side mismatch means your engine thinks you are long but the exchange shows short (or vice versa). This requires immediate attention.
</Warning>

### auto_reconcile

Pipeline function for `hz.run()` that runs reconciliation at a configurable interval.

```python
hz.run(
    pipeline=[
        auto_reconcile(engine, interval=300.0),  # Every 5 minutes
        my_model,
        my_quoter,
    ],
    ...
)
```

#### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `engine` | `Engine or None` | `None` | Optional engine override. If `None`, uses `ctx.params["engine"]`. |
| `interval` | `float` | `300.0` | Seconds between reconciliation runs. |

#### Injected into ctx.params

| Key | Type | Description |
|-----|------|-------------|
| `"recon_clean"` | `bool` | `True` if no breaks were found in the last reconciliation run. |
| `"recon_breaks"` | `int` | Number of breaks found in the last reconciliation run. |

<Note>
Between reconciliation runs, the pipeline injects the results from the most recent run. The first run happens on the first cycle; subsequent runs occur every `interval` seconds. Critical breaks are logged at WARNING level; info-level breaks are logged at INFO.
</Note>

### Type Reference

#### PositionBreak

| Field | Type | Description |
|-------|------|-------------|
| `market_id` | `str` | Market identifier where the break was detected. |
| `break_type` | `str` | One of: `"missing_engine"`, `"missing_exchange"`, `"size_mismatch"`, `"side_mismatch"`. |
| `engine_size` | `float or None` | Position size in the engine (None if missing from engine). |
| `exchange_size` | `float or None` | Position size on the exchange (None if missing from exchange). |
| `engine_side` | `str or None` | Side in the engine (`"yes"` or `"no"`). |
| `exchange_side` | `str or None` | Side on the exchange (`"yes"` or `"no"`). |
| `severity` | `str` | `"critical"`, `"warning"`, or `"info"`. |

#### ReconciliationReport

| Field | Type | Description |
|-------|------|-------------|
| `breaks` | `list[PositionBreak]` | All detected discrepancies. |
| `matched` | `int` | Number of positions that matched within tolerance. |
| `total_engine` | `int` | Total positions in the engine. |
| `total_exchange` | `int` | Total positions on the exchange. |
| `is_clean` | `bool` | `True` if no breaks were found. |
| `timestamp` | `float` | Unix timestamp of the reconciliation run. |

---

## Full Multi-Strategy Workflow

```python
import horizon as hz
from horizon.multi_strategy import StrategyBook
from horizon.reconciliation import reconcile, auto_reconcile

# 1. Set up engines
engine_a = hz.Engine(exchange=hz.Paper(), risk_config=hz.RiskConfig(...))
engine_b = hz.Engine(exchange=hz.Paper(), risk_config=hz.RiskConfig(...))

# 2. Build the strategy book
book = StrategyBook(total_capital=100_000)
book.add_strategy("momentum", engine_a, risk_budget=0.4)
book.add_strategy("mean_revert", engine_b, risk_budget=0.6)

# 3. Run strategies with pipeline integration and auto-reconciliation
hz.run(
    name="momentum",
    engine=engine_a,
    pipeline=[
        auto_reconcile(engine_a, interval=300.0),
        book.strategy_pipeline("momentum"),
        momentum_model,
        quoter,
    ],
    ...
)

# 4. Periodic oversight (e.g., in a monitoring loop)
book.update()
report = book.report()

# Check correlations -- high correlation reduces diversification benefit
for c in report.correlations:
    if c.correlation > 0.8:
        print(f"High correlation: {c.strategy_a} <-> {c.strategy_b} "
              f"({c.correlation:.2f})")

# Check for struggling strategies
troubled = book.needs_intervention(max_drawdown=0.10, min_sharpe=-0.5)
if troubled:
    print(f"Strategies needing review: {troubled}")

    # Rebalance away from struggling strategies
    new_budgets = book.rebalance_capital(method="risk_parity")
    print(f"Suggested budgets: {new_budgets}")

# 5. Reconcile against exchange
recon = reconcile(engine_a, exchange_positions=[...])
if not recon.is_clean:
    print(f"{len(recon.breaks)} breaks detected!")
```
