> ## Documentation Index
> Fetch the complete documentation index at: https://mathematicalcompany.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-Strategy Operations

> Run multiple strategies with isolated risk budgets, position reconciliation, and capital rebalancing.

<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 theme={null}
from horizon.multi_strategy import StrategyBook
```

### Quick Start

```python theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
# 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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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!")
```
