Skip to main content
Pro Feature. Requires a Pro or Ultra subscription. Get started at api.mathematicalcompany.com

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.

Strategy Book

Manage multiple engines with isolated risk budgets, rolling Sharpe, and drawdown tracking.

Correlation Monitoring

Pairwise Pearson correlation of PnL changes across strategies.

Capital Rebalancing

Equal, risk-parity, and performance-based budget allocation.

Position Reconciliation

Compare engine vs exchange positions and flag breaks by severity.

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.
from horizon.multi_strategy import StrategyBook

Quick Start

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

book = StrategyBook(total_capital=100_000.0)
ParameterTypeDefaultDescription
total_capitalfloat100000.0Total capital budget across all strategies.

add_strategy

Add a strategy to the book.
book.add_strategy(
    name="momentum",
    engine=engine_a,
    risk_budget=0.4,  # 40% of total capital
)
ParameterTypeDefaultDescription
namestrrequiredUnique strategy name.
engineEnginerequiredReference to the strategy’s Engine instance.
risk_budgetfloat or NoneNoneFraction of total capital to allocate (0.0 to 1.0). If None, budgets are redistributed equally across all strategies.
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.

remove_strategy

Remove a strategy from the book. Its PnL history and equity curve are also discarded.
book.remove_strategy("momentum")
ParameterTypeDefaultDescription
namestrrequiredStrategy 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.
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.
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

FieldTypeDescription
strategieslist[StrategySlot]Snapshot of each strategy’s current state.
total_pnlfloatSum of all strategy PnLs.
total_drawdownfloatPortfolio-level drawdown (fraction of peak combined equity).
portfolio_sharpefloatSharpe ratio of the combined equity curve.
correlationslist[StrategyCorrelation]Pairwise PnL correlations between active strategies.
capital_utilizationfloatFraction of total capital allocated to active strategies.

StrategySlot

FieldTypeDescription
namestrUnique identifier for this strategy.
engineEngineReference to the strategy’s Engine instance.
risk_budgetfloatFraction of total capital allocated (0.0 to 1.0).
pnlfloatCurrent unrealized + realized PnL.
sharpefloatRolling Sharpe ratio.
drawdownfloatCurrent drawdown as fraction of peak equity.
is_activeboolWhether the strategy is currently active.

StrategyCorrelation

FieldTypeDescription
strategy_astrName of the first strategy.
strategy_bstrName of the second strategy.
correlationfloatPearson correlation of PnL changes.
n_observationsintNumber of overlapping observations used.

rebalance_capital

Compute target risk budgets for each active strategy.
# 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")
ParameterTypeDefaultDescription
methodstr"equal"Rebalancing method (see table below).

Rebalancing Methods

MethodDescription
"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.
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")
ParameterTypeDefaultDescription
max_drawdownfloat0.1Maximum allowed drawdown (e.g., 0.1 = 10%).
min_sharpefloat0.0Minimum acceptable Sharpe ratio. Only checked when 10+ PnL observations exist.

Returns

list[str] of strategy names that need attention.
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.

strategy_pipeline

Return a pipeline function that records metrics for the named strategy inside hz.run().
hz.run(
    pipeline=[
        my_model,
        book.strategy_pipeline("momentum"),
        my_quoter,
    ],
    ...
)
ParameterTypeDefaultDescription
strategy_namestrrequiredName of the strategy (must already be in the book).

Injected into ctx.params

KeyTypeDescription
"strategy_pnl"floatCurrent PnL for this strategy.
"strategy_drawdown"floatCurrent drawdown fraction.
"strategy_sharpe"floatRolling 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.
from horizon.reconciliation import reconcile, auto_reconcile, PositionBreak, ReconciliationReport

How Reconciliation Works

1

Snapshot engine positions

Queries engine.positions() and aggregates by market_id (summing sizes for markets with multiple positions).
2

Snapshot exchange positions

Uses the provided exchange_positions list or attempts to fetch from engine.exchange_positions().
3

Compare both sides

For every market across both snapshots, checks for: missing positions, side mismatches, and size mismatches.
4

Assign severity

Each break gets a severity level based on the type and magnitude of the discrepancy.

reconcile

Compare engine positions vs exchange and return all detected breaks.
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

ParameterTypeDefaultDescription
engineEnginerequiredHorizon Engine instance.
exchange_positionslist[dict] or NoneNoneList of position dicts from the exchange. If None, tries engine.exchange_positions() or returns an engine-only report.
Each exchange position dict should have:
KeyTypeDescription
"market_id"strMarket identifier.
"size"floatPosition size.
"side"str"yes" or "no".

Returns

ReconciliationReport with all detected breaks.

Break Severity Levels

Break TypeConditionSeverity
"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)
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.

auto_reconcile

Pipeline function for hz.run() that runs reconciliation at a configurable interval.
hz.run(
    pipeline=[
        auto_reconcile(engine, interval=300.0),  # Every 5 minutes
        my_model,
        my_quoter,
    ],
    ...
)

Parameters

ParameterTypeDefaultDescription
engineEngine or NoneNoneOptional engine override. If None, uses ctx.params["engine"].
intervalfloat300.0Seconds between reconciliation runs.

Injected into ctx.params

KeyTypeDescription
"recon_clean"boolTrue if no breaks were found in the last reconciliation run.
"recon_breaks"intNumber of breaks found in the last reconciliation run.
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.

Type Reference

PositionBreak

FieldTypeDescription
market_idstrMarket identifier where the break was detected.
break_typestrOne of: "missing_engine", "missing_exchange", "size_mismatch", "side_mismatch".
engine_sizefloat or NonePosition size in the engine (None if missing from engine).
exchange_sizefloat or NonePosition size on the exchange (None if missing from exchange).
engine_sidestr or NoneSide in the engine ("yes" or "no").
exchange_sidestr or NoneSide on the exchange ("yes" or "no").
severitystr"critical", "warning", or "info".

ReconciliationReport

FieldTypeDescription
breakslist[PositionBreak]All detected discrepancies.
matchedintNumber of positions that matched within tolerance.
total_engineintTotal positions in the engine.
total_exchangeintTotal positions on the exchange.
is_cleanboolTrue if no breaks were found.
timestampfloatUnix timestamp of the reconciliation run.

Full Multi-Strategy Workflow

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!")