Running a Backtest

hz.run(mode='backtest', ...) from end to end

The minimal call

python
import horizon as hz
from horizon.asset_classes import AssetClass, Equity
from horizon.data import SyntheticGBM
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market
from horizon.portfolio import KellyOptimizer
from horizon.quant import TSMomentum
from horizon.risk import RiskProfile

result = hz.run(
    mode="backtest",
    strategies=[TSMomentum(lookback=20)],
    asset_classes=[Equity],
    universe=StaticUniverse([
        Market(id=t, asset_class=AssetClass.Equity)
        for t in ["AAPL", "MSFT", "NVDA"]
    ]),
    portfolio=KellyOptimizer(kelly_fraction=0.25),
    risk=RiskProfile.moderate(),
    data_source=SyntheticGBM(["AAPL", "MSFT", "NVDA"], n_bars=252, seed=42),
    backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)

Required arguments

modestr
Must be `"backtest"` for backtest mode.
strategieslist
List of `Strategy` subclasses or instances. Multiple strategies run simultaneously.
asset_classeslist[AssetClass]
Which asset classes the backtest covers. Usually auto-inferred from strategies.
universeDiscoveryProvider
The market universe. Use `StaticUniverse([Market(...), ...])` for a fixed list.
data_sourceDataSource
Historical data source. See [Data Sources](/docs/backtest/data-sources).

Optional arguments

venuesdict[str, Venue]
Dict of venue name → Venue instance. Defaults to a `Paper` venue with `initial_cash_usd` from the backtest config.
portfolioSizer
Portfolio sizer. Defaults to `EqualWeight()`.
riskRiskConfig
Risk configuration. Defaults to `RiskConfig()` (empty: no stops, no drawdown guards).
lifecycleLifecycleConfig
Lifecycle config (rebalance schedule, option roll DTE, etc.).
backtestBacktestConfig
Backtest-specific configuration. See [BacktestConfig](/docs/backtest/config).
executorsdict[AssetClass, Executor]
Override the default executor per asset class. Default is `EquityExecutor` for equities.
metricsMetricsConfig | bool
Metrics collection config. `True` enables defaults.
dashboardbool
Start a TUI dashboard (not yet wired in backtest mode).
paramsdict
Free-form params passed into `Context.params` for strategies to read.

The return value

python
result: BacktestResult

Fields:

python
@dataclass
class BacktestResult:
    sharpe: float
    sortino: float
    total_return: float
    max_drawdown: float
    n_trades: int
    equity_curve: list[tuple[datetime, float]]
    trades: list[TradeRecord]
    signals_log: Any
    metrics_path: str

Inspecting results

python
# Summary
print(f"Sharpe:       {result.sharpe:+.3f}")
print(f"Sortino:      {result.sortino:+.3f}")
print(f"Total return: {result.total_return:+.2%}")
print(f"Max drawdown: {result.max_drawdown:.2%}")
print(f"Trades:       {result.n_trades}")

# Equity curve
for ts, equity in result.equity_curve[:5]:
    print(ts, equity)
print("...")
for ts, equity in result.equity_curve[-5:]:
    print(ts, equity)

# Per-trade detail
for trade in result.trades[:10]:
    print(
        trade.market_id,
        trade.side,
        trade.quantity,
        trade.price,
        f"fee={trade.fee:.2f}",
        f"P&L={trade.realized_pnl:+.2f}",
        trade.strategy_id,
    )

Saving results

python
import json

# Trade log as CSV
import csv
with open("trades.csv", "w") as fh:
    writer = csv.DictWriter(fh, fieldnames=[
        "timestamp", "market_id", "side", "quantity", "price",
        "fee", "realized_pnl", "strategy_id",
    ])
    writer.writeheader()
    for t in result.trades:
        writer.writerow({
            "timestamp": t.timestamp.isoformat(),
            "market_id": t.market_id,
            "side": t.side,
            "quantity": t.quantity,
            "price": t.price,
            "fee": t.fee,
            "realized_pnl": t.realized_pnl,
            "strategy_id": t.strategy_id,
        })

# Equity curve as CSV
with open("equity.csv", "w") as fh:
    writer = csv.writer(fh)
    writer.writerow(["timestamp", "equity"])
    for ts, eq in result.equity_curve:
        writer.writerow([ts.isoformat(), eq])

# Summary as JSON
summary = {
    "sharpe": result.sharpe,
    "sortino": result.sortino,
    "total_return": result.total_return,
    "max_drawdown": result.max_drawdown,
    "n_trades": result.n_trades,
}
with open("summary.json", "w") as fh:
    json.dump(summary, fh, indent=2)

Plotting

With horizon[research]:

python
import plotly.graph_objects as go

ts = [t for t, _ in result.equity_curve]
eq = [e for _, e in result.equity_curve]

fig = go.Figure()
fig.add_trace(go.Scatter(x=ts, y=eq, mode="lines", name="Equity"))
fig.update_layout(
    title="Backtest equity curve",
    xaxis_title="Date",
    yaxis_title="USD",
)
fig.show()

Next