The Problem

Evaluating options‑and‑equity portfolio strategies over historical data demands a reproducible pipeline that handles realistic execution, Greeks‑aware risk, and tail‑risk hedging. The repo supplies those capabilities, but its module graph contains circular imports, oversized files, and deep nesting that make changes risky and hard to reason about.

What This Does

The package is a full‑stack backtester built around a Rust compute core (rust/ob_core) and Python orchestration (options_portfolio_backtester).

  • Data ingestionoptions_portfolio_backtester/data/providers.py supplies HistoricalOptionsData and TiingoData; raw parquet/CSV is fetched with scripts/fetch_data.py.
  • Strategy constructionoptions_portfolio_backtester/strategy/strategy.py and strategy_leg.py let legs be added, filtered, and cleared; the canonical Spitznagel tail‑hedge leg is built with deep_otm_put.
  • Engine executionoptions_portfolio_backtester/engine/engine.py (BacktestEngine) coordinates rebalancing, cost models (execution/cost_model.py), and risk checks (portfolio/risk.py).
  • Analysis & reportinganalytics/tearsheet.py and analytics/charts.py produce HTML reports, equity curves, and options‑specific panels from engine.balance and engine.trade_log.

A single run call (entry point options_portfolio_backtester/engine/engine.py:345) drives the backtest from data load through trade execution to results, reaching 28 internal functions.

How It Is Wired

  • Entry pointssetup (test‑only, reaches 47 functions, called from nothing else), main in research/spitznagel_spy/experiments/cross_underlying.py:159 (reaches 148 functions, invoked once), and run in engine.py:345 (reaches 28 functions, called once).
  • Call graph – 3 239 resolved edges; the most‑connected symbols are _ctx (116 callers), from_balance (105), and BacktestEngine (70).
  • Hubs & cyclesoptions_portfolio_backtester/core/types is imported by 59 modules (instability 0); options_portfolio_backtester/engine/engine.py and options_portfolio_backtester/__init__.py form a circular‑import cycle.
  • Blast‑radius filesengine/engine.py (864 loc, 3 classes, 32 functions) and engine/pipeline.py (106 functions) are change‑propagation points; a modification ripples widely.
  • Outside‑process effects – execution paths can write CSV (to_csv), run external commands, make a single outbound network call (traced from main → to_csv), and perform a cryptographic operation.

How To Use It

Setup

# With Nix
nix develop

# Without Nix (Python ≥ 3.12)
python -m venv .venv && source .venv/bin/activate
make install-dev   # installs from pyproject.toml

Get data

python scripts/fetch_data.py all --symbols SPY
# pulls SPY options & underlying parquet into data/processed/

Run a first backtest (example from the README)

from options_portfolio_backtester import (
    BacktestEngine, Stock,
    HistoricalOptionsData, TiingoData,
    deep_otm_put,
)

options_data = HistoricalOptionsData("data/processed/options.parquet")
stocks_data  = TiingoData("data/processed/stocks.csv")

strategy = deep_otm_put(options_data.schema, "SPY")

engine = BacktestEngine({"stocks": 1.0, "options": 0.0, "cash": 0.0},
                        initial_capital=1_000_000)
engine.use_external_budget(annual_pct=0.005)
engine.stocks = [Stock("SPY", 1.0)]
engine.stocks_data = stocks_data
engine.options_data = options_data
engine.options_strategy = strategy
engine.run(rebalance_freq=1, rebalance_unit="BMS")

results = engine.get_results()
print(results.summary())
# {'annual_return': 13.5, 'max_drawdown': -46.4, 'sharpe': 0.72, 'trades': 327, ...}

Generate a tearsheet

from options_portfolio_backtester.analytics.tearsheet import build_tearsheet

report = build_tearsheet(
    engine.balance,
    budget_annual_pct=0.033,
    trade_log=engine.trade_log,
)
report.to_file("tearsheet.html")

Real‑World Use

A portfolio manager seeking a tail‑hedge can instantiate the Spitznagel leg (deep_otm_put) and allocate a small options budget (e.g., engine.use_allocation(stocks=0.99, options=0.01, cash=0.0)). The backtest then simulates monthly rebalancing, tracks premium spend against the budget, and produces a crash‑window P&L attribution—all within the same framework used for equity‑only sweeps.

Code Health & Issues

Measured analysis (static, 171 files): 66 issues across 7 categories

  • High/cognitive_load – deep nesting in engine/engine.py, strategy/strategy.py, portfolio/risk.py (max indent 8).
  • High/soundness – import‑cycle member in engine/engine.py & __init__.py.
  • High/clarity – hub module core/types (59 importers) and duplicated 6‑line blocks (173 repeats across 53 files).
  • Medium/resilience – broad except in engine/engine.py, analytics/results.py, analytics/tearsheet.py.
  • Medium/resource_safety – file opened without context manager in tests/engine/test_rust_parity.py.

Code‑health audit (8 findings, 0 critical):

  • HIGH – Pin GitHub Actions to commit SHA (.github/workflows, dtolnay/rust-toolchain@stable, Swatinem/rust-cache@v2).
  • HIGH – Commit lockfile beside pyproject.toml.
  • HIGH – Remove continue-on-error from correctness‑gating steps (.github/workflows/ci.yml:40).
  • MEDIUM – Declare least‑privilege permissions: contents: read for GITHUB_TOKEN.
  • MEDIUM – Enable Dependabot or Renovate (cover Python, Rust, GitHub‑Actions).
  • MEDIUM – Gate PRs on dependency vulnerability scan (add dependency-review-action).
  • MEDIUM – Set persist-credentials: false on checkout.
  • LOW – Add timeout-minutes to workflow jobs.

The Bottom Line

This repo delivers a capable, reproducible options‑backtesting platform with a Rust core and rich analytics, but its module interdependencies, oversized files, and circular imports raise maintenance risk. Teams that need fast strategy sweeps and tail‑risk hedging will find value here, provided they invest in cleaning the identified cycles and pinning CI dependencies.