master 09514782c168 cached
181 files
1.5 MB
507.2k tokens
2830 symbols
1 requests
Download .txt
Showing preview only (1,557K chars total). Download the full file or copy to clipboard to get everything.
Repository: lambdaclass/options_backtester
Branch: master
Commit: 09514782c168
Files: 181
Total size: 1.5 MB

Directory structure:
gitextract_dxl517zn/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .python-version
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── benchmarks/
│   ├── benchmark_large_pipeline.py
│   ├── benchmark_matrix.py
│   ├── benchmark_rust_vs_python.py
│   ├── benchmark_sweep.py
│   └── compare_with_bt.py
├── data/
│   ├── README.md
│   ├── convert_optionsdx.py
│   ├── fetch_data.py
│   └── fetch_signals.py
├── flake.nix
├── options_portfolio_backtester/
│   ├── __init__.py
│   ├── analytics/
│   │   ├── __init__.py
│   │   ├── charts.py
│   │   ├── optimization.py
│   │   ├── stats.py
│   │   ├── summary.py
│   │   ├── tearsheet.py
│   │   └── trade_log.py
│   ├── convexity/
│   │   ├── __init__.py
│   │   ├── _utils.py
│   │   ├── allocator.py
│   │   ├── backtest.py
│   │   ├── config.py
│   │   ├── scoring.py
│   │   └── viz.py
│   ├── core/
│   │   ├── __init__.py
│   │   └── types.py
│   ├── data/
│   │   ├── __init__.py
│   │   ├── providers.py
│   │   └── schema.py
│   ├── engine/
│   │   ├── __init__.py
│   │   ├── algo_adapters.py
│   │   ├── clock.py
│   │   ├── engine.py
│   │   ├── multi_strategy.py
│   │   ├── pipeline.py
│   │   └── strategy_tree.py
│   ├── execution/
│   │   ├── __init__.py
│   │   ├── _rust_bridge.py
│   │   ├── cost_model.py
│   │   ├── fill_model.py
│   │   ├── signal_selector.py
│   │   └── sizer.py
│   ├── portfolio/
│   │   ├── __init__.py
│   │   ├── greeks.py
│   │   ├── portfolio.py
│   │   ├── position.py
│   │   └── risk.py
│   └── strategy/
│       ├── __init__.py
│       ├── presets.py
│       ├── strategy.py
│       └── strategy_leg.py
├── pyproject.toml
├── rust/
│   ├── .cargo/
│   │   └── config.toml
│   ├── Cargo.toml
│   ├── ob_core/
│   │   ├── Cargo.toml
│   │   ├── benches/
│   │   │   └── hot_paths.rs
│   │   └── src/
│   │       ├── backtest.rs
│   │       ├── balance.rs
│   │       ├── convexity_backtest.rs
│   │       ├── convexity_scoring.rs
│   │       ├── cost_model.rs
│   │       ├── entries.rs
│   │       ├── exits.rs
│   │       ├── fill_model.rs
│   │       ├── filter.rs
│   │       ├── inventory.rs
│   │       ├── lib.rs
│   │       ├── risk.rs
│   │       ├── signal_selector.rs
│   │       ├── stats.rs
│   │       └── types.rs
│   └── ob_python/
│       ├── Cargo.toml
│       └── src/
│           ├── arrow_bridge.rs
│           ├── lib.rs
│           ├── py_backtest.rs
│           ├── py_balance.rs
│           ├── py_convexity.rs
│           ├── py_entries.rs
│           ├── py_execution.rs
│           ├── py_exits.rs
│           ├── py_filter.rs
│           ├── py_stats.rs
│           └── py_sweep.rs
├── setup.cfg
└── tests/
    ├── __init__.py
    ├── analytics/
    │   ├── __init__.py
    │   ├── test_analytics_pbt.py
    │   ├── test_charts.py
    │   ├── test_optimization.py
    │   ├── test_stats.py
    │   ├── test_stats_python_path.py
    │   ├── test_summary.py
    │   ├── test_tearsheet.py
    │   └── test_trade_log.py
    ├── bench/
    │   ├── __init__.py
    │   ├── _test_helpers.py
    │   ├── extract_prod_slices.py
    │   ├── generate_test_data.py
    │   ├── test_edge_cases.py
    │   ├── test_execution_models.py
    │   ├── test_invariants.py
    │   ├── test_multi_leg.py
    │   ├── test_partial_exits.py
    │   └── test_sweep.py
    ├── compat/
    │   ├── __init__.py
    │   └── test_bt_overlap_gate.py
    ├── conftest.py
    ├── convexity/
    │   ├── __init__.py
    │   ├── conftest.py
    │   ├── test_allocator.py
    │   ├── test_backtest.py
    │   └── test_config.py
    ├── core/
    │   ├── __init__.py
    │   ├── test_types.py
    │   └── test_types_pbt.py
    ├── data/
    │   ├── __init__.py
    │   ├── test_filter.py
    │   ├── test_property_based.py
    │   ├── test_providers.py
    │   ├── test_providers_extended.py
    │   └── test_schema.py
    ├── engine/
    │   ├── __init__.py
    │   ├── test_algo_adapters.py
    │   ├── test_capital_conservation.py
    │   ├── test_chaos.py
    │   ├── test_clock.py
    │   ├── test_engine.py
    │   ├── test_engine_deep.py
    │   ├── test_engine_unit.py
    │   ├── test_full_liquidation.py
    │   ├── test_max_notional.py
    │   ├── test_multi_strategy.py
    │   ├── test_multi_strategy_engine.py
    │   ├── test_per_leg_overrides.py
    │   ├── test_pipeline.py
    │   ├── test_portfolio_integration.py
    │   ├── test_regression_snapshots.py
    │   ├── test_risk_wiring.py
    │   ├── test_rust_parity.py
    │   ├── test_signal_selector_wiring.py
    │   └── test_strategy_tree.py
    ├── execution/
    │   ├── __init__.py
    │   ├── test_cost_model.py
    │   ├── test_execution_deep.py
    │   ├── test_execution_pbt.py
    │   ├── test_fill_model.py
    │   ├── test_rust_parity_execution.py
    │   ├── test_signal_selector.py
    │   └── test_sizer.py
    ├── portfolio/
    │   ├── __init__.py
    │   ├── test_greeks_aggregation.py
    │   ├── test_portfolio.py
    │   ├── test_position.py
    │   ├── test_property_based.py
    │   └── test_risk.py
    ├── strategy/
    │   ├── __init__.py
    │   ├── test_presets.py
    │   ├── test_strangle.py
    │   ├── test_strategy.py
    │   ├── test_strategy_deep.py
    │   ├── test_strategy_leg.py
    │   └── test_strategy_pbt.py
    ├── test_cleanup.py
    ├── test_data/
    │   ├── ivy_5assets_data.csv
    │   ├── ivy_portfolio.csv
    │   ├── options_data.csv
    │   ├── test_data_options.csv
    │   └── test_data_stocks.csv
    ├── test_deep_analytics_convexity.py
    ├── test_intrinsic_sign.py
    ├── test_intrinsic_value.py
    ├── test_property_based.py
    └── test_smoke.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: cachix/install-nix-action@v30
      - name: Download data
        run: nix develop --command python data/fetch_data.py all --symbols SPY --force
      - name: Run tests
        run: nix develop --command python -m pytest -v tests/ --ignore=tests/bench --ignore=tests/compat
      - name: Type check
        run: nix develop --command python -m mypy options_portfolio_backtester --ignore-missing-imports


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
.venv-bt/
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

# Generated charts/plots
*.png

# Mac OS-specific storage files
.DS_Store

# VS Code
.vscode/
_ob_rust.so
mutants/

# Large regenerable test data (from tests/bench/extract_prod_slices.py)
tests/data/*.csv

# exclude data from source control by default
data/*
!data/README.md
!data/fetch_data.py
!data/fetch_signals.py
!data/convert_optionsdx.py
!data/raw/
data/raw/*
!data/raw/.gitkeep
!data/processed/
data/processed/*
!data/processed/.gitkeep
!data/processed/signals.csv


================================================
FILE: .python-version
================================================
3.12



================================================
FILE: CONTRIBUTING.md
================================================
Contributing
============

Contributions are welcome and very much appreciated. Credit will be appropriately given.

## License

By contributing, you agree that your contributions will be licensed under its MIT License.

## Code contributions

To begin contributing to the project, please follow these steps.

1. [Fork](https://help.github.com/en/articles/fork-a-repo) the repo.
2. Clone your fork locally:

```shell
$ git clone git@github.com:your_username/backtester_options.git
```

3. Create the environment and install dependencies:

```shell
$ make init
```

4. Create your development branch from `master`

```shell
$ git checkout -b your_branch master
```

5. Start coding your contribution (Thanks!)

6. Make sure your code passes all tests, lints and is formatted correctly (`TODO`: Add linting, code formatting to Travis.)

6. Submit a pull request with a brief explanation of your work.

## Types of Contributions

### Bug reports

Make sure to follow the setup steps detailed in the [readme](README.md). If you find a bug, please create an issue with the label `bug` and provide the following information:

- Operating System and version.
- Steps taken to replicate the bug.
- What was the expected output and what actually happend.
- Any details of your local environment that might be helpful for troubleshooting.

### Bug fixes

If you find a bug issue you want to fix, follow the steps outlined [above](#code-contributions) and submit a pull request with a link to the original issue.

### Proposing Features

Create an issue detailing what functionality you'd like to see implemented. If you can, provide general advice as to how the proposed feature could be done.

### Implementing Features

Find an issue with the label `help wanted` or `improvement` and [start coding](#code-contributions).  
When you are done, submit a pull request with a link to the original issue and some code samples showing how the code works. Tests are expected when adding new functionality.


### Documentation

We encourage users to improve our project documentation, either via docstrings, markdown documents to be added to the [project wiki](https://github.com/lambdaclass/backtester_options/wiki) or writing blog posts.  
Let us know via issues labeled `docs` and we'll credit you appropriately.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 Federico Carrone

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
NIX_CMD := XDG_CACHE_HOME=$(CURDIR)/.cache nix --extra-experimental-features 'nix-command flakes' develop --command
RUNCMD := $(NIX_CMD)
PYTHON := python

.PHONY: test test-bench lint typecheck notebooks rust-build rust-test rust-bench bench install-dev compare-bt benchmark-matrix walk-forward-report parity-gate bench-rust-vs-python help
.DEFAULT_GOAL := help

test: ## Run all tests
	$(RUNCMD) $(PYTHON) -m pytest -v tests

test-bench: ## Run benchmark/property tests
	$(RUNCMD) $(PYTHON) -m pytest -v -m bench tests/bench

lint: ## Run ruff linter
	$(RUNCMD) $(PYTHON) -m ruff check options_portfolio_backtester

typecheck: ## Run mypy type checker
	$(RUNCMD) $(PYTHON) -m mypy options_portfolio_backtester --ignore-missing-imports

notebooks: ## Execute all notebooks
	@for nb in notebooks/*.ipynb; do \
		echo "Running $$nb..."; \
		$(RUNCMD) $(PYTHON) -m jupyter nbconvert --to notebook --execute "$$nb" \
			--output "$$(basename $$nb)" --ExecutePreprocessor.timeout=600 || true; \
	done

rust-build: ## Build Rust extension with maturin (release)
	$(RUNCMD) bash -c 'cd rust && maturin develop --manifest-path ob_python/Cargo.toml --release'

rust-test: ## Run Rust unit tests
	$(RUNCMD) bash -c 'cd rust && cargo test'

rust-bench: ## Run Rust benchmarks (criterion)
	$(RUNCMD) bash -c 'cd rust && cargo bench'

bench: rust-build ## Run Python benchmarks (requires Rust build)
	$(RUNCMD) $(PYTHON) -m pytest tests/bench/ -v -m bench --benchmark-only 2>/dev/null || \
		echo "Install pytest-benchmark for Python benchmarks"

install-dev: ## Install local dev deps into active nix dev environment
	$(PYTHON) -m pip install -e '.[dev,charts,notebooks,rust]'

compare-bt: ## Compare stock-only monthly rebalance vs bt library
	$(RUNCMD) $(PYTHON) scripts/compare_with_bt.py

benchmark-matrix: ## Run standardized runtime/accuracy matrix vs bt
	$(RUNCMD) $(PYTHON) scripts/benchmark_matrix.py

walk-forward-report: ## Run walk-forward/OOS harness and save report
	$(RUNCMD) $(PYTHON) scripts/walk_forward_report.py

bench-rust-vs-python: ## Benchmark Rust vs Python vs bt (options + stock-only)
	$(RUNCMD) $(PYTHON) scripts/benchmark_rust_vs_python.py --stock-only

parity-gate: ## Run bt overlap parity CI gate (bench marker)
	$(RUNCMD) $(PYTHON) -m pytest -v tests/compat/test_bt_overlap_gate.py -m bench

help:
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\
	awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'


================================================
FILE: README.md
================================================
Options Portfolio Backtester
============================

Backtest options strategies with realistic execution, Greeks-aware risk management, and contract-level inventory. Also handles equities and multi-asset portfolios. Optional Rust core for speed.

## Get started

### Install

With Nix:
```shell
nix develop
```

Without Nix (Python >= 3.12):
```shell
python -m venv .venv && source .venv/bin/activate
make install-dev
```

### Get data

```shell
python data/fetch_data.py all --symbols SPY
```

Downloads SPY stock prices and options chains to `data/processed/`. Supports 104+ symbols. See [`data/README.md`](data/README.md) for details.

### Run your first backtest

```python
from options_portfolio_backtester import (
    BacktestEngine, Stock, Type, Direction,
    HistoricalOptionsData, TiingoData,
    Strategy, StrategyLeg,
    NearestDelta, PerContractCommission,
    RiskManager, MaxDelta, MaxDrawdown,
)

# Load data
options_data = HistoricalOptionsData("data/processed/options.csv")
stocks_data = TiingoData("data/processed/stocks.csv")
schema = options_data.schema

# Define strategy: buy OTM puts on SPY, exit when DTE drops below 30
strategy = Strategy(schema)
leg = StrategyLeg("leg_1", schema, option_type=Type.PUT, direction=Direction.BUY)
leg.entry_filter = (
    (schema.underlying == "SPY")
    & (schema.dte >= 60) & (schema.dte <= 120)
    & (schema.delta >= -0.25) & (schema.delta <= -0.10)
)
leg.exit_filter = schema.dte <= 30
strategy.add_leg(leg)

# Run backtest: 97% stocks, 3% options
engine = BacktestEngine(
    allocation={"stocks": 0.97, "options": 0.03, "cash": 0.0},
    initial_capital=1_000_000,
    cost_model=PerContractCommission(rate=0.65),
    signal_selector=NearestDelta(target_delta=-0.20),
    risk_manager=RiskManager([MaxDelta(100.0), MaxDrawdown(0.20)]),
)
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)

# Results
print(engine.balance["total capital"].iloc[-1])  # final capital
print(len(engine.trade_log))                      # number of trades
```

### Strategy presets

Instead of building legs manually:

```python
from options_portfolio_backtester import Strangle

strangle = Strangle(schema, "short", "SPY",
                    dte_entry_range=(30, 60), dte_exit=7,
                    otm_pct=5, pct_tolerance=1,
                    exit_thresholds=(0.2, 0.2))
```

Available presets: `Strangle`, `IronCondor`, `CoveredCall`, `CashSecuredPut`, `Collar`, `Butterfly`.

### Stock-only backtest with algo pipeline

For equity portfolios without options, use the pipeline API:

```python
from options_portfolio_backtester.engine.pipeline import (
    AlgoPipelineBacktester,
    RunMonthly, SelectAll, WeighInvVol, LimitWeights, Rebalance,
)
import pandas as pd

prices = pd.read_csv("data/processed/stocks.csv", parse_dates=["date"])
prices = prices.pivot(index="date", columns="symbol", values="adjClose")

bt = AlgoPipelineBacktester(
    prices=prices,
    initial_capital=1_000_000,
    algos=[
        RunMonthly(),
        SelectAll(),
        WeighInvVol(lookback=252),
        LimitWeights(limit=0.25),
        Rebalance(),
    ],
)
bt.run()
```

## Execution models

Every component is swappable. Pass them to `BacktestEngine(...)` or override per-leg.

**Signal selectors** — which contract to pick from candidates:
`FirstMatch()`, `NearestDelta(target)`, `MaxOpenInterest()`

**Cost models** — commissions and fees:
`NoCosts()`, `PerContractCommission(rate)`, `TieredCommission(tiers)`, `SpreadSlippage(pct)`

**Fill models** — execution price:
`MarketAtBidAsk()`, `MidPrice()`, `VolumeAwareFill(threshold)`

**Position sizers** — how many contracts:
`CapitalBased()`, `FixedQuantity(qty)`, `FixedDollar(amount)`, `PercentOfPortfolio(pct)`

**Risk constraints** — pre-trade gating:
`MaxDelta(limit)`, `MaxVega(limit)`, `MaxDrawdown(max_dd_pct)`

## Rebalancing model

At each rebalance date, the engine follows a **full liquidation** approach:

1. **Liquidate all options** — every open option position is sold at current market price (bid for long, ask for short)
2. **Compute total capital** — cash + stock value (options are zero after liquidation)
3. **Rebalance stocks** — sell all stocks, buy fresh at target allocation (e.g. 97%)
4. **Buy new options** — use the full options allocation (e.g. 3%) to purchase contracts matching entry criteria (DTE, delta, etc.)

This ensures:
- **Clean accounting** — no stale option value carried across rebalances, no money creation
- **Fresh positions** — every rebalance picks the best available contracts for current market conditions
- **Simple math** — `total_capital = cash + stocks` at the point of redeployment, no complex delta tracking

Between rebalance dates, positions are held (mark-to-market for balance tracking). If `check_exits_daily=True`, exit filters run daily but no new entries are made until the next rebalance.

For the **Spitznagel leverage** model (`options_budget` parameter), options are funded separately from the stock allocation so `{stocks: 1.0, options: 0.005}` means 100% equity + 0.5% put budget on top.

## Rust acceleration

Optional. Falls back to Python when not installed.

```shell
make rust-build
```

| Benchmark | Python | Rust |
|-----------|--------|------|
| Full options backtest (24.7M rows) | 10.0s | **4.2s** |
| Stock-only monthly rebalance | 3.7s | **0.6s** |
| Parallel grid sweep (100 configs) | — | **5-8x** faster (Rayon, bypasses GIL) |

## Data

```shell
# SPY stock + options data
python data/fetch_data.py all --symbols SPY

# Multiple symbols
python data/fetch_data.py all --symbols SPY IWM QQQ --start 2020-01-01 --end 2023-01-01

# FRED macro signals (VIX, GDP, Buffett Indicator, etc.)
python data/fetch_signals.py

# Convert OptionsDX format
python data/convert_optionsdx.py data/raw/spx_eod_2020.csv --output data/processed/spx_options.csv
```

You can also bring your own CSVs. Required columns:
- **Stocks**: `date`, `symbol`, `adjClose`
- **Options**: `quotedate`, `underlying`, `type`, `strike`, `expiration`, `dte`, `bid`, `ask`, `volume`, `openinterest`, `delta`

## Tests

```shell
make test            # all tests (1300+)
make test-regression # regression snapshots (locked golden values)
make test-chaos      # fault injection (corrupted/adversarial data)
make muttest         # mutation testing on core modules
make lint            # ruff
make typecheck       # mypy
make rust-test       # Rust unit tests
```

## Architecture

```
options_portfolio_backtester/
├── core/            # Types: Direction, OptionType, Greeks, Fill, Order
├── data/            # Schema DSL, CSV providers
├── strategy/        # Strategy, StrategyLeg, presets
├── execution/       # CostModel, FillModel, Sizer, SignalSelector
├── portfolio/       # Portfolio, OptionPosition, RiskManager
├── engine/          # BacktestEngine, AlgoPipelineBacktester, StrategyTreeEngine
└── analytics/       # BacktestStats, TradeLog, TearsheetReport, charts

rust/
├── ob_core/         # Backtest loop, stats, execution models, filter parser
└── ob_python/       # PyO3 bindings, parallel sweep, Arrow bridge
```

## Pipeline algos

40+ composable algos for the `AlgoPipelineBacktester`. All follow `__call__(ctx) -> StepDecision`.

**Scheduling**: `RunDaily`, `RunWeekly`, `RunMonthly`, `RunQuarterly`, `RunYearly`, `RunOnce`, `RunOnDate`, `RunAfterDate`, `RunAfterDays`, `RunEveryNPeriods`, `RunIfOutOfBounds`, `Or`, `Not`, `Require`

**Selection**: `SelectAll`, `SelectThese`, `SelectHasData`, `SelectN`, `SelectMomentum`, `SelectWhere`, `SelectRandomly`, `SelectActive`, `SelectRegex`

**Weighting**: `WeighEqually`, `WeighSpecified`, `WeighTarget`, `WeighInvVol`, `WeighMeanVar`, `WeighERC`, `TargetVol`, `WeighRandomly`

**Risk & rebalancing**: `LimitWeights`, `LimitDeltas`, `ScaleWeights`, `HedgeRisks`, `Margin`, `MaxDrawdownGuard`, `Rebalance`, `RebalanceOverTime`, `CapitalFlow`, `CloseDead`, `ClosePositionsAfterDates`, `ReplayTransactions`, `CouponPayingPosition`

## Research

Research notebooks and analysis: [finance_research](https://github.com/unbalancedparentheses/finance_research).


================================================
FILE: benchmarks/benchmark_large_pipeline.py
================================================
"""Large-scale performance benchmark: Rust vs Python on production data.

Runs the same strategy through Rust full-loop and Python BacktestEngine on
the full SPY options dataset (24.7M rows, 4500+ trading days) with frequent
rebalancing to produce thousands of trades.

Usage:
    python scripts/benchmark_large_pipeline.py
    python scripts/benchmark_large_pipeline.py --rebalance-freq 2 --runs 3
"""

from __future__ import annotations

import argparse
import gc
import os
import sys
import time
from dataclasses import dataclass, field
from pathlib import Path

import numpy as np
import pandas as pd

REPO_ROOT = Path(__file__).resolve().parents[1]

from options_portfolio_backtester import BacktestEngine as LegacyBacktest
from options_portfolio_backtester.data.providers import HistoricalOptionsData, TiingoData
from options_portfolio_backtester.core.types import Direction, Stock, OptionType as Type
from options_portfolio_backtester.strategy.strategy import Strategy
from options_portfolio_backtester.strategy.strategy_leg import StrategyLeg

from options_portfolio_backtester.engine.engine import BacktestEngine
from options_portfolio_backtester.engine._dispatch import use_rust
from options_portfolio_backtester.engine import _dispatch as _rust_dispatch
from options_portfolio_backtester.execution.cost_model import NoCosts


STOCKS_FILE = os.path.join(REPO_ROOT, "data", "processed", "stocks.csv")
OPTIONS_FILE = os.path.join(REPO_ROOT, "data", "processed", "options.csv")


@dataclass
class BenchResult:
    name: str
    runtime_s: float
    final_capital: float
    total_return_pct: float
    n_trades: int
    n_balance_rows: int
    dispatch_mode: str
    peak_mem_mb: float = 0.0
    per_run_times: list[float] = field(default_factory=list)


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Large-scale Rust vs Python benchmark.")
    p.add_argument("--runs", type=int, default=3,
                   help="Number of timing runs (default: 3).")
    p.add_argument("--rebalance-freq", type=int, default=1,
                   help="Rebalance frequency in business months (1=monthly).")
    p.add_argument("--dte-min", type=int, default=20,
                   help="Min DTE for entry filter.")
    p.add_argument("--dte-max", type=int, default=60,
                   help="Max DTE for entry filter.")
    p.add_argument("--dte-exit", type=int, default=10,
                   help="DTE threshold for exit.")
    p.add_argument("--initial-capital", type=int, default=1_000_000,
                   help="Initial capital.")
    p.add_argument("--options-pct", type=float, default=0.10,
                   help="Options allocation pct (0.10 = 10%%).")
    return p.parse_args()


def _load_data():
    print("Loading data...")
    t0 = time.perf_counter()
    stocks_data = TiingoData(STOCKS_FILE)
    options_data = HistoricalOptionsData(OPTIONS_FILE)
    load_time = time.perf_counter() - t0
    n_opt = len(options_data._data)
    n_stk = len(stocks_data._data)
    n_dates = options_data._data["quotedate"].nunique()
    print(f"  Loaded in {load_time:.2f}s")
    print(f"  Options: {n_opt:,} rows, {n_dates:,} trading days")
    print(f"  Stocks:  {n_stk:,} rows")
    return stocks_data, options_data


def _strategy(schema, dte_min, dte_max, dte_exit):
    strat = Strategy(schema)
    leg = StrategyLeg("leg_1", schema, option_type=Type.PUT, direction=Direction.BUY)
    leg.entry_filter = (
        (schema.underlying == "SPY")
        & (schema.dte >= dte_min)
        & (schema.dte <= dte_max)
    )
    leg.exit_filter = schema.dte <= dte_exit
    strat.add_legs([leg])
    return strat


def _copy_data(stocks_data, options_data):
    """Deep-copy data handlers to avoid cross-run contamination."""
    sd = TiingoData.__new__(TiingoData)
    sd.__dict__.update(stocks_data.__dict__)
    sd._data = stocks_data._data.copy()

    od = HistoricalOptionsData.__new__(HistoricalOptionsData)
    od.__dict__.update(options_data.__dict__)
    od._data = options_data._data.copy()
    return sd, od


def run_engine(
    stocks_data, options_data, args, runs, force_python=False,
) -> BenchResult:
    """Run BacktestEngine. If force_python, temporarily disable Rust dispatch."""
    label = "Python BacktestEngine" if force_python else "Rust BacktestEngine"
    times = []
    engine = None

    for i in range(runs):
        sd, od = _copy_data(stocks_data, options_data)
        engine = BacktestEngine(
            {"stocks": 1.0 - args.options_pct, "options": args.options_pct, "cash": 0.0},
            cost_model=NoCosts(),
            initial_capital=args.initial_capital,
        )
        engine.stocks = [Stock("SPY", 1.0)]
        engine.stocks_data = sd
        engine.options_data = od
        engine.options_strategy = _strategy(od.schema, args.dte_min, args.dte_max, args.dte_exit)

        gc.collect()
        saved_rust = _rust_dispatch.RUST_AVAILABLE
        if force_python:
            _rust_dispatch.RUST_AVAILABLE = False
        try:
            t0 = time.perf_counter()
            engine.run(rebalance_freq=args.rebalance_freq)
            elapsed = time.perf_counter() - t0
        finally:
            _rust_dispatch.RUST_AVAILABLE = saved_rust
        times.append(elapsed)
        print(f"  {label} run {i+1}/{runs}: {elapsed:.3f}s")

    assert engine is not None
    mode = engine.run_metadata.get("dispatch_mode", "unknown")
    final = float(engine.balance["total capital"].iloc[-1])
    n_trades = len(engine.trade_log) if not engine.trade_log.empty else 0
    total_ret = (final / args.initial_capital - 1.0) * 100.0
    return BenchResult(
        name=label,
        runtime_s=float(np.mean(times)),
        final_capital=final,
        total_return_pct=total_ret,
        n_trades=n_trades,
        n_balance_rows=len(engine.balance),
        dispatch_mode=mode,
        per_run_times=times,
    )


def run_legacy(stocks_data, options_data, args, runs) -> BenchResult:
    """Run legacy Backtest class."""
    times = []
    bt = None

    for i in range(runs):
        sd, od = _copy_data(stocks_data, options_data)
        bt = LegacyBacktest(
            {"stocks": 1.0 - args.options_pct, "options": args.options_pct, "cash": 0.0},
            initial_capital=args.initial_capital,
        )
        bt.stocks = [Stock("SPY", 1.0)]
        bt.stocks_data = sd
        bt.options_data = od
        bt.options_strategy = _strategy(od.schema, args.dte_min, args.dte_max, args.dte_exit)

        gc.collect()
        t0 = time.perf_counter()
        bt.run(rebalance_freq=args.rebalance_freq)
        elapsed = time.perf_counter() - t0
        times.append(elapsed)
        print(f"  Legacy Python run {i+1}/{runs}: {elapsed:.3f}s")

    assert bt is not None
    final = float(bt.balance["total capital"].iloc[-1])
    n_trades = len(bt.trade_log) if not bt.trade_log.empty else 0
    total_ret = (final / bt.initial_capital - 1.0) * 100.0
    return BenchResult(
        name="Legacy Python Backtest",
        runtime_s=float(np.mean(times)),
        final_capital=final,
        total_return_pct=total_ret,
        n_trades=n_trades,
        n_balance_rows=len(bt.balance),
        dispatch_mode="python-legacy",
        per_run_times=times,
    )


def print_result(r: BenchResult, indent: str = "  ") -> None:
    print(f"{indent}{r.name}")
    print(f"{indent}  dispatch:       {r.dispatch_mode}")
    print(f"{indent}  avg runtime:    {r.runtime_s:.3f}s")
    print(f"{indent}  per-run times:  [{', '.join(f'{t:.3f}s' for t in r.per_run_times)}]")
    print(f"{indent}  final capital:  ${r.final_capital:,.2f}")
    print(f"{indent}  total return:   {r.total_return_pct:.4f}%")
    print(f"{indent}  trades:         {r.n_trades:,}")
    print(f"{indent}  balance rows:   {r.n_balance_rows:,}")


def print_comparison(a: BenchResult, b: BenchResult) -> None:
    if a.runtime_s > 0:
        speedup = b.runtime_s / a.runtime_s
    else:
        speedup = float("nan")
    cap_delta = abs(a.final_capital - b.final_capital)
    ret_delta = a.total_return_pct - b.total_return_pct
    cap_pct = (cap_delta / max(a.final_capital, 1)) * 100
    print(f"  {a.name} vs {b.name}:")
    print(f"    speedup:          {speedup:.2f}x ({a.name} is {'faster' if speedup > 1 else 'slower'})")
    print(f"    capital delta:    ${cap_delta:,.2f} ({cap_pct:.4f}%)")
    print(f"    return delta:     {ret_delta:+.4f} pct-pts")
    if a.n_trades > 0 and b.n_trades > 0:
        print(f"    trade count:      {a.n_trades:,} vs {b.n_trades:,} ({'match' if a.n_trades == b.n_trades else 'MISMATCH'})")


def main() -> None:
    args = parse_args()

    for f in (STOCKS_FILE, OPTIONS_FILE):
        if not Path(f).exists():
            print(f"ERROR: Missing data file: {f}")
            print("Run this benchmark from the repo root with production data in data/processed/")
            sys.exit(1)

    print(f"\n{'='*65}")
    print("Large-Scale Performance Benchmark: Rust vs Python")
    print(f"{'='*65}")
    print(f"  Rust available:    {use_rust()}")
    print(f"  runs per backend:  {args.runs}")
    print(f"  rebalance freq:    {args.rebalance_freq} BMS")
    print(f"  strategy:          BUY PUT, DTE {args.dte_min}-{args.dte_max}, exit DTE <= {args.dte_exit}")
    print(f"  allocation:        {(1-args.options_pct)*100:.0f}% stocks / {args.options_pct*100:.0f}% options")
    print(f"  initial capital:   ${args.initial_capital:,}")
    print()

    stocks_data, options_data = _load_data()
    print()

    # -- Run all backends --
    results = []

    if use_rust():
        print("Running Rust BacktestEngine...")
        rust_result = run_engine(stocks_data, options_data, args, args.runs, force_python=False)
        results.append(rust_result)
        print()

    print("Running Python BacktestEngine...")
    python_result = run_engine(stocks_data, options_data, args, args.runs, force_python=True)
    results.append(python_result)
    print()

    print("Running Legacy Python Backtest...")
    legacy_result = run_legacy(stocks_data, options_data, args, args.runs)
    results.append(legacy_result)
    print()

    # -- Report --
    print(f"{'='*65}")
    print("Results")
    print(f"{'='*65}")
    for r in results:
        print_result(r)
        print()

    print(f"{'='*65}")
    print("Comparisons")
    print(f"{'='*65}")
    if use_rust():
        print_comparison(rust_result, python_result)
        print()
        print_comparison(rust_result, legacy_result)
        print()
    print_comparison(python_result, legacy_result)
    print()

    # -- Summary table --
    print(f"{'='*65}")
    print("Summary Table")
    print(f"{'='*65}")
    rows = []
    for r in results:
        rows.append({
            "Backend": r.name,
            "Dispatch": r.dispatch_mode,
            "Avg Time (s)": f"{r.runtime_s:.3f}",
            "Trades": f"{r.n_trades:,}",
            "Final Capital": f"${r.final_capital:,.0f}",
            "Return %": f"{r.total_return_pct:.2f}",
        })
    df = pd.DataFrame(rows)
    print(df.to_string(index=False))

    if use_rust() and rust_result.runtime_s > 0:
        print(f"\n  Rust speedup over Python Engine: {python_result.runtime_s / rust_result.runtime_s:.2f}x")
        print(f"  Rust speedup over Legacy:        {legacy_result.runtime_s / rust_result.runtime_s:.2f}x")

    print("\nDone.")


if __name__ == "__main__":
    main()


================================================
FILE: benchmarks/benchmark_matrix.py
================================================
"""Standardized benchmark matrix for options_portfolio_backtester vs bt.

Runs multiple scenarios over date ranges/rebalance frequencies and writes
a CSV scorecard with runtime and parity metrics.
"""

from __future__ import annotations

import argparse
import sys
import time
from dataclasses import dataclass
from pathlib import Path

import numpy as np
import pandas as pd

REPO_ROOT = Path(__file__).resolve().parents[1]

from options_portfolio_backtester import BacktestEngine as Backtest
from options_portfolio_backtester.data.providers import TiingoData
from options_portfolio_backtester.core.types import Stock


@dataclass(frozen=True)
class Scenario:
    label: str
    start: pd.Timestamp
    end: pd.Timestamp
    rebalance_months: int
    initial_capital: float


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Benchmark matrix vs bt.")
    p.add_argument("--stocks-file", default="data/processed/stocks.csv")
    p.add_argument("--symbols", default="SPY")
    p.add_argument("--weights", default=None)
    p.add_argument("--date-ranges", default="2008-01-01:2025-12-12,2016-01-01:2025-12-12")
    p.add_argument("--rebalance-months", default="1,3")
    p.add_argument("--initial-capitals", default="1000000")
    p.add_argument("--runs", type=int, default=3)
    p.add_argument("--output", default="data/processed/benchmark_matrix.csv")
    return p.parse_args()


def parse_csv_list(s: str, cast):
    return [cast(x.strip()) for x in s.split(",") if x.strip()]


def normalize_weights(symbols: list[str], raw_weights: str | None) -> list[float]:
    if raw_weights is None:
        return [1.0 / len(symbols)] * len(symbols)
    vals = [float(x) for x in raw_weights.split(",")]
    if len(vals) != len(symbols):
        raise ValueError("--weights length must match --symbols length")
    total = float(sum(vals))
    if total <= 0:
        raise ValueError("--weights must sum to > 0")
    return [v / total for v in vals]


def compute_metrics(total_capital: pd.Series) -> tuple[float, float, float, float, float]:
    total_capital = total_capital.dropna()
    if total_capital.empty:
        return 0.0, 0.0, 0.0, 0.0, 0.0
    rets = total_capital.pct_change().dropna()
    total_return = total_capital.iloc[-1] / total_capital.iloc[0] - 1.0
    n_years = len(total_capital) / 252.0
    cagr = (total_capital.iloc[-1] / total_capital.iloc[0]) ** (1.0 / n_years) - 1.0 if n_years > 0 else 0.0
    peak = total_capital.cummax()
    dd = total_capital / peak - 1.0
    max_dd = float(dd.min()) if not dd.empty else 0.0
    vol = float(rets.std(ddof=1) * np.sqrt(252)) if len(rets) > 1 else 0.0
    sharpe = float((rets.mean() / rets.std(ddof=1)) * np.sqrt(252)) if len(rets) > 1 and rets.std(ddof=1) > 0 else 0.0
    return total_return, cagr, max_dd, vol, sharpe


def slice_stocks_data(stocks_file: str, start: pd.Timestamp, end: pd.Timestamp) -> TiingoData:
    d = TiingoData(stocks_file)
    m = (d._data["date"] >= start) & (d._data["date"] <= end)
    d._data = d._data.loc[m].copy()
    d.start_date = d._data["date"].min()
    d.end_date = d._data["date"].max()
    return d


def run_options_portfolio_backtester(
    stocks_file: str,
    symbols: list[str],
    weights: list[float],
    scenario: Scenario,
    runs: int,
) -> tuple[dict[str, float], pd.Series]:
    stocks = [Stock(sym, w) for sym, w in zip(symbols, weights)]
    runtimes = []
    last_eq = pd.Series(dtype=float)
    for _ in range(runs):
        stocks_data = slice_stocks_data(stocks_file, scenario.start, scenario.end)
        bt = Backtest({"stocks": 1.0, "options": 0.0, "cash": 0.0}, initial_capital=int(scenario.initial_capital))
        bt.stocks = stocks
        bt.stocks_data = stocks_data
        t0 = time.perf_counter()
        bt.run(rebalance_freq=scenario.rebalance_months, rebalance_unit="BMS")
        runtimes.append(time.perf_counter() - t0)
        last_eq = bt.balance["total capital"].dropna()
    tr, cagr, mdd, vol, sharpe = compute_metrics(last_eq)
    return ({
        "ob_runtime_s": float(np.mean(runtimes)),
        "ob_total_return_pct": tr * 100.0,
        "ob_cagr_pct": cagr * 100.0,
        "ob_max_drawdown_pct": mdd * 100.0,
        "ob_vol_annual_pct": vol * 100.0,
        "ob_sharpe": sharpe,
        "ob_rows": float(len(last_eq)),
    }, last_eq)


def run_bt(
    stocks_file: str,
    symbols: list[str],
    weights: list[float],
    scenario: Scenario,
    runs: int,
) -> tuple[dict[str, float], pd.Series | None]:
    try:
        import bt  # type: ignore
    except Exception:
        return ({"bt_available": 0.0}, None)

    prices = pd.read_csv(stocks_file, parse_dates=["date"])
    m = (prices["date"] >= scenario.start) & (prices["date"] <= scenario.end) & (prices["symbol"].isin(symbols))
    prices = prices.loc[m].copy()
    px = prices.pivot(index="date", columns="symbol", values="adjClose").sort_index().dropna()
    px = px[symbols]

    runtimes = []
    last_eq = None
    for _ in range(runs):
        algos = [
            bt.algos.RunMonthly(),
            bt.algos.SelectThese(symbols),
            bt.algos.WeighSpecified(**{s: w for s, w in zip(symbols, weights)}),
            bt.algos.Rebalance(),
        ]
        test = bt.Backtest(bt.Strategy("bench_matrix", algos), px, initial_capital=scenario.initial_capital)
        t0 = time.perf_counter()
        res = bt.run(test)
        runtimes.append(time.perf_counter() - t0)
        last_eq = res.prices.iloc[:, 0]

    assert last_eq is not None
    tr, cagr, mdd, vol, sharpe = compute_metrics(last_eq)
    return ({
        "bt_available": 1.0,
        "bt_runtime_s": float(np.mean(runtimes)),
        "bt_total_return_pct": tr * 100.0,
        "bt_cagr_pct": cagr * 100.0,
        "bt_max_drawdown_pct": mdd * 100.0,
        "bt_vol_annual_pct": vol * 100.0,
        "bt_sharpe": sharpe,
        "bt_rows": float(len(last_eq)),
    }, last_eq)


def overlap_parity(ob_eq: pd.Series, bt_eq: pd.Series | None) -> dict[str, float]:
    if bt_eq is None:
        return {"overlap_rows": 0.0, "overlap_end_delta": np.nan, "overlap_max_abs_delta": np.nan}
    common = ob_eq.index.intersection(bt_eq.index)
    if len(common) == 0:
        return {"overlap_rows": 0.0, "overlap_end_delta": np.nan, "overlap_max_abs_delta": np.nan}
    ob_n = ob_eq.loc[common] / ob_eq.loc[common].iloc[0]
    bt_n = bt_eq.loc[common] / bt_eq.loc[common].iloc[0]
    d = ob_n - bt_n
    return {
        "overlap_rows": float(len(common)),
        "overlap_end_delta": float(d.iloc[-1]),
        "overlap_max_abs_delta": float(d.abs().max()),
    }


def build_scenarios(args: argparse.Namespace) -> list[Scenario]:
    date_ranges = []
    for chunk in args.date_ranges.split(","):
        chunk = chunk.strip()
        if not chunk:
            continue
        s, e = chunk.split(":")
        date_ranges.append((pd.Timestamp(s), pd.Timestamp(e)))
    rebal = parse_csv_list(args.rebalance_months, int)
    capitals = parse_csv_list(args.initial_capitals, float)

    scenarios = []
    idx = 1
    for s, e in date_ranges:
        for r in rebal:
            for c in capitals:
                scenarios.append(Scenario(
                    label=f"S{idx}",
                    start=s,
                    end=e,
                    rebalance_months=r,
                    initial_capital=c,
                ))
                idx += 1
    return scenarios


def main() -> None:
    args = parse_args()
    symbols = [s.strip().upper() for s in args.symbols.split(",") if s.strip()]
    if not symbols:
        raise ValueError("No symbols provided")
    weights = normalize_weights(symbols, args.weights)
    scenarios = build_scenarios(args)

    rows = []
    for sc in scenarios:
        ob_stats, ob_eq = run_options_portfolio_backtester(
            stocks_file=args.stocks_file,
            symbols=symbols,
            weights=weights,
            scenario=sc,
            runs=args.runs,
        )
        bt_stats, bt_eq = run_bt(
            stocks_file=args.stocks_file,
            symbols=symbols,
            weights=weights,
            scenario=sc,
            runs=args.runs,
        )
        parity = overlap_parity(ob_eq, bt_eq)
        row = {
            "scenario": sc.label,
            "start": sc.start.date().isoformat(),
            "end": sc.end.date().isoformat(),
            "rebalance_months": sc.rebalance_months,
            "initial_capital": sc.initial_capital,
            "symbols": ",".join(symbols),
            "weights": ",".join(f"{w:.6f}" for w in weights),
            **ob_stats,
            **bt_stats,
            **parity,
        }
        if bt_stats.get("bt_available", 0.0) == 1.0:
            row["speed_ratio_bt_over_ob"] = row["bt_runtime_s"] / row["ob_runtime_s"] if row["ob_runtime_s"] > 0 else np.nan
            row["return_delta_pct_pts"] = row["ob_total_return_pct"] - row["bt_total_return_pct"]
            row["maxdd_delta_pct_pts"] = row["ob_max_drawdown_pct"] - row["bt_max_drawdown_pct"]
        else:
            row["speed_ratio_bt_over_ob"] = np.nan
            row["return_delta_pct_pts"] = np.nan
            row["maxdd_delta_pct_pts"] = np.nan
        rows.append(row)

    out = pd.DataFrame(rows).sort_values(["start", "rebalance_months", "initial_capital"])
    out_path = Path(args.output)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out.to_csv(out_path, index=False)

    print("\n=== Benchmark Matrix Summary ===")
    print(f"scenarios: {len(out)}")
    print(f"output: {out_path}")
    cols = [
        "scenario", "start", "end", "rebalance_months",
        "ob_runtime_s", "bt_runtime_s", "speed_ratio_bt_over_ob",
        "return_delta_pct_pts", "maxdd_delta_pct_pts", "overlap_max_abs_delta",
    ]
    print(out[cols].to_string(index=False))


if __name__ == "__main__":
    main()


================================================
FILE: benchmarks/benchmark_rust_vs_python.py
================================================
"""Benchmark: Rust full-loop vs Python BacktestEngine vs legacy Backtest vs bt.

Runs options backtest (with options data) through Rust and Python paths, plus
a stock-only comparison against bt if installed.

Usage:
    python scripts/benchmark_rust_vs_python.py
    python scripts/benchmark_rust_vs_python.py --runs 5 --stock-only
"""

from __future__ import annotations

import argparse
import os
import sys
import time
from dataclasses import dataclass
from pathlib import Path

import numpy as np
import pandas as pd

REPO_ROOT = Path(__file__).resolve().parents[1]

from options_portfolio_backtester import BacktestEngine as LegacyBacktest
from options_portfolio_backtester.data.providers import HistoricalOptionsData, TiingoData
from options_portfolio_backtester.core.types import Direction, Stock, OptionType as Type
from options_portfolio_backtester.strategy.strategy import Strategy
from options_portfolio_backtester.strategy.strategy_leg import StrategyLeg

from options_portfolio_backtester.engine.engine import BacktestEngine
from options_portfolio_backtester.engine._dispatch import use_rust
from options_portfolio_backtester.engine import _dispatch as _rust_dispatch
from options_portfolio_backtester.execution.cost_model import NoCosts
from options_portfolio_backtester.execution.fill_model import MarketAtBidAsk
from options_portfolio_backtester.execution.signal_selector import FirstMatch

TEST_DIR = os.path.join(REPO_ROOT, "backtester", "test")
STOCKS_FILE = os.path.join(TEST_DIR, "test_data", "ivy_5assets_data.csv")
OPTIONS_FILE = os.path.join(TEST_DIR, "test_data", "options_data.csv")
PROD_STOCKS_FILE = os.path.join(REPO_ROOT, "data", "processed", "stocks.csv")
PROD_OPTIONS_FILE = os.path.join(REPO_ROOT, "data", "processed", "options.csv")


@dataclass
class BenchResult:
    name: str
    runtime_s: float
    final_capital: float
    total_return_pct: float
    n_trades: int
    dispatch_mode: str


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Benchmark Rust vs Python backtest paths.")
    p.add_argument("--runs", type=int, default=3, help="Timing averaging repeats.")
    p.add_argument("--stock-only", action="store_true", help="Also run stock-only comparison vs bt.")
    p.add_argument("--use-prod-data", action="store_true", help="Use production data files if available.")
    p.add_argument("--rebalance-freq", type=int, default=1, help="Rebalance frequency.")
    return p.parse_args()


def _stocks(use_prod: bool = False):
    if use_prod:
        return [Stock("SPY", 1.0)]
    return [Stock("VTI", 0.2), Stock("VEU", 0.2), Stock("BND", 0.2),
            Stock("VNQ", 0.2), Stock("DBC", 0.2)]


def _load_data(use_prod: bool):
    if use_prod and Path(PROD_STOCKS_FILE).exists() and Path(PROD_OPTIONS_FILE).exists():
        stocks_file, options_file = PROD_STOCKS_FILE, PROD_OPTIONS_FILE
    else:
        stocks_file, options_file = STOCKS_FILE, OPTIONS_FILE

    stocks_data = TiingoData(stocks_file)
    options_data = HistoricalOptionsData(options_file)

    if stocks_file == STOCKS_FILE:
        stocks_data._data["adjClose"] = 10
        options_data._data.at[2, "ask"] = 1
        options_data._data.at[2, "bid"] = 0.5
        options_data._data.at[51, "ask"] = 1.5
        options_data._data.at[50, "bid"] = 0.5
        options_data._data.at[130, "bid"] = 0.5
        options_data._data.at[131, "bid"] = 1.5
        options_data._data.at[206, "bid"] = 0.5
        options_data._data.at[207, "bid"] = 1.5

    return stocks_data, options_data, stocks_file


def _buy_strategy(schema):
    strat = Strategy(schema)
    leg = StrategyLeg("leg_1", schema, option_type=Type.PUT, direction=Direction.BUY)
    leg.entry_filter = (schema.underlying == "SPX") & (schema.dte >= 60)
    leg.exit_filter = schema.dte <= 30
    strat.add_legs([leg])
    return strat


# ---------------------------------------------------------------------------
# Runners
# ---------------------------------------------------------------------------

def run_engine_python(stocks_data, options_data, stocks, rebalance_freq, runs) -> BenchResult:
    """Force Python path by temporarily disabling Rust dispatch."""
    times = []
    engine = None
    for _ in range(runs):
        sd = TiingoData.__new__(TiingoData)
        sd.__dict__.update(stocks_data.__dict__)
        sd._data = stocks_data._data.copy()

        od = HistoricalOptionsData.__new__(HistoricalOptionsData)
        od.__dict__.update(options_data.__dict__)
        od._data = options_data._data.copy()

        engine = BacktestEngine(
            {"stocks": 0.97, "options": 0.03, "cash": 0},
            cost_model=NoCosts(),
        )
        engine.stocks = stocks
        engine.stocks_data = sd
        engine.options_data = od
        engine.options_strategy = _buy_strategy(od.schema)

        saved_rust = _rust_dispatch.RUST_AVAILABLE
        _rust_dispatch.RUST_AVAILABLE = False
        try:
            t0 = time.perf_counter()
            engine.run(rebalance_freq=rebalance_freq)
            times.append(time.perf_counter() - t0)
        finally:
            _rust_dispatch.RUST_AVAILABLE = saved_rust

    assert engine is not None
    final = float(engine.balance["total capital"].iloc[-1])
    n_trades = len(engine.trade_log) if not engine.trade_log.empty else 0
    total_ret = (final / engine.initial_capital - 1) * 100
    return BenchResult(
        name="Python BacktestEngine",
        runtime_s=float(np.mean(times)),
        final_capital=final,
        total_return_pct=total_ret,
        n_trades=n_trades,
        dispatch_mode="python",
    )


def run_engine_rust(stocks_data, options_data, stocks, rebalance_freq, runs) -> BenchResult | None:
    """Let Rust dispatch happen naturally (default path)."""
    if not use_rust():
        return None

    times = []
    engine = None
    for _ in range(runs):
        sd = TiingoData.__new__(TiingoData)
        sd.__dict__.update(stocks_data.__dict__)
        sd._data = stocks_data._data.copy()

        od = HistoricalOptionsData.__new__(HistoricalOptionsData)
        od.__dict__.update(options_data.__dict__)
        od._data = options_data._data.copy()

        engine = BacktestEngine(
            {"stocks": 0.97, "options": 0.03, "cash": 0},
            cost_model=NoCosts(),
        )
        engine.stocks = stocks
        engine.stocks_data = sd
        engine.options_data = od
        engine.options_strategy = _buy_strategy(od.schema)

        t0 = time.perf_counter()
        engine.run(rebalance_freq=rebalance_freq)
        times.append(time.perf_counter() - t0)

    assert engine is not None
    mode = engine.run_metadata.get("dispatch_mode", "unknown")
    final = float(engine.balance["total capital"].iloc[-1])
    n_trades = len(engine.trade_log) if not engine.trade_log.empty else 0
    total_ret = (final / engine.initial_capital - 1) * 100
    return BenchResult(
        name="Rust BacktestEngine",
        runtime_s=float(np.mean(times)),
        final_capital=final,
        total_return_pct=total_ret,
        n_trades=n_trades,
        dispatch_mode=mode,
    )


def run_legacy_python(stocks_data, options_data, stocks, rebalance_freq, runs) -> BenchResult:
    """Legacy Backtest class."""
    times = []
    bt = None
    for _ in range(runs):
        sd = TiingoData.__new__(TiingoData)
        sd.__dict__.update(stocks_data.__dict__)
        sd._data = stocks_data._data.copy()

        od = HistoricalOptionsData.__new__(HistoricalOptionsData)
        od.__dict__.update(options_data.__dict__)
        od._data = options_data._data.copy()

        bt = LegacyBacktest({"stocks": 0.97, "options": 0.03, "cash": 0})
        bt.stocks = stocks
        bt.stocks_data = sd
        bt.options_data = od
        bt.options_strategy = _buy_strategy(od.schema)

        t0 = time.perf_counter()
        bt.run(rebalance_freq=rebalance_freq)
        times.append(time.perf_counter() - t0)

    assert bt is not None
    final = float(bt.balance["total capital"].iloc[-1])
    n_trades = len(bt.trade_log) if not bt.trade_log.empty else 0
    total_ret = (final / bt.initial_capital - 1) * 100
    return BenchResult(
        name="Legacy Python Backtest",
        runtime_s=float(np.mean(times)),
        final_capital=final,
        total_return_pct=total_ret,
        n_trades=n_trades,
        dispatch_mode="python-legacy",
    )


def run_bt_stock_only(stocks_file, symbols, weights, initial_capital, runs) -> BenchResult | None:
    """bt library stock-only benchmark."""
    try:
        import bt
    except Exception:
        return None

    prices = pd.read_csv(stocks_file, parse_dates=["date"])
    prices = prices[prices["symbol"].isin(symbols)].copy()
    px = prices.pivot(index="date", columns="symbol", values="adjClose").sort_index().dropna()
    px = px[symbols]

    times = []
    last_res = None
    for _ in range(runs):
        algos = [
            bt.algos.RunMonthly(),
            bt.algos.SelectThese(symbols),
            bt.algos.WeighSpecified(**dict(zip(symbols, weights))),
            bt.algos.Rebalance(),
        ]
        strat = bt.Strategy("bench", algos)
        test = bt.Backtest(strat, px, initial_capital=initial_capital)
        t0 = time.perf_counter()
        last_res = bt.run(test)
        times.append(time.perf_counter() - t0)

    assert last_res is not None
    series = last_res.prices.iloc[:, 0]
    # bt normalizes NAV to start at initial_capital
    final = float(series.iloc[-1])
    start = float(series.iloc[0])
    total_ret = (final / start - 1) * 100
    return BenchResult(
        name="bt library",
        runtime_s=float(np.mean(times)),
        final_capital=final,
        total_return_pct=total_ret,
        n_trades=0,
        dispatch_mode="bt",
    )


def run_ob_stock_only(stocks_file, symbols, weights, initial_capital, runs) -> BenchResult:
    """options_portfolio_backtester stock-only benchmark."""
    stocks = [Stock(sym, w) for sym, w in zip(symbols, weights)]
    times = []
    bt_obj = None
    for _ in range(runs):
        stocks_data = TiingoData(stocks_file)
        bt_obj = LegacyBacktest({"stocks": 1.0, "options": 0.0, "cash": 0.0},
                                initial_capital=int(initial_capital))
        bt_obj.stocks = stocks
        bt_obj.stocks_data = stocks_data
        t0 = time.perf_counter()
        bt_obj.run(rebalance_freq=1, rebalance_unit="BMS")
        times.append(time.perf_counter() - t0)

    assert bt_obj is not None
    bal = bt_obj.balance["total capital"].dropna()
    final = float(bal.iloc[-1])
    total_ret = (final / initial_capital - 1) * 100
    return BenchResult(
        name="options_portfolio_backtester (stock-only)",
        runtime_s=float(np.mean(times)),
        final_capital=final,
        total_return_pct=total_ret,
        n_trades=0,
        dispatch_mode="python-legacy-stock-only",
    )


# ---------------------------------------------------------------------------
# Display
# ---------------------------------------------------------------------------

def print_result(r: BenchResult) -> None:
    print(f"  {r.name}")
    print(f"    dispatch:     {r.dispatch_mode}")
    print(f"    runtime:      {r.runtime_s:.4f}s")
    print(f"    final_capital: {r.final_capital:,.2f}")
    print(f"    total_return: {r.total_return_pct:.4f}%")
    print(f"    n_trades:     {r.n_trades}")


def print_comparison(a: BenchResult, b: BenchResult) -> None:
    speedup = b.runtime_s / a.runtime_s if a.runtime_s > 0 else float("nan")
    cap_delta = abs(a.final_capital - b.final_capital)
    ret_delta = a.total_return_pct - b.total_return_pct
    print(f"  {a.name} vs {b.name}:")
    print(f"    speedup:       {speedup:.2f}x ({a.name} is {'faster' if speedup > 1 else 'slower'})")
    print(f"    capital delta: ${cap_delta:,.2f}")
    print(f"    return delta:  {ret_delta:+.4f} pct-pts")
    if a.n_trades > 0 and b.n_trades > 0:
        print(f"    trades match:  {a.n_trades == b.n_trades} ({a.n_trades} vs {b.n_trades})")


def main() -> None:
    args = parse_args()
    stocks_data, options_data, stocks_file = _load_data(args.use_prod_data)
    stocks = _stocks(use_prod=args.use_prod_data)

    print(f"\n{'='*60}")
    print("Benchmark: Rust vs Python vs Legacy")
    print(f"{'='*60}")
    print(f"  Rust available: {use_rust()}")
    print(f"  runs per backend: {args.runs}")
    print(f"  rebalance_freq: {args.rebalance_freq}")
    print(f"  data: {'production' if args.use_prod_data else 'test'}")
    print()

    # -- Options backtest benchmarks --
    print("--- Options Backtest (with options data) ---")
    results = []

    legacy = run_legacy_python(stocks_data, options_data, stocks, args.rebalance_freq, args.runs)
    results.append(legacy)
    print_result(legacy)

    python_engine = run_engine_python(stocks_data, options_data, stocks, args.rebalance_freq, args.runs)
    results.append(python_engine)
    print_result(python_engine)

    rust_engine = run_engine_rust(stocks_data, options_data, stocks, args.rebalance_freq, args.runs)
    if rust_engine:
        results.append(rust_engine)
        print_result(rust_engine)
    else:
        print("  Rust BacktestEngine: SKIPPED (Rust not available)")

    print()
    print("--- Comparisons ---")
    if rust_engine:
        print_comparison(rust_engine, python_engine)
        print_comparison(rust_engine, legacy)
    print_comparison(python_engine, legacy)

    # -- Stock-only benchmarks --
    if args.stock_only and Path(PROD_STOCKS_FILE).exists():
        print()
        print("--- Stock-Only Monthly Rebalance (vs bt) ---")
        symbols = ["SPY"]
        weights = [1.0]
        capital = 1_000_000.0

        ob_stock = run_ob_stock_only(PROD_STOCKS_FILE, symbols, weights, capital, args.runs)
        print_result(ob_stock)

        bt_res = run_bt_stock_only(PROD_STOCKS_FILE, symbols, weights, capital, args.runs)
        if bt_res:
            print_result(bt_res)
            print()
            print_comparison(ob_stock, bt_res)
        else:
            print("  bt: SKIPPED (not installed)")

    print()
    print("Done.")


if __name__ == "__main__":
    main()


================================================
FILE: benchmarks/benchmark_sweep.py
================================================
"""Benchmark: Rust parallel_sweep vs Python sequential grid search.

This is the PRIMARY benchmark for justifying the Rust backend.
Single backtests have Pandas<->Polars conversion overhead, but
parallel_sweep amortizes that cost over N grid points and runs
all backtests on Rayon threads (no GIL, no pickle, zero-copy data).

Usage:
    python scripts/benchmark_sweep.py
    python scripts/benchmark_sweep.py --grid-sizes 10 50 100 --runs 3
"""

from __future__ import annotations

import argparse
import gc
import math
import os
import sys
import time
from pathlib import Path

import numpy as np
import pandas as pd
import polars as pl

REPO_ROOT = Path(__file__).resolve().parents[1]

from options_portfolio_backtester.data.providers import HistoricalOptionsData, TiingoData
from options_portfolio_backtester.core.types import Direction, Stock, OptionType as Type
from options_portfolio_backtester.strategy.strategy import Strategy
from options_portfolio_backtester.strategy.strategy_leg import StrategyLeg

from options_portfolio_backtester.engine.engine import BacktestEngine
from options_portfolio_backtester.engine._dispatch import use_rust, rust
from options_portfolio_backtester.engine import _dispatch as _rust_dispatch
from options_portfolio_backtester.execution.cost_model import NoCosts

TEST_DIR = os.path.join(REPO_ROOT, "backtester", "test")
STOCKS_FILE = os.path.join(TEST_DIR, "test_data", "ivy_5assets_data.csv")
OPTIONS_FILE = os.path.join(TEST_DIR, "test_data", "options_data.csv")
PROD_STOCKS_FILE = os.path.join(REPO_ROOT, "data", "processed", "stocks.csv")
PROD_OPTIONS_FILE = os.path.join(REPO_ROOT, "data", "processed", "options.csv")


def parse_args():
    p = argparse.ArgumentParser(description="Benchmark Rust parallel_sweep vs Python sequential")
    p.add_argument("--grid-sizes", nargs="+", type=int, default=[5, 10, 25, 50],
                   help="Grid sizes to test (number of parameter combos)")
    p.add_argument("--runs", type=int, default=2, help="Timing runs per grid size")
    p.add_argument("--use-prod-data", action="store_true", help="Use production data")
    return p.parse_args()


def _load_data(use_prod: bool):
    if use_prod and Path(PROD_STOCKS_FILE).exists() and Path(PROD_OPTIONS_FILE).exists():
        sf, of = PROD_STOCKS_FILE, PROD_OPTIONS_FILE
    else:
        sf, of = STOCKS_FILE, OPTIONS_FILE

    stocks_data = TiingoData(sf)
    options_data = HistoricalOptionsData(of)

    if sf == STOCKS_FILE:
        stocks_data._data["adjClose"] = 10
        options_data._data.at[2, "ask"] = 1
        options_data._data.at[2, "bid"] = 0.5
        options_data._data.at[51, "ask"] = 1.5
        options_data._data.at[50, "bid"] = 0.5
        options_data._data.at[130, "bid"] = 0.5
        options_data._data.at[131, "bid"] = 1.5
        options_data._data.at[206, "bid"] = 0.5
        options_data._data.at[207, "bid"] = 1.5

    return stocks_data, options_data, sf


def _build_param_grid(n: int, underlying: str = "SPX") -> list[dict]:
    """Generate n parameter override dicts varying DTE thresholds.

    Returns list of dicts with both Rust filter strings AND raw dte values
    so both the Rust and Python paths can use the same grid.
    """
    dte_mins = np.linspace(20, 90, max(int(n**0.5), 2)).astype(int)
    dte_exits = np.linspace(5, 45, max(int(n / len(dte_mins)) + 1, 2)).astype(int)
    grid = []
    for dmin in dte_mins:
        for dex in dte_exits:
            if len(grid) >= n:
                break
            grid.append({
                "label": f"dte_min={dmin}_exit={dex}",
                "leg_entry_filters": [
                    f"(underlying == '{underlying}') & (dte >= {dmin})",
                ],
                "leg_exit_filters": [
                    f"dte <= {dex}",
                ],
                # Raw params for Python path
                "_dte_min": int(dmin),
                "_dte_exit": int(dex),
                "_underlying": underlying,
            })
        if len(grid) >= n:
            break
    return grid[:n]


def _build_rust_config(stocks_data, options_data, stocks, underlying="SPX"):
    """Build the config dict for rust.parallel_sweep."""
    schema = options_data.schema
    strat = Strategy(schema)
    leg = StrategyLeg("leg_1", schema, option_type=Type.PUT, direction=Direction.BUY)
    leg.entry_filter = (schema.underlying == underlying) & (schema.dte >= 60)
    leg.exit_filter = schema.dte <= 30
    strat.add_legs([leg])

    date_fmt = "%Y-%m-%d %H:%M:%S"
    dates_df = (
        pd.DataFrame(options_data._data[["quotedate", "volume"]])
        .drop_duplicates("quotedate")
        .set_index("quotedate")
    )
    rebalancing_days = pd.to_datetime(
        dates_df.groupby(pd.Grouper(freq="1BMS"))
        .apply(lambda x: x.index.min())
        .values
    )
    rb_dates = [d.strftime(date_fmt) for d in rebalancing_days]

    config = {
        "allocation": {"stocks": 0.97, "options": 0.03, "cash": 0.0},
        "initial_capital": 1_000_000.0,
        "shares_per_contract": 100,
        "rebalance_dates": rb_dates,
        "legs": [{
            "name": leg.name,
            "entry_filter": leg.entry_filter.query,
            "exit_filter": leg.exit_filter.query,
            "direction": leg.direction.value,
            "type": leg.type.value,
            "entry_sort_col": None,
            "entry_sort_asc": True,
        }],
        "profit_pct": None,
        "loss_pct": None,
        "stocks": [(s.symbol, s.percentage) for s in stocks],
    }

    stocks_schema = stocks_data.schema
    opts_schema = options_data.schema
    schema_mapping = {
        "contract": opts_schema["contract"],
        "date": opts_schema["date"],
        "stocks_date": stocks_schema["date"],
        "stocks_symbol": stocks_schema["symbol"],
        "stocks_price": stocks_schema["adjClose"],
        "underlying": opts_schema["underlying"],
        "expiration": opts_schema["expiration"],
        "type": opts_schema["type"],
        "strike": opts_schema["strike"],
    }

    # Convert datetime columns
    opts_copy = options_data._data.copy()
    for c in [opts_schema["date"], opts_schema["expiration"]]:
        if c in opts_copy.columns and pd.api.types.is_datetime64_any_dtype(opts_copy[c]):
            opts_copy[c] = opts_copy[c].dt.strftime(date_fmt)

    stocks_copy = stocks_data._data.copy()
    sc = stocks_schema["date"]
    if sc in stocks_copy.columns and pd.api.types.is_datetime64_any_dtype(stocks_copy[sc]):
        stocks_copy[sc] = stocks_copy[sc].dt.strftime(date_fmt)

    opts_pl = pl.from_pandas(opts_copy)
    stocks_pl = pl.from_pandas(stocks_copy)

    return config, schema_mapping, opts_pl, stocks_pl, strat


def run_rust_sweep(opts_pl, stocks_pl, config, schema_mapping, param_grid, runs):
    """Run Rust parallel_sweep."""
    times = []
    last_results = None
    for _ in range(runs):
        gc.collect()
        t0 = time.perf_counter()
        results = rust.parallel_sweep(
            opts_pl, stocks_pl, config, schema_mapping, param_grid, None,
        )
        elapsed = time.perf_counter() - t0
        times.append(elapsed)
        last_results = results
    return times, last_results


def run_python_sequential(stocks_data, options_data, stocks, param_grid, runs, underlying="SPX"):
    """Run sequential Python backtests for same grid."""
    times = []
    last_results = None
    for _ in range(runs):
        gc.collect()
        t0 = time.perf_counter()
        results = []
        for params in param_grid:
            sd = TiingoData.__new__(TiingoData)
            sd.__dict__.update(stocks_data.__dict__)
            sd._data = stocks_data._data.copy()

            od = HistoricalOptionsData.__new__(HistoricalOptionsData)
            od.__dict__.update(options_data.__dict__)
            od._data = options_data._data.copy()

            schema = od.schema
            strat = Strategy(schema)
            leg = StrategyLeg("leg_1", schema, option_type=Type.PUT, direction=Direction.BUY)

            # Construct filters from raw DTE params
            dte_min = params.get("_dte_min", 60)
            dte_exit = params.get("_dte_exit", 30)
            und = params.get("_underlying", underlying)
            leg.entry_filter = (schema.underlying == und) & (schema.dte >= dte_min)
            leg.exit_filter = schema.dte <= dte_exit
            strat.add_legs([leg])

            engine = BacktestEngine(
                {"stocks": 0.97, "options": 0.03, "cash": 0},
                cost_model=NoCosts(),
            )
            engine.stocks = stocks
            engine.stocks_data = sd
            engine.options_data = od
            engine.options_strategy = strat

            saved = _rust_dispatch.RUST_AVAILABLE
            _rust_dispatch.RUST_AVAILABLE = False
            try:
                engine.run(rebalance_freq=1)
            finally:
                _rust_dispatch.RUST_AVAILABLE = saved

            final = float(engine.balance["total capital"].iloc[-1])
            n_trades = len(engine.trade_log) if not engine.trade_log.empty else 0
            results.append({
                "label": params.get("label", ""),
                "final_capital": final,
                "total_trades": n_trades,
            })

        elapsed = time.perf_counter() - t0
        times.append(elapsed)
        last_results = results
    return times, last_results


def run_rust_single(opts_pl, stocks_pl, config, schema_mapping, runs):
    """Run a single Rust backtest (for overhead measurement)."""
    times = []
    for _ in range(runs):
        gc.collect()
        t0 = time.perf_counter()
        rust.run_backtest_py(opts_pl, stocks_pl, config, schema_mapping)
        elapsed = time.perf_counter() - t0
        times.append(elapsed)
    return times


def run_python_single(stocks_data, options_data, stocks, runs, underlying="SPX"):
    """Run a single Python backtest."""
    times = []
    for _ in range(runs):
        sd = TiingoData.__new__(TiingoData)
        sd.__dict__.update(stocks_data.__dict__)
        sd._data = stocks_data._data.copy()

        od = HistoricalOptionsData.__new__(HistoricalOptionsData)
        od.__dict__.update(options_data.__dict__)
        od._data = options_data._data.copy()

        engine = BacktestEngine(
            {"stocks": 0.97, "options": 0.03, "cash": 0},
            cost_model=NoCosts(),
        )
        engine.stocks = stocks
        engine.stocks_data = sd
        engine.options_data = od
        schema = od.schema
        strat = Strategy(schema)
        leg = StrategyLeg("leg_1", schema, option_type=Type.PUT, direction=Direction.BUY)
        leg.entry_filter = (schema.underlying == underlying) & (schema.dte >= 60)
        leg.exit_filter = schema.dte <= 30
        strat.add_legs([leg])
        engine.options_strategy = strat

        saved = _rust_dispatch.RUST_AVAILABLE
        _rust_dispatch.RUST_AVAILABLE = False
        try:
            gc.collect()
            t0 = time.perf_counter()
            engine.run(rebalance_freq=1)
            elapsed = time.perf_counter() - t0
        finally:
            _rust_dispatch.RUST_AVAILABLE = saved
        times.append(elapsed)
    return times


def main():
    args = parse_args()

    if not use_rust():
        print("ERROR: Rust extension not available. Build with: make rust-build")
        sys.exit(1)

    stocks_data, options_data, sf = _load_data(args.use_prod_data)
    if args.use_prod_data and Path(PROD_STOCKS_FILE).exists():
        stocks = [Stock("SPY", 1.0)]
    else:
        stocks = [Stock("VTI", 0.2), Stock("VEU", 0.2), Stock("BND", 0.2),
                  Stock("VNQ", 0.2), Stock("DBC", 0.2)]
    underlying = "SPY" if args.use_prod_data else "SPX"
    n_rows = len(options_data._data)
    n_dates = options_data._data["quotedate"].nunique()

    print(f"\n{'='*65}")
    print("Benchmark: Rust parallel_sweep vs Python sequential")
    print(f"{'='*65}")
    print(f"  Data: {'production' if args.use_prod_data else 'test'} ({n_rows:,} options rows, {n_dates} dates)")
    print(f"  Underlying: {underlying}")
    print(f"  Grid sizes: {args.grid_sizes}")
    print(f"  Runs per test: {args.runs}")
    print(f"  CPU cores: {os.cpu_count()}")
    print()

    # Build Rust config once (amortized over all grid sizes)
    config, schema_mapping, opts_pl, stocks_pl, strat = _build_rust_config(
        stocks_data, options_data, stocks, underlying=underlying
    )

    # -- Single backtest comparison --
    print("--- Single Backtest (1 run) ---")
    rust_single = run_rust_single(opts_pl, stocks_pl, config, schema_mapping, args.runs)
    python_single = run_python_single(stocks_data, options_data, stocks, args.runs, underlying=underlying)
    rust_avg = np.mean(rust_single)
    py_avg = np.mean(python_single)
    print(f"  Rust single:    {rust_avg:.4f}s (per-run: [{', '.join(f'{t:.4f}s' for t in rust_single)}])")
    print(f"  Python single:  {py_avg:.4f}s (per-run: [{', '.join(f'{t:.4f}s' for t in python_single)}])")
    print(f"  Speedup:        {py_avg/rust_avg:.2f}x {'(Rust faster)' if rust_avg < py_avg else '(Python faster)'}")
    print()

    # -- Grid sweep comparison --
    print("--- Grid Sweep (N parallel Rust vs N sequential Python) ---")
    rows = []
    for grid_size in args.grid_sizes:
        param_grid = _build_param_grid(grid_size, underlying=underlying)

        print(f"\n  Grid size: {grid_size}")

        # Rust parallel_sweep
        rust_times, rust_results = run_rust_sweep(
            opts_pl, stocks_pl, config, schema_mapping, param_grid, args.runs
        )
        rust_avg = np.mean(rust_times)
        print(f"    Rust parallel:  {rust_avg:.4f}s (per-run: [{', '.join(f'{t:.4f}s' for t in rust_times)}])")

        # Python sequential
        python_times, python_results = run_python_sequential(
            stocks_data, options_data, stocks, param_grid, args.runs, underlying=underlying
        )
        py_avg = np.mean(python_times)
        print(f"    Python seq:     {py_avg:.4f}s (per-run: [{', '.join(f'{t:.4f}s' for t in python_times)}])")

        speedup = py_avg / rust_avg if rust_avg > 0 else float("nan")
        throughput_rust = grid_size / rust_avg if rust_avg > 0 else 0
        throughput_py = grid_size / py_avg if py_avg > 0 else 0
        print(f"    Speedup:        {speedup:.2f}x {'(Rust faster)' if speedup > 1 else '(Python faster)'}")
        print(f"    Throughput:     Rust={throughput_rust:.1f}/s, Python={throughput_py:.1f}/s")

        rows.append({
            "Grid": grid_size,
            "Rust (s)": f"{rust_avg:.4f}",
            "Python (s)": f"{py_avg:.4f}",
            "Speedup": f"{speedup:.2f}x",
            "Rust runs/s": f"{throughput_rust:.1f}",
            "Python runs/s": f"{throughput_py:.1f}",
        })

    # -- Summary Table --
    print(f"\n{'='*65}")
    print("Summary")
    print(f"{'='*65}")
    df = pd.DataFrame(rows)
    print(df.to_string(index=False))

    print(f"\n{'='*65}")
    print("Conclusion")
    print(f"{'='*65}")
    if rows:
        final_speedup = float(rows[-1]["Speedup"].replace("x", ""))
        if final_speedup > 1:
            print(f"  Rust parallel_sweep is {final_speedup:.1f}x faster for {args.grid_sizes[-1]} grid points.")
            print(f"  For optimization/grid search, Rust + Rayon provides real value.")
        else:
            print(f"  Rust is {1/final_speedup:.1f}x slower even for parallel sweep.")
            print(f"  The Pandas<->Polars conversion overhead dominates.")

    single_speedup = np.mean(python_single) / np.mean(rust_single)
    if single_speedup < 1:
        print(f"  Single backtest: Rust is {1/single_speedup:.1f}x SLOWER (conversion overhead).")
    else:
        print(f"  Single backtest: Rust is {single_speedup:.1f}x faster.")

    print("\nDone.")


if __name__ == "__main__":
    main()


================================================
FILE: benchmarks/compare_with_bt.py
================================================
"""Head-to-head comparison: options_portfolio_backtester stock-only mode vs bt.

This harness runs the same monthly stock-rebalance policy in both frameworks:
- options_portfolio_backtester (legacy Backtest with options allocation = 0)
- bt (if installed)

Outputs a small scorecard with performance and runtime metrics.
"""

from __future__ import annotations

import argparse
import sys
import time
from dataclasses import dataclass
from pathlib import Path

import numpy as np
import pandas as pd

REPO_ROOT = Path(__file__).resolve().parents[1]

from options_portfolio_backtester import BacktestEngine as Backtest
from options_portfolio_backtester.data.providers import TiingoData
from options_portfolio_backtester.core.types import Stock


@dataclass
class RunResult:
    name: str
    total_return_pct: float
    cagr_pct: float
    max_drawdown_pct: float
    vol_annual_pct: float
    sharpe: float
    runtime_s: float
    start_date: str
    end_date: str
    n_days: int
    equity: pd.Series


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Compare options_portfolio_backtester vs bt on stock-only allocation.")
    parser.add_argument("--stocks-file", default="data/processed/stocks.csv")
    parser.add_argument("--options-file", default="data/processed/options.csv")
    parser.add_argument("--symbols", default="SPY", help="Comma-separated symbols. Example: SPY or SPY,TLT,GLD")
    parser.add_argument("--weights", default=None, help="Comma-separated weights matching symbols. Defaults equal.")
    parser.add_argument("--initial-capital", type=float, default=1_000_000.0)
    parser.add_argument("--rebalance-months", type=int, default=1, help="Business-month-start rebalance frequency.")
    parser.add_argument("--runs", type=int, default=3, help="Runtime averaging repeats.")
    return parser.parse_args()


def normalize_weights(symbols: list[str], raw_weights: str | None) -> list[float]:
    if raw_weights is None:
        return [1.0 / len(symbols)] * len(symbols)
    vals = [float(x) for x in raw_weights.split(",")]
    if len(vals) != len(symbols):
        raise ValueError("--weights length must match --symbols length")
    total = float(sum(vals))
    if total <= 0:
        raise ValueError("--weights must sum to > 0")
    return [v / total for v in vals]


def compute_metrics(total_capital: pd.Series) -> tuple[float, float, float, float, float]:
    total_capital = total_capital.dropna()
    if total_capital.empty:
        return 0.0, 0.0, 0.0, 0.0, 0.0

    rets = total_capital.pct_change().dropna()
    total_return = total_capital.iloc[-1] / total_capital.iloc[0] - 1.0
    n_years = len(total_capital) / 252.0
    cagr = (total_capital.iloc[-1] / total_capital.iloc[0]) ** (1.0 / n_years) - 1.0 if n_years > 0 else 0.0

    peak = total_capital.cummax()
    dd = total_capital / peak - 1.0
    max_dd = float(dd.min()) if not dd.empty else 0.0

    vol = float(rets.std(ddof=1) * np.sqrt(252)) if len(rets) > 1 else 0.0
    sharpe = float((rets.mean() / rets.std(ddof=1)) * np.sqrt(252)) if len(rets) > 1 and rets.std(ddof=1) > 0 else 0.0
    return total_return, cagr, max_dd, vol, sharpe


def run_options_portfolio_backtester(
    stocks_file: str,
    symbols: list[str],
    weights: list[float],
    initial_capital: float,
    rebalance_months: int,
    runs: int,
) -> RunResult:
    stocks_data = TiingoData(stocks_file)

    stocks = [Stock(sym, w) for sym, w in zip(symbols, weights)]
    times: list[float] = []
    bt_obj = None
    for _ in range(runs):
        bt = Backtest({"stocks": 1.0, "options": 0.0, "cash": 0.0}, initial_capital=int(initial_capital))
        bt.stocks = stocks
        bt.stocks_data = stocks_data

        t0 = time.perf_counter()
        bt.run(rebalance_freq=rebalance_months, rebalance_unit="BMS")
        times.append(time.perf_counter() - t0)
        bt_obj = bt

    assert bt_obj is not None
    bal = bt_obj.balance["total capital"].dropna()
    tr, cagr, mdd, vol, sharpe = compute_metrics(bal)
    return RunResult(
        name="options_portfolio_backtester",
        total_return_pct=tr * 100.0,
        cagr_pct=cagr * 100.0,
        max_drawdown_pct=mdd * 100.0,
        vol_annual_pct=vol * 100.0,
        sharpe=sharpe,
        runtime_s=float(np.mean(times)),
        start_date=str(bal.index.min().date()),
        end_date=str(bal.index.max().date()),
        n_days=int(len(bal)),
        equity=bal,
    )


def run_bt(
    stocks_file: str,
    symbols: list[str],
    weights: list[float],
    initial_capital: float,
    runs: int,
) -> RunResult | None:
    try:
        import bt  # type: ignore
    except Exception:
        return None

    prices = pd.read_csv(stocks_file, parse_dates=["date"])
    prices = prices[prices["symbol"].isin(symbols)].copy()
    px = prices.pivot(index="date", columns="symbol", values="adjClose").sort_index().dropna()
    px = px[symbols]

    times: list[float] = []
    last_res = None
    for _ in range(runs):
        algos = [
            bt.algos.RunMonthly(),
            bt.algos.SelectThese(symbols),
            bt.algos.WeighSpecified(**{s: w for s, w in zip(symbols, weights)}),
            bt.algos.Rebalance(),
        ]
        strat = bt.Strategy("bt_monthly_rebal", algos)
        test = bt.Backtest(strat, px, initial_capital=initial_capital)

        t0 = time.perf_counter()
        last_res = bt.run(test)
        times.append(time.perf_counter() - t0)

    assert last_res is not None
    series = last_res.prices.iloc[:, 0]
    tr, cagr, mdd, vol, sharpe = compute_metrics(series)
    return RunResult(
        name="bt",
        total_return_pct=tr * 100.0,
        cagr_pct=cagr * 100.0,
        max_drawdown_pct=mdd * 100.0,
        vol_annual_pct=vol * 100.0,
        sharpe=sharpe,
        runtime_s=float(np.mean(times)),
        start_date=str(series.index.min().date()),
        end_date=str(series.index.max().date()),
        n_days=int(len(series)),
        equity=series,
    )


def print_result(r: RunResult) -> None:
    print(f"{r.name}")
    print(f"  period: {r.start_date} -> {r.end_date} ({r.n_days} rows)")
    print(f"  total_return: {r.total_return_pct:8.2f}%")
    print(f"  cagr:         {r.cagr_pct:8.2f}%")
    print(f"  max_drawdown: {r.max_drawdown_pct:8.2f}%")
    print(f"  vol_annual:   {r.vol_annual_pct:8.2f}%")
    print(f"  sharpe:       {r.sharpe:8.3f}")
    print(f"  runtime:      {r.runtime_s:8.4f}s")


def print_overlap_parity(a: RunResult, b: RunResult) -> None:
    common = a.equity.index.intersection(b.equity.index)
    if len(common) == 0:
        print("  overlap: none")
        return

    a_n = a.equity.loc[common] / a.equity.loc[common].iloc[0]
    b_n = b.equity.loc[common] / b.equity.loc[common].iloc[0]
    diff = a_n - b_n
    print(f"  overlap rows: {len(common)}")
    print(f"  overlap end delta: {float(diff.iloc[-1]):.6e}")
    print(f"  overlap max abs delta: {float(diff.abs().max()):.6e}")


def main() -> None:
    args = parse_args()
    symbols = [s.strip().upper() for s in args.symbols.split(",") if s.strip()]
    if not symbols:
        raise ValueError("No symbols provided")
    weights = normalize_weights(symbols, args.weights)

    for file_path in (args.stocks_file,):
        if not Path(file_path).exists():
            raise FileNotFoundError(f"Missing file: {file_path}")

    ob = run_options_portfolio_backtester(
        stocks_file=args.stocks_file,
        symbols=symbols,
        weights=weights,
        initial_capital=args.initial_capital,
        rebalance_months=args.rebalance_months,
        runs=args.runs,
    )
    bt_res = run_bt(
        stocks_file=args.stocks_file,
        symbols=symbols,
        weights=weights,
        initial_capital=args.initial_capital,
        runs=args.runs,
    )

    print("\n=== Comparison Scorecard ===")
    print_result(ob)
    if bt_res is None:
        print("\nbt")
        print("  not available (module 'bt' is not installed in this environment).")
        print("  install in nix shell and rerun:")
        print("    pip install bt")
    else:
        print()
        print_result(bt_res)
        speedup = bt_res.runtime_s / ob.runtime_s if ob.runtime_s > 0 else float("nan")
        print("\nsummary")
        print(f"  speed ratio (bt / options_portfolio_backtester): {speedup:0.2f}x")
        print(
            f"  return delta (options_portfolio_backtester - bt): "
            f"{(ob.total_return_pct - bt_res.total_return_pct):0.2f} pct-pts"
        )
        print(
            f"  maxDD delta (options_portfolio_backtester - bt): "
            f"{(ob.max_drawdown_pct - bt_res.max_drawdown_pct):0.2f} pct-pts"
        )
        print("  overlap parity:")
        print_overlap_parity(ob, bt_res)


if __name__ == "__main__":
    main()


================================================
FILE: data/README.md
================================================
# Data Scripts

Scripts for fetching and converting market data into the formats expected by the backtester.

## Quick Start

Fetch both stock and options data for SPY, aligned by date:

```bash
python data/fetch_data.py all --symbols SPY --start 2020-01-01 --end 2023-01-01
```

Data is first fetched from the [self-hosted GitHub Release](https://github.com/lambdaclass/options_backtester/releases/tag/data-v1), falling back to [philippdubach/options-data](https://github.com/philippdubach/options-data) CDN and yfinance. Outputs:
- `data/processed/stocks.csv` — Tiingo-format stock data
- `data/processed/options.csv` — options data with Greeks

## Subcommands

```bash
# Stocks only (GitHub Release > options-data > yfinance)
python data/fetch_data.py stocks --symbols SPY --start 2020-01-01 --end 2023-01-01

# Options only
python data/fetch_data.py options --symbols SPY --start 2020-01-01 --end 2023-01-01

# Both + date alignment (default)
python data/fetch_data.py all --symbols SPY --start 2020-01-01 --end 2023-01-01

# Multiple symbols
python data/fetch_data.py all --symbols SPY IWM QQQ --start 2020-01-01 --end 2023-01-01

# Custom output paths
python data/fetch_data.py all --symbols SPY --start 2020-01-01 --end 2023-01-01 \
    --stocks-output data/processed/spy_stocks.csv \
    --options-output data/processed/spy_options.csv

# Force re-download (skip cache)
python data/fetch_data.py all --symbols SPY --start 2020-01-01 --end 2023-01-01 --force
```

## OptionsDX Conversion (separate)

For SPX index options from [optionsdx.com](https://www.optionsdx.com/):

```bash
python data/convert_optionsdx.py data/raw/spx_eod_2020.csv --output data/processed/spx_options.csv
```

## Loading Data in the Backtester

```python
from backtester.datahandler import HistoricalOptionsData, TiingoData

options = HistoricalOptionsData("data/processed/options.csv")
stocks = TiingoData("data/processed/stocks.csv")
```

The `all` subcommand automatically aligns stock and option dates so the backtester's `np.array_equal` assertion passes.

## Directory Structure

- `raw/` — Cached parquet downloads (gitignored)
- `processed/` — Converted CSV output ready for the backtester (gitignored)


================================================
FILE: data/convert_optionsdx.py
================================================
#!/usr/bin/env python3
"""Convert OptionsDX wide-format CSV to backtester long-format CSV.

OptionsDX provides one row per strike/date/expiry with both call and put data
in wide format. The backtester expects one row per contract in long format.

Usage:
    python data/convert_optionsdx.py data/raw/spx_eod.csv --output data/processed/spx_options.csv
"""

import argparse
import sys

import pandas as pd


# OptionsDX columns we need (they have trailing spaces, stripped on read)
CALL_COLS = {
    "C_BID": "bid",
    "C_ASK": "ask",
    "C_LAST": "last",
    "C_VOLUME": "volume",
    "C_IV": "impliedvol",
    "C_DELTA": "delta",
    "C_GAMMA": "gamma",
    "C_THETA": "theta",
    "C_VEGA": "vega",
}

PUT_COLS = {
    "P_BID": "bid",
    "P_ASK": "ask",
    "P_LAST": "last",
    "P_VOLUME": "volume",
    "P_IV": "impliedvol",
    "P_DELTA": "delta",
    "P_GAMMA": "gamma",
    "P_THETA": "theta",
    "P_VEGA": "vega",
}

OUTPUT_COLUMNS = [
    "underlying",
    "underlying_last",
    "optionroot",
    "type",
    "expiration",
    "quotedate",
    "strike",
    "last",
    "bid",
    "ask",
    "volume",
    "openinterest",
    "impliedvol",
    "delta",
    "gamma",
    "theta",
    "vega",
    "optionalias",
]


def make_optionroot(expire_dates, option_type, strikes):
    """Generate OCC-format option root symbols vectorized.

    Format: SPX{YYMMDD}{C|P}{strike*1000:08d}
    Example: SPX170317C00300000
    """
    date_str = expire_dates.dt.strftime("%y%m%d")
    type_char = "C" if option_type == "call" else "P"
    strike_str = (strikes * 1000).astype(int).astype(str).str.zfill(8)
    return "SPX" + date_str + type_char + strike_str


def convert(input_path, output_path):
    df = pd.read_csv(input_path, parse_dates=["QUOTE_DATE", "EXPIRE_DATE"])
    # Strip whitespace from column names (OptionsDX CSVs have trailing spaces)
    df.columns = df.columns.str.strip()

    shared = {
        "underlying": "SPX",
        "underlying_last": df["UNDERLYING_LAST"],
        "expiration": df["EXPIRE_DATE"],
        "quotedate": df["QUOTE_DATE"],
        "strike": df["STRIKE"],
        "openinterest": 0,
    }

    # Build call rows
    calls = pd.DataFrame(shared)
    calls["type"] = "call"
    for src, dst in CALL_COLS.items():
        calls[dst] = df[src].values
    calls["optionroot"] = make_optionroot(df["EXPIRE_DATE"], "call", df["STRIKE"])
    calls["optionalias"] = calls["optionroot"]

    # Build put rows
    puts = pd.DataFrame(shared)
    puts["type"] = "put"
    for src, dst in PUT_COLS.items():
        puts[dst] = df[src].values
    puts["optionroot"] = make_optionroot(df["EXPIRE_DATE"], "put", df["STRIKE"])
    puts["optionalias"] = puts["optionroot"]

    result = pd.concat([calls, puts], ignore_index=True)
    result = result[OUTPUT_COLUMNS]
    result = result.sort_values(["quotedate", "expiration", "strike", "type"])
    result.to_csv(output_path, index=False)
    print(f"Wrote {len(result)} rows to {output_path}")


def main():
    parser = argparse.ArgumentParser(
        description="Convert OptionsDX wide CSV to backtester long CSV"
    )
    parser.add_argument("input", help="Path to OptionsDX CSV file")
    parser.add_argument(
        "--output",
        default="data/processed/spx_options.csv",
        help="Output path (default: data/processed/spx_options.csv)",
    )
    args = parser.parse_args()
    convert(args.input, args.output)


if __name__ == "__main__":
    main()


================================================
FILE: data/fetch_data.py
================================================
#!/usr/bin/env python3
"""Unified data fetch script for the options backtester.

Downloads stock and options data, converts to backtester CSV formats,
and aligns dates between datasets.

Download priority (for each symbol):
  1. Self-hosted GitHub Release (lambdaclass/options_backtester data-v1)
  2. philippdubach/options-data CDN — 104 symbols
  3. philippdubach/options-dataset-hist — SPY/IWM/QQQ underlying prices
  4. yfinance (last resort, stocks only)

Usage:
    python data/fetch_data.py all --symbols SPY --start 2020-01-01 --end 2023-01-01
    python data/fetch_data.py stocks --symbols SPY --start 2020-01-01 --end 2023-01-01
    python data/fetch_data.py options --symbols SPY --start 2020-01-01 --end 2023-01-01
    python data/fetch_data.py all --symbols SPY --start 2020-01-01 --end 2023-01-01 --update
"""

import argparse
import shutil
import sys
from pathlib import Path
from urllib.request import Request, urlopen

import pandas as pd

BASE_DIR = Path(__file__).resolve().parent
RAW_DIR = BASE_DIR / "raw"
PROCESSED_DIR = BASE_DIR / "processed"

# Self-hosted data on GitHub Releases (primary source)
RELEASE_URL = "https://github.com/lambdaclass/options_backtester/releases/download/data-v1"

# philippdubach/options-data — 104 symbols, options + underlying (underlying empty for some ETFs)
OPTIONS_DATA_URL = "https://static.philippdubach.com/data/options"

# philippdubach/options-dataset-hist — SPY/IWM/QQQ, proper underlying_prices via GitHub LFS
HIST_REPO_RAW = "https://github.com/philippdubach/options-dataset-hist/raw/main/data"
HIST_SYMBOLS = {"SPY", "IWM", "QQQ"}


# ---------------------------------------------------------------------------
# Download helpers
# ---------------------------------------------------------------------------

def _download(url, dest, force=False):
    """Download url to dest. Returns dest on success, None on failure."""
    dest = Path(dest)
    dest.parent.mkdir(parents=True, exist_ok=True)

    if dest.exists() and not force:
        print(f"  Using cached {dest}")
        return dest

    print(f"  Downloading {url} ...")
    try:
        req = Request(url, headers={"User-Agent": "options-backtester/1.0"})
        with urlopen(req) as resp, open(dest, "wb") as f:
            shutil.copyfileobj(resp, f)
        print(f"  Saved to {dest}")
    except Exception as e:
        print(f"  Error: {e}", file=sys.stderr)
        if dest.exists():
            dest.unlink()
        return None
    return dest


def download_options_parquet(symbol, force=False):
    """Download options parquet. Priority: GitHub Release > options-data CDN."""
    sym = symbol.upper()

    # 1. Self-hosted GitHub Release
    dest = RAW_DIR / "release" / f"{sym}_options.parquet"
    url = f"{RELEASE_URL}/{sym}_options.parquet"
    result = _download(url, dest, force)
    if result is not None:
        return result

    # 2. options-data CDN
    dest = RAW_DIR / "options-data" / sym / "options.parquet"
    url = f"{OPTIONS_DATA_URL}/{sym.lower()}/options.parquet"
    return _download(url, dest, force)


def download_underlying(symbol, force=False):
    """Download underlying prices.

    Priority: GitHub Release > options-dataset-hist > options-data > None (caller falls back to yfinance).
    """
    sym = symbol.upper()

    # 1. Self-hosted GitHub Release
    dest = RAW_DIR / "release" / f"{sym}_underlying.parquet"
    url = f"{RELEASE_URL}/{sym}_underlying.parquet"
    result = _download(url, dest, force)
    if result is not None:
        df = pd.read_parquet(result)
        if not df.empty:
            return result
        print(f"  Warning: release underlying empty for {sym}")

    # 2. options-dataset-hist has proper underlying for SPY/IWM/QQQ
    if sym in HIST_SYMBOLS:
        dest = RAW_DIR / "options-dataset-hist" / sym / "underlying_prices.parquet"
        url = f"{HIST_REPO_RAW}/parquet_{sym.lower()}/underlying_prices.parquet"
        result = _download(url, dest, force)
        if result is not None:
            df = pd.read_parquet(result)
            if not df.empty:
                return result
            print(f"  Warning: options-dataset-hist underlying empty for {sym}")

    # 3. options-data underlying
    dest = RAW_DIR / "options-data" / sym / "underlying.parquet"
    url = f"{OPTIONS_DATA_URL}/{sym.lower()}/underlying.parquet"
    result = _download(url, dest, force)
    if result is not None:
        df = pd.read_parquet(result)
        if not df.empty:
            return result
        print(f"  Warning: options-data underlying empty for {sym}")

    return None


# ---------------------------------------------------------------------------
# Underlying price reading
# ---------------------------------------------------------------------------

def read_underlying_prices(symbol, und_path, start, end):
    """Read underlying parquet and return (date, close) DataFrame for joining."""
    und = pd.read_parquet(und_path)
    und["date"] = pd.to_datetime(und["date"])
    und = und[(und["date"] >= start) & (und["date"] <= end)]
    if und.empty:
        return None
    return und[["date", "close"]].rename(columns={"close": "underlying_last"})


def underlying_to_tiingo(symbol, und_path, start, end):
    """Convert an underlying.parquet to Tiingo-format DataFrame."""
    und = pd.read_parquet(und_path)
    und["date"] = pd.to_datetime(und["date"])
    und = und[(und["date"] >= start) & (und["date"] <= end)]

    if und.empty:
        return pd.DataFrame()

    ratio = und["adjusted_close"] / und["close"]

    return pd.DataFrame({
        "symbol": symbol,
        "date": und["date"].values,
        "close": und["close"].values,
        "high": und["high"].values,
        "low": und["low"].values,
        "open": und["open"].values,
        "volume": und["volume"].values,
        "adjClose": und["adjusted_close"].values,
        "adjHigh": (und["high"] * ratio).values,
        "adjLow": (und["low"] * ratio).values,
        "adjOpen": (und["open"] * ratio).values,
        "adjVolume": und["volume"].values,
        "divCash": und["dividend_amount"].values,
        "splitFactor": und["split_coefficient"].values,
    })


def fetch_yfinance(symbol, start, end):
    """Fetch one symbol via yfinance (last resort)."""
    try:
        import yfinance as yf
    except ImportError:
        print(f"  yfinance not installed, cannot fetch {symbol}", file=sys.stderr)
        return pd.DataFrame()

    print(f"  Last resort: fetching {symbol} from yfinance...")
    ticker = yf.Ticker(symbol)
    df = ticker.history(start=str(start.date()), end=str(end.date()), auto_adjust=False)

    if df.empty:
        return pd.DataFrame()

    if df.index.tz is not None:
        df.index = df.index.tz_localize(None)

    ratio = df["Adj Close"] / df["Close"]

    return pd.DataFrame({
        "symbol": symbol,
        "date": df.index,
        "close": df["Close"].values,
        "high": df["High"].values,
        "low": df["Low"].values,
        "open": df["Open"].values,
        "volume": df["Volume"].values,
        "adjClose": df["Adj Close"].values,
        "adjHigh": (df["High"] * ratio).values,
        "adjLow": (df["Low"] * ratio).values,
        "adjOpen": (df["Open"] * ratio).values,
        "adjVolume": df["Volume"].values,
        "divCash": 0.0,
        "splitFactor": 1.0,
    })


# ---------------------------------------------------------------------------
# Options
# ---------------------------------------------------------------------------

def fetch_options(symbols, start, end, output, force=False):
    """Download options parquets and convert to backtester CSV format."""
    frames = []

    for symbol in symbols:
        sym = symbol.upper()
        print(f"Fetching options for {sym}...")

        opt_path = download_options_parquet(sym, force)
        if opt_path is None:
            print(f"  Skipping {sym} options (download failed)", file=sys.stderr)
            continue

        opts = pd.read_parquet(opt_path)
        opts["date"] = pd.to_datetime(opts["date"])
        opts = opts[(opts["date"] >= start) & (opts["date"] <= end)]

        if opts.empty:
            print(f"  No options data for {sym} in [{start}, {end}]")
            continue

        # Get underlying close prices for underlying_last
        und_path = download_underlying(sym, force)
        und_prices = None
        if und_path is not None:
            und_prices = read_underlying_prices(sym, und_path, start, end)

        if und_prices is None:
            yf_df = fetch_yfinance(sym, start, end)
            if not yf_df.empty:
                und_prices = pd.DataFrame({
                    "date": pd.to_datetime(yf_df["date"]),
                    "underlying_last": yf_df["close"].values,
                })

        if und_prices is not None:
            opts = opts.merge(und_prices, on="date", how="left")
        else:
            opts["underlying_last"] = float("nan")

        # Last price: use column if present, else mid
        if "last" in opts.columns:
            opts["_last"] = opts["last"].fillna((opts["bid"] + opts["ask"]) / 2)
        else:
            opts["_last"] = (opts["bid"] + opts["ask"]) / 2

        out = pd.DataFrame({
            "underlying": sym,
            "underlying_last": opts["underlying_last"].values,
            "optionroot": opts["contract_id"].values,
            "type": opts["type"].values,
            "expiration": pd.to_datetime(opts["expiration"]).values,
            "quotedate": opts["date"].values,
            "strike": opts["strike"].values,
            "last": opts["_last"].values,
            "bid": opts["bid"].values,
            "ask": opts["ask"].values,
            "volume": opts["volume"].values,
            "openinterest": opts["open_interest"].values,
            "impliedvol": opts["implied_volatility"].values,
            "delta": opts["delta"].values,
            "gamma": opts["gamma"].values,
            "theta": opts["theta"].values,
            "vega": opts["vega"].values,
            "optionalias": opts["contract_id"].values,
        })
        frames.append(out)
        print(f"  {len(out)} option rows for {sym}")

    if not frames:
        print("No options data fetched.", file=sys.stderr)
        return None

    result = pd.concat(frames, ignore_index=True)
    result = result.sort_values(["quotedate", "underlying", "expiration", "strike", "type"])
    PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
    result.to_csv(output, index=False)
    print(f"Wrote {len(result)} option rows to {output}")
    return result


# ---------------------------------------------------------------------------
# Stocks
# ---------------------------------------------------------------------------

def fetch_stocks(symbols, start, end, output, force=False):
    """Download stock data. Priority: options-dataset-hist > options-data > yfinance."""
    frames = []

    for symbol in symbols:
        sym = symbol.upper()
        print(f"Fetching stocks for {sym}...")

        und_path = download_underlying(sym, force)
        if und_path is not None:
            df = underlying_to_tiingo(sym, und_path, start, end)
            if not df.empty:
                source = "options-dataset-hist" if "options-dataset-hist" in str(und_path) else "options-data"
                frames.append(df)
                print(f"  {len(df)} stock rows for {sym} (from {source})")
                continue

        df = fetch_yfinance(sym, start, end)
        if not df.empty:
            frames.append(df)
            print(f"  {len(df)} stock rows for {sym} (from yfinance)")
        else:
            print(f"  No stock data for {sym}", file=sys.stderr)

    if not frames:
        print("No stock data fetched.", file=sys.stderr)
        return None

    result = pd.concat(frames, ignore_index=True)
    PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
    result.to_csv(output, index=False)
    print(f"Wrote {len(result)} stock rows to {output}")
    return result


# ---------------------------------------------------------------------------
# Date alignment
# ---------------------------------------------------------------------------

def align_dates(stocks_path, options_path):
    """Align stock and option dates to their intersection."""
    stocks = pd.read_csv(stocks_path, parse_dates=["date"])
    options = pd.read_csv(options_path, parse_dates=["quotedate", "expiration"])

    stock_dates = set(stocks["date"].dt.normalize())
    option_dates = set(options["quotedate"].dt.normalize())
    shared = stock_dates & option_dates

    if not shared:
        print("Warning: no overlapping dates between stocks and options!", file=sys.stderr)
        return

    stocks_filtered = stocks[stocks["date"].dt.normalize().isin(shared)]
    options_filtered = options[options["quotedate"].dt.normalize().isin(shared)]

    stocks_filtered.to_csv(stocks_path, index=False)
    options_filtered.to_csv(options_path, index=False)

    dropped_stock = len(stocks) - len(stocks_filtered)
    dropped_opt = len(options) - len(options_filtered)
    print(f"Aligned dates: {len(shared)} shared trading days")
    if dropped_stock:
        print(f"  Dropped {dropped_stock} stock rows without matching option dates")
    if dropped_opt:
        print(f"  Dropped {dropped_opt} option rows without matching stock dates")


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Fetch stock and options data for the backtester"
    )
    parser.add_argument(
        "command", nargs="?", default="all",
        choices=["all", "stocks", "options"],
        help="What to fetch (default: all)",
    )
    parser.add_argument(
        "--symbols", nargs="+", required=True,
        help="Ticker symbols (e.g. SPY IWM QQQ AAPL)",
    )
    parser.add_argument("--start", required=True, help="Start date (YYYY-MM-DD)")
    parser.add_argument("--end", required=True, help="End date (YYYY-MM-DD)")
    parser.add_argument(
        "--stocks-output", default="data/processed/stocks.csv",
        help="Stock CSV output path",
    )
    parser.add_argument(
        "--options-output", default="data/processed/options.csv",
        help="Options CSV output path",
    )
    parser.add_argument(
        "--update", action="store_true",
        help="Re-download parquets to get latest data",
    )
    args = parser.parse_args()

    start = pd.Timestamp(args.start)
    end = pd.Timestamp(args.end)
    force = args.update

    if args.command in ("all", "options"):
        fetch_options(args.symbols, start, end, args.options_output, force)

    if args.command in ("all", "stocks"):
        fetch_stocks(args.symbols, start, end, args.stocks_output, force)

    if args.command == "all":
        if Path(args.stocks_output).exists() and Path(args.options_output).exists():
            print("\nAligning dates...")
            align_dates(args.stocks_output, args.options_output)

    print("\nDone!")


if __name__ == "__main__":
    main()


================================================
FILE: data/fetch_signals.py
================================================
#!/usr/bin/env python3
"""Download macro signal data from FRED for use in backtest signal filters.

Downloads:
  - GDP (quarterly) — for Buffett Indicator proxy
  - VIX (daily) — CBOE Volatility Index
  - High Yield Spread (daily) — credit stress indicator
  - 10Y-2Y Yield Curve (daily) — recession predictor
  - Nonfinancial Corporate Equity Market Value (quarterly) — for Tobin's Q
  - Nonfinancial Corporate Net Worth (quarterly) — for Tobin's Q
  - Dollar Index (daily) — broad trade-weighted USD

Outputs:
  data/processed/signals.csv — daily signal data, forward-filled from quarterly
"""

import io
import urllib.request

import pandas as pd

FRED_SERIES = {
    'gdp': 'GDP',
    'vix': 'VIXCLS',
    'hy_spread': 'BAMLH0A0HYM2',
    'yield_curve_10y2y': 'T10Y2Y',
    'nfc_equity_mv': 'NCBEILQ027S',
    'nfc_net_worth': 'NCBCMDPMVCE',
    'dollar_index': 'DTWEXBGS',
}

START = '2007-01-01'
END = '2025-12-31'


def fetch_fred(series_id: str) -> pd.Series:
    url = (f'https://fred.stlouisfed.org/graph/fredgraph.csv'
           f'?id={series_id}&cosd={START}&coed={END}')
    req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
    resp = urllib.request.urlopen(req, timeout=30)
    data = resp.read().decode()
    df = pd.read_csv(io.StringIO(data), parse_dates=['observation_date'],
                     index_col='observation_date')
    col = df.columns[0]
    s = pd.to_numeric(df[col], errors='coerce')
    s.index.name = 'date'
    return s.dropna()


def main():
    signals = {}

    for name, sid in FRED_SERIES.items():
        print(f'Fetching {name} ({sid})...', end=' ', flush=True)
        try:
            s = fetch_fred(sid)
            signals[name] = s
            print(f'{len(s)} obs, {s.index[0].date()} to {s.index[-1].date()}')
        except Exception as e:
            print(f'FAILED: {e}')

    if not signals:
        print('No data fetched.')
        return

    # Build daily DataFrame
    all_dates = sorted(set().union(*(s.index for s in signals.values())))
    daily = pd.DataFrame(index=pd.DatetimeIndex(all_dates, name='date'))

    for name, s in signals.items():
        daily[name] = s.reindex(daily.index)

    # Forward-fill quarterly data to daily
    daily = daily.ffill()

    # Compute derived signals
    if 'gdp' in daily.columns:
        # Buffett Indicator proxy: we don't have total market cap, but
        # nfc_equity_mv is corporate equity market value (in millions)
        # GDP is in billions. Scale NFC equity to billions to match.
        if 'nfc_equity_mv' in daily.columns:
            daily['buffett_indicator'] = daily['nfc_equity_mv'] / (daily['gdp'] * 1000) * 100
            print(f'Computed buffett_indicator (nfc_equity_mv / GDP)')

    if 'nfc_equity_mv' in daily.columns and 'nfc_net_worth' in daily.columns:
        # Tobin's Q proxy: market value / net worth
        # nfc_net_worth is in weird units (ratio), use nfc_equity_mv levels
        # Actually NCBCMDPMVCE is "market value / cost" already
        daily['tobin_q'] = daily['nfc_net_worth']
        print(f'Computed tobin_q (NCBCMDPMVCE is already MV/replacement cost)')

    daily = daily.dropna(how='all')

    out = 'data/processed/signals.csv'
    daily.to_csv(out)
    print(f'\nSaved {len(daily)} rows to {out}')
    print(f'Columns: {list(daily.columns)}')
    print(f'Date range: {daily.index[0].date()} to {daily.index[-1].date()}')
    print(daily.describe().round(2))


if __name__ == '__main__':
    main()


================================================
FILE: flake.nix
================================================
{
  description = "Options backtester dev environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    rust-overlay.url = "github:oxalica/rust-overlay";
  };

  outputs = { self, nixpkgs, rust-overlay }:
    let
      supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
    in {
      devShells = forAllSystems (system:
        let
          pkgs = import nixpkgs {
            inherit system;
            overlays = [ rust-overlay.overlays.default ];
          };
          python = pkgs.python312;
          pythonPkgs = python.pkgs;
          rustToolchain = pkgs.rust-bin.stable.latest.default.override {
            extensions = [ "rust-src" "rust-analyzer" ];
          };
        in {
          default = pkgs.mkShell {
            packages = [
              # Rust
              rustToolchain
              pkgs.maturin
              pkgs.cargo-nextest

              (python.withPackages (ps: [
                # Runtime
                ps.pandas
                ps.numpy
                ps.altair
                ps.pyprind
                ps.seaborn
                ps.matplotlib
                ps.pyarrow
                ps.polars

                # Notebooks
                ps.jupyter
                ps.nbconvert
                ps.ipykernel

                # Testing
                ps.pytest
                ps.hypothesis
                ps.pytest-benchmark
                ps.mypy
                ps.pandas-stubs
                ps.ruff

                # Dev tools
                ps.yapf

                # Data fetching (optional, for data/ scripts)
                ps.yfinance
              ]))
            ];

            shellHook = ''
              export PYO3_PYTHON=${python}/bin/python
              export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1

              # Build Rust extension and symlink for Python import
              if [ -f rust/ob_python/Cargo.toml ]; then
                if [ ! -f rust/target/release/lib_ob_rust.dylib ] && [ ! -f rust/target/release/lib_ob_rust.so ]; then
                  echo "Building Rust extension (first time only)..."
                  cargo build --manifest-path rust/ob_python/Cargo.toml --release 2>&1 | tail -1
                fi
                # Python needs _ob_rust.so, Rust produces lib_ob_rust.dylib/.so
                if [ -f rust/target/release/lib_ob_rust.dylib ] && [ ! -f _ob_rust.so ]; then
                  ln -sf rust/target/release/lib_ob_rust.dylib _ob_rust.so
                elif [ -f rust/target/release/lib_ob_rust.so ] && [ ! -f _ob_rust.so ]; then
                  ln -sf rust/target/release/lib_ob_rust.so _ob_rust.so
                fi
              fi
            '';
          };
        });
    };
}


================================================
FILE: options_portfolio_backtester/__init__.py
================================================
"""options_portfolio_backtester — the open-source options backtesting framework."""

# Core types
from options_portfolio_backtester.core.types import (
    Direction,
    OptionType,
    Type,
    Order,
    Signal,
    Fill,
    Greeks,
    OptionContract,
    StockAllocation,
    Stock,
    get_order,
)

# Data
from options_portfolio_backtester.data.schema import Schema, Field, Filter
from options_portfolio_backtester.data.providers import (
    CsvOptionsProvider, CsvStocksProvider,
    TiingoData, HistoricalOptionsData,
)

# Strategy
from options_portfolio_backtester.strategy.strategy import Strategy
from options_portfolio_backtester.strategy.strategy_leg import StrategyLeg
from options_portfolio_backtester.strategy.presets import Strangle

# Execution
from options_portfolio_backtester.execution.cost_model import (
    NoCosts, PerContractCommission, TieredCommission, SpreadSlippage,
)
from options_portfolio_backtester.execution.fill_model import MarketAtBidAsk, MidPrice, VolumeAwareFill
from options_portfolio_backtester.execution.sizer import (
    CapitalBased, FixedQuantity, FixedDollar, PercentOfPortfolio,
)
from options_portfolio_backtester.execution.signal_selector import (
    FirstMatch, NearestDelta, MaxOpenInterest,
)

# Portfolio
from options_portfolio_backtester.portfolio.portfolio import Portfolio
from options_portfolio_backtester.portfolio.position import OptionPosition
from options_portfolio_backtester.portfolio.greeks import aggregate_greeks
from options_portfolio_backtester.portfolio.risk import RiskManager, MaxDelta, MaxVega, MaxDrawdown

# Engine
from options_portfolio_backtester.engine.engine import BacktestEngine
from options_portfolio_backtester.engine.clock import TradingClock

# Analytics
from options_portfolio_backtester.analytics.stats import BacktestStats, PeriodStats, LookbackReturns
from options_portfolio_backtester.analytics.trade_log import TradeLog
from options_portfolio_backtester.analytics.tearsheet import TearsheetReport, build_tearsheet
from options_portfolio_backtester.analytics.summary import summary

__all__ = [
    # Core types
    "Direction", "OptionType", "Type", "Order", "Signal", "Fill", "Greeks",
    "OptionContract", "StockAllocation", "Stock", "get_order",
    # Data
    "Schema", "Field", "Filter", "CsvOptionsProvider", "CsvStocksProvider",
    "TiingoData", "HistoricalOptionsData",
    # Strategy
    "Strategy", "StrategyLeg", "Strangle",
    # Execution
    "NoCosts", "PerContractCommission", "TieredCommission", "SpreadSlippage",
    "MarketAtBidAsk", "MidPrice", "VolumeAwareFill",
    "CapitalBased", "FixedQuantity", "FixedDollar", "PercentOfPortfolio",
    "FirstMatch", "NearestDelta", "MaxOpenInterest",
    # Portfolio
    "Portfolio", "OptionPosition", "aggregate_greeks",
    "RiskManager", "MaxDelta", "MaxVega", "MaxDrawdown",
    # Engine
    "BacktestEngine", "TradingClock",
    # Analytics
    "BacktestStats", "PeriodStats", "LookbackReturns",
    "TradeLog", "TearsheetReport", "build_tearsheet",
    "summary",
]


================================================
FILE: options_portfolio_backtester/analytics/__init__.py
================================================


================================================
FILE: options_portfolio_backtester/analytics/charts.py
================================================
"""Charts — Altair charts + matplotlib additions."""

from __future__ import annotations

import altair as alt
import pandas as pd


def returns_chart(report: pd.DataFrame) -> alt.VConcatChart:
    # Time interval selector
    time_interval = alt.selection_interval(encodings=['x'])

    # Area plot
    areas = alt.Chart().mark_area(opacity=0.7).encode(x='index:T',
                                                      y=alt.Y('accumulated return:Q', axis=alt.Axis(format='%')))

    # Nearest point selector
    nearest = alt.selection_point(nearest=True, on='mouseover', fields=['index'])

    points = areas.mark_point().encode(opacity=alt.condition(nearest, alt.value(1), alt.value(0)))

    # Transparent date selector
    selectors = alt.Chart().mark_point().encode(
        x='index:T',
        opacity=alt.value(0),
    ).add_params(nearest)

    text = areas.mark_text(
        align='left', dx=5,
        dy=-5).encode(text=alt.condition(nearest, 'accumulated return:Q', alt.value(' '), format='.2%'))

    layered = alt.layer(selectors,
                        points,
                        text,
                        areas.encode(
                            alt.X('index:T', axis=alt.Axis(title='date'), scale=alt.Scale(domain=time_interval))),
                        width=700,
                        height=350,
                        title='Returns over time')

    lower = areas.properties(width=700, height=70).add_params(time_interval)

    return alt.vconcat(layered, lower, data=report.reset_index())


def returns_histogram(report: pd.DataFrame) -> alt.Chart:
    bar = alt.Chart(report).mark_bar().encode(x=alt.X('% change:Q',
                                                      bin=alt.BinParams(maxbins=100),
                                                      axis=alt.Axis(format='%')),
                                              y='count():Q')
    return bar


def monthly_returns_heatmap(report: pd.DataFrame) -> alt.Chart:
    resample = report.resample('ME')['total capital'].last()
    monthly_returns = resample.pct_change().reset_index()
    monthly_returns.loc[monthly_returns.index[0], 'total capital'] = resample.iloc[0] / report.iloc[0]['total capital'] - 1
    monthly_returns.columns = ['date', 'total capital']

    chart = alt.Chart(monthly_returns).mark_rect().encode(
        alt.X('year(date):O', title='Year'), alt.Y('month(date):O', title='Month'),
        alt.Color('mean(total capital)', title='Return', scale=alt.Scale(scheme='redyellowgreen')),
        alt.Tooltip('mean(total capital)', format='.2f')).properties(title='Monthly Returns')

    return chart


def weights_chart(balance: pd.DataFrame, figsize: tuple[float, float] = (12, 6)):
    """Stacked area chart of portfolio weights over time.

    Expects a balance DataFrame with ``{symbol} qty`` columns and a
    ``total capital`` column (as produced by ``AlgoPipelineBacktester``).

    Returns ``(fig, ax)`` from matplotlib.
    """
    import matplotlib.pyplot as plt

    qty_cols = [c for c in balance.columns if c.endswith(" qty")]
    if not qty_cols:
        fig, ax = plt.subplots(figsize=figsize)
        ax.set_title("Portfolio Weights (no positions found)")
        return fig, ax

    symbols = [c.replace(" qty", "") for c in qty_cols]
    total = balance["total capital"]

    # Compute weights: qty * price / total_capital
    # We don't have price columns directly, but stocks capital is available.
    # Reconstruct per-symbol value: qty * (total - cash) is aggregate,
    # so we estimate from qty shares of total stock value.
    weights = pd.DataFrame(index=balance.index)
    for sym, col in zip(symbols, qty_cols):
        weights[sym] = balance[col].fillna(0)

    # Normalize to weights (proportional share of total qty-weighted value)
    row_sums = weights.abs().sum(axis=1)
    row_sums = row_sums.replace(0, 1)  # avoid division by zero
    # If we have cash and total capital, use stock fraction
    if "cash" in balance.columns:
        stock_fraction = 1.0 - balance["cash"] / total.replace(0, 1)
        for sym in symbols:
            weights[sym] = (weights[sym] / row_sums) * stock_fraction
    else:
        weights = weights.div(row_sums, axis=0)

    fig, ax = plt.subplots(figsize=figsize)
    ax.stackplot(weights.index, *[weights[s] for s in symbols], labels=symbols, alpha=0.8)
    ax.set_title("Portfolio Weights Over Time")
    ax.set_ylabel("Weight")
    ax.set_xlabel("Date")
    ax.legend(loc="upper left", fontsize="small")
    ax.set_ylim(0, 1)
    fig.tight_layout()
    return fig, ax


__all__ = ["returns_chart", "returns_histogram", "monthly_returns_heatmap", "weights_chart"]


================================================
FILE: options_portfolio_backtester/analytics/optimization.py
================================================
"""Walk-forward optimization and parameter grid sweep."""

from __future__ import annotations

import itertools
from concurrent.futures import ProcessPoolExecutor, as_completed
from dataclasses import dataclass
from typing import Any, Callable

import pandas as pd
import numpy as np

from options_portfolio_backtester.analytics.stats import BacktestStats


@dataclass
class OptimizationResult:
    """Result of a single parameter combination."""
    params: dict[str, Any]
    stats: BacktestStats
    balance: pd.DataFrame


def grid_sweep(
    run_fn: Callable[..., tuple[BacktestStats, pd.DataFrame]],
    param_grid: dict[str, list[Any]],
    max_workers: int | None = None,
) -> list[OptimizationResult]:
    """Run a parameter grid sweep using parallel execution.

    Args:
        run_fn: Function that takes **params and returns (BacktestStats, balance).
        param_grid: Dict mapping parameter names to lists of values.
        max_workers: Number of parallel workers (None = CPU count).

    Returns:
        List of OptimizationResult, sorted by Sharpe ratio descending.
    """
    keys = list(param_grid.keys())
    combos = list(itertools.product(*param_grid.values()))
    results: list[OptimizationResult] = []

    with ProcessPoolExecutor(max_workers=max_workers) as executor:
        futures = {}
        for combo in combos:
            params = dict(zip(keys, combo))
            future = executor.submit(run_fn, **params)
            futures[future] = params

        for future in as_completed(futures):
            params = futures[future]
            try:
                stats, balance = future.result()
                results.append(OptimizationResult(
                    params=params, stats=stats, balance=balance,
                ))
            except Exception:
                continue

    results.sort(key=lambda r: r.stats.sharpe_ratio, reverse=True)
    return results


def walk_forward(
    run_fn: Callable[[pd.Timestamp, pd.Timestamp], tuple[BacktestStats, pd.DataFrame]],
    dates: pd.DatetimeIndex,
    in_sample_pct: float = 0.70,
    n_splits: int = 5,
) -> list[tuple[OptimizationResult, OptimizationResult]]:
    """Walk-forward analysis with rolling in-sample/out-of-sample splits.

    Args:
        run_fn: Function that takes (start_date, end_date) and returns (stats, balance).
        dates: Full date range.
        in_sample_pct: Fraction of each window used for in-sample.
        n_splits: Number of walk-forward windows.

    Returns:
        List of (in_sample_result, out_of_sample_result) tuples.
    """
    total = len(dates)
    window_size = total // n_splits
    results = []

    for i in range(n_splits):
        start_idx = i * window_size
        end_idx = min(start_idx + window_size, total)
        split_idx = start_idx + int((end_idx - start_idx) * in_sample_pct)

        is_start = dates[start_idx]
        is_end = dates[split_idx - 1]
        oos_start = dates[split_idx]
        oos_end = dates[end_idx - 1]

        try:
            is_stats, is_balance = run_fn(is_start, is_end)
            oos_stats, oos_balance = run_fn(oos_start, oos_end)
            results.append((
                OptimizationResult(params={"split": i, "type": "in_sample"},
                                   stats=is_stats, balance=is_balance),
                OptimizationResult(params={"split": i, "type": "out_of_sample"},
                                   stats=oos_stats, balance=oos_balance),
            ))
        except Exception:
            continue

    return results


def rust_grid_sweep(
    options_data,
    stocks_data,
    base_config: dict,
    schema_mapping: dict,
    param_overrides: list[dict],
    n_workers: int | None = None,
) -> list[dict]:
    """Run a parallel grid sweep using the Rust backtest engine.

    Each dict in param_overrides can contain:
        - "label": str
        - "profit_pct": Optional[float]
        - "loss_pct": Optional[float]
        - "rebalance_dates": Optional[list[str]]
        - "leg_entry_filters": Optional[list[Optional[str]]]
        - "leg_exit_filters": Optional[list[Optional[str]]]

    Returns list of result dicts sorted by sharpe_ratio descending.
    """
    from options_portfolio_backtester._ob_rust import parallel_sweep

    results = parallel_sweep(
        options_data, stocks_data,
        base_config, schema_mapping,
        param_overrides, n_workers,
    )
    results.sort(key=lambda r: r.get("sharpe_ratio", 0.0), reverse=True)
    return results


================================================
FILE: options_portfolio_backtester/analytics/stats.py
================================================
"""BacktestStats — comprehensive analytics matching and exceeding bt/ffn.

Provides:
- Trade stats: profit factor, win rate, largest win/loss
- Return stats: total, annualized, Sharpe, Sortino, Calmar
- Risk stats: max drawdown, drawdown duration, volatility, tail ratio
- Period stats: monthly/yearly Sharpe, Sortino, mean, vol, skew, kurtosis
- Extreme analysis: best/worst day, month, year
- Lookback returns: MTD, 3M, 6M, YTD, 1Y, 3Y, 5Y, 10Y
- Portfolio metrics: turnover, Herfindahl concentration index
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

import numpy as np
import pandas as pd

from options_portfolio_backtester._ob_rust import compute_full_stats


@dataclass
class PeriodStats:
    """Stats for a specific return frequency (daily, monthly, yearly)."""
    mean: float = 0.0
    vol: float = 0.0
    sharpe: float = 0.0
    sortino: float = 0.0
    skew: float = 0.0
    kurtosis: float = 0.0
    best: float = 0.0
    worst: float = 0.0


@dataclass
class LookbackReturns:
    """Trailing period returns as of the end date."""
    mtd: float | None = None
    three_month: float | None = None
    six_month: float | None = None
    ytd: float | None = None
    one_year: float | None = None
    three_year: float | None = None
    five_year: float | None = None
    ten_year: float | None = None


@dataclass
class BacktestStats:
    """Comprehensive backtest statistics."""

    # Trade stats
    total_trades: int = 0
    wins: int = 0
    losses: int = 0
    win_pct: float = 0.0
    profit_factor: float = 0.0
    largest_win: float = 0.0
    largest_loss: float = 0.0
    avg_win: float = 0.0
    avg_loss: float = 0.0
    avg_trade: float = 0.0

    # Return stats
    total_return: float = 0.0
    annualized_return: float = 0.0
    sharpe_ratio: float = 0.0
    sortino_ratio: float = 0.0
    calmar_ratio: float = 0.0

    # Risk stats
    max_drawdown: float = 0.0
    max_drawdown_duration: int = 0
    avg_drawdown: float = 0.0
    avg_drawdown_duration: int = 0
    volatility: float = 0.0
    tail_ratio: float = 0.0

    # Period stats
    daily: PeriodStats = field(default_factory=PeriodStats)
    monthly: PeriodStats = field(default_factory=PeriodStats)
    yearly: PeriodStats = field(default_factory=PeriodStats)

    # Lookback
    lookback: LookbackReturns = field(default_factory=LookbackReturns)

    # Portfolio metrics
    turnover: float = 0.0
    herfindahl: float = 0.0

    @classmethod
    def from_balance_range(
        cls,
        balance: pd.DataFrame,
        start: str | pd.Timestamp | None = None,
        end: str | pd.Timestamp | None = None,
        **kwargs,
    ) -> "BacktestStats":
        """Slice balance to [start, end] and recompute all stats."""
        if balance.empty:
            return cls()
        b = balance.copy()
        if start:
            b = b.loc[pd.Timestamp(start):]
        if end:
            b = b.loc[:pd.Timestamp(end)]
        if b.empty:
            return cls()
        b["% change"] = b["total capital"].pct_change()
        return cls.from_balance(b, **kwargs)

    @classmethod
    def from_balance(
        cls,
        balance: pd.DataFrame,
        trade_pnls: np.ndarray | None = None,
        risk_free_rate: float = 0.0,
    ) -> "BacktestStats":
        """Compute stats from a balance DataFrame and optional trade P&Ls."""
        if balance.empty:
            return cls()

        total_capital = balance["total capital"].values.astype(np.float64)
        timestamps_ns = balance.index.values.astype("datetime64[ns]").view("int64").astype(np.int64).tolist()
        pnls = trade_pnls.astype(np.float64).tolist() if trade_pnls is not None else []

        # Build stock weight matrix
        stock_cols = [c for c in balance.columns if f"{c} qty" in balance.columns]
        if stock_cols:
            total = balance["total capital"].values
            with np.errstate(divide="ignore", invalid="ignore"):
                weights = balance[stock_cols].values / total[:, None]
            weights = np.nan_to_num(weights, 0.0).astype(np.float64)
            flat_weights = weights.ravel().tolist()
            n_stocks = len(stock_cols)
        else:
            flat_weights = []
            n_stocks = 0

        d = compute_full_stats(
            total_capital.tolist(),
            timestamps_ns,
            pnls,
            flat_weights,
            n_stocks,
            risk_free_rate,
        )

        stats = cls()
        # Scalars
        for attr in (
            "total_trades", "wins", "losses", "win_pct", "profit_factor",
            "largest_win", "largest_loss", "avg_win", "avg_loss", "avg_trade",
            "total_return", "annualized_return", "sharpe_ratio", "sortino_ratio",
            "calmar_ratio", "max_drawdown", "max_drawdown_duration",
            "avg_drawdown", "avg_drawdown_duration", "volatility", "tail_ratio",
            "turnover", "herfindahl",
        ):
            setattr(stats, attr, d[attr])

        # Period stats
        for period_name in ("daily", "monthly", "yearly"):
            pd_dict = d[period_name]
            setattr(stats, period_name, PeriodStats(
                mean=pd_dict["mean"], vol=pd_dict["vol"],
                sharpe=pd_dict["sharpe"], sortino=pd_dict["sortino"],
                skew=pd_dict["skew"], kurtosis=pd_dict["kurtosis"],
                best=pd_dict["best"], worst=pd_dict["worst"],
            ))

        # Lookback
        lb = d["lookback"]
        stats.lookback = LookbackReturns(
            mtd=lb["mtd"], three_month=lb["three_month"],
            six_month=lb["six_month"], ytd=lb["ytd"],
            one_year=lb["one_year"], three_year=lb["three_year"],
            five_year=lb["five_year"], ten_year=lb["ten_year"],
        )

        return stats

    def to_dataframe(self) -> pd.DataFrame:
        """Return stats as a styled DataFrame."""
        data = {
            "Total trades": self.total_trades,
            "Wins": self.wins,
            "Losses": self.losses,
            "Win %": self.win_pct,
            "Profit factor": self.profit_factor,
            "Largest win": self.largest_win,
            "Largest loss": self.largest_loss,
            "Avg win": self.avg_win,
            "Avg loss": self.avg_loss,
            "Avg trade": self.avg_trade,
            "Total return": self.total_return,
            "Annualized return": self.annualized_return,
            "Sharpe ratio": self.sharpe_ratio,
            "Sortino ratio": self.sortino_ratio,
            "Calmar ratio": self.calmar_ratio,
            "Max drawdown": self.max_drawdown,
            "Max DD duration (days)": self.max_drawdown_duration,
            "Avg drawdown": self.avg_drawdown,
            "Avg DD duration (days)": self.avg_drawdown_duration,
            "Volatility": self.volatility,
            "Tail ratio": self.tail_ratio,
            # Daily
            "Daily mean": self.daily.mean,
            "Daily vol": self.daily.vol,
            "Daily Sharpe": self.daily.sharpe,
            "Daily Sortino": self.daily.sortino,
            "Daily skew": self.daily.skew,
            "Daily kurtosis": self.daily.kurtosis,
            "Best day": self.daily.best,
            "Worst day": self.daily.worst,
            # Monthly
            "Monthly mean": self.monthly.mean,
            "Monthly vol": self.monthly.vol,
            "Monthly Sharpe": self.monthly.sharpe,
            "Monthly Sortino": self.monthly.sortino,
            "Monthly skew": self.monthly.skew,
            "Monthly kurtosis": self.monthly.kurtosis,
            "Best month": self.monthly.best,
            "Worst month": self.monthly.worst,
            # Yearly
            "Yearly mean": self.yearly.mean,
            "Yearly vol": self.yearly.vol,
            "Yearly Sharpe": self.yearly.sharpe,
            "Yearly Sortino": self.yearly.sortino,
            "Best year": self.yearly.best,
            "Worst year": self.yearly.worst,
            # Portfolio
            "Turnover": self.turnover,
            "Herfindahl index": self.herfindahl,
        }
        # Add lookback returns (skip None values)
        lb = self.lookback
        for label, val in [
            ("MTD", lb.mtd), ("3M return", lb.three_month),
            ("6M return", lb.six_month), ("YTD", lb.ytd),
            ("1Y return", lb.one_year), ("3Y return", lb.three_year),
            ("5Y return", lb.five_year), ("10Y return", lb.ten_year),
        ]:
            if val is not None:
                data[label] = val

        return pd.DataFrame(
            list(data.values()), index=list(data.keys()), columns=["Value"]
        )

    def summary(self) -> str:
        """Return a formatted text summary."""
        lines = [
            f"Total Return:      {self.total_return:>10.2%}",
            f"Annualized Return: {self.annualized_return:>10.2%}",
            f"Sharpe Ratio:      {self.sharpe_ratio:>10.2f}",
            f"Sortino Ratio:     {self.sortino_ratio:>10.2f}",
            f"Max Drawdown:      {self.max_drawdown:>10.2%}",
            f"Max DD Duration:   {self.max_drawdown_duration:>10d} days",
            f"Calmar Ratio:      {self.calmar_ratio:>10.2f}",
            f"Profit Factor:     {self.profit_factor:>10.2f}",
            f"Win Rate:          {self.win_pct:>10.1f}%",
            f"Total Trades:      {self.total_trades:>10d}",
        ]
        if self.monthly.sharpe != 0:
            lines.append(f"Monthly Sharpe:    {self.monthly.sharpe:>10.2f}")
        if self.monthly.best != 0:
            lines.append(f"Best Month:        {self.monthly.best:>10.2%}")
            lines.append(f"Worst Month:       {self.monthly.worst:>10.2%}")
        if self.turnover != 0:
            lines.append(f"Turnover:          {self.turnover:>10.2%}")
        return "\n".join(lines)

    def lookback_table(self) -> pd.DataFrame:
        """Lookback returns as a single-row DataFrame."""
        lb = self.lookback
        data = {}
        for label, val in [
            ("MTD", lb.mtd), ("3M", lb.three_month), ("6M", lb.six_month),
            ("YTD", lb.ytd), ("1Y", lb.one_year), ("3Y", lb.three_year),
            ("5Y", lb.five_year), ("10Y", lb.ten_year),
        ]:
            if val is not None:
                data[label] = val
        if not data:
            return pd.DataFrame()
        return pd.DataFrame([data])


================================================
FILE: options_portfolio_backtester/analytics/summary.py
================================================
"""Summary statistics for trade logs."""

from __future__ import annotations

import numpy as np
import pandas as pd

from options_portfolio_backtester.core.types import Order


def summary(trade_log: pd.DataFrame, balance: pd.DataFrame) -> pd.io.formats.style.Styler:
    """Returns a table with summary statistics about the trade log"""

    initial_capital: float = balance['total capital'].iloc[0]
    trade_log.loc[:,
                  ('totals',
                   'capital')] = (-trade_log['totals']['cost'] * trade_log['totals']['qty']).cumsum() + initial_capital

    daily_returns: pd.Series = balance['% change'] * 100

    first_leg: str = trade_log.columns.levels[0][0]

    ## Not sure of a better way to to this, just doing `Order` or `@Order` inside
    ## the .eval(...) does not seem to work.
    order_bto = Order.BTO
    order_sto = Order.STO

    entry_mask: pd.Series = trade_log[first_leg].eval('(order == @order_bto) | (order == @order_sto)')
    entries: pd.DataFrame = trade_log.loc[entry_mask]
    exits: pd.DataFrame = trade_log.loc[~entry_mask]

    costs: np.ndarray = np.array([])
    for contract in entries[first_leg]['contract']:
        entry = entries.loc[entries[first_leg]['contract'] == contract]
        exit_ = exits.loc[exits[first_leg]['contract'] == contract]
        try:
            # Here we assume we are entering only once per contract (i.e both entry and exit_ have only one row)
            costs = np.append(costs, (entry['totals']['cost'] * entry['totals']['qty']).values[0] +
                              (exit_['totals']['cost'] * exit_['totals']['qty']).values[0])
        except IndexError:
            continue

    wins: np.ndarray = costs < 0
    losses: np.ndarray = costs >= 0
    total_trades: int = len(exits)
    win_number: int = int(np.sum(wins))
    loss_number: int = total_trades - win_number
    win_pct: float = (win_number / total_trades) * 100 if total_trades > 0 else 0
    profit_factor: float = np.sum(wins) / np.sum(losses) if np.sum(losses) > 0 else 0
    largest_loss: float = max(0, np.max(costs)) if len(costs) > 0 else 0
    avg_profit: float = np.mean(-costs) if len(costs) > 0 else 0
    avg_pl: float = np.mean(daily_returns)
    total_pl: float = (trade_log['totals']['capital'].iloc[-1] / initial_capital) * 100

    data = [total_trades, win_number, loss_number, win_pct, largest_loss, profit_factor, avg_profit, avg_pl, total_pl]
    stats = [
        'Total trades', 'Number of wins', 'Number of losses', 'Win %', 'Largest loss', 'Profit factor',
        'Average profit', 'Average P&L %', 'Total P&L %'
    ]
    strat = ['Strategy']
    summary_df = pd.DataFrame(data, stats, strat)

    formatters: dict[str, str] = {
        "Total trades": "{:.0f}",
        "Number of wins": "{:.0f}",
        "Number of losses": "{:.0f}",
        "Win %": "{:.2f}%",
        "Largest loss": "${:.2f}",
        "Profit factor": "{:.2f}",
        "Average profit": "${:.2f}",
        "Average P&L %": "{:.2f}%",
        "Total P&L %": "{:.2f}%"
    }

    styled = summary_df.style
    for row_label, fmt in formatters.items():
        styled = styled.format(fmt, subset=pd.IndexSlice[row_label, :])
    return styled


================================================
FILE: options_portfolio_backtester/analytics/tearsheet.py
================================================
"""Simple tearsheet-style report helpers."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import numpy as np
import pandas as pd

from options_portfolio_backtester.analytics.stats import BacktestStats


@dataclass
class TearsheetReport:
    """Container for common report artifacts."""

    stats: BacktestStats
    stats_table: pd.DataFrame
    monthly_returns: pd.DataFrame
    drawdown_series: pd.Series

    def to_dict(self) -> dict[str, object]:
        return {
            "stats": self.stats,
            "stats_table": self.stats_table,
            "monthly_returns": self.monthly_returns,
            "drawdown_series": self.drawdown_series,
        }

    def to_csv(self, directory: str | Path) -> dict[str, Path]:
        out_dir = Path(directory)
        out_dir.mkdir(parents=True, exist_ok=True)
        stats_path = out_dir / "stats_table.csv"
        monthly_path = out_dir / "monthly_returns.csv"
        drawdown_path = out_dir / "drawdown_series.csv"
        self.stats_table.to_csv(stats_path)
        self.monthly_returns.to_csv(monthly_path)
        self.drawdown_series.rename("drawdown").to_frame().to_csv(drawdown_path)
        return {
            "stats_table": stats_path,
            "monthly_returns": monthly_path,
            "drawdown_series": drawdown_path,
        }

    def to_markdown(self) -> str:
        lines = ["# Tearsheet", "", "## Summary", ""]
        try:
            lines.extend(self.stats_table.to_markdown().splitlines())
        except Exception:
            lines.extend(self.stats_table.to_string().splitlines())
        lines.extend(["", "## Monthly Returns", ""])
        if self.monthly_returns.empty:
            lines.append("_No monthly returns available._")
        else:
            try:
                lines.extend(self.monthly_returns.to_markdown().splitlines())
            except Exception:
                lines.extend(self.monthly_returns.to_string().splitlines())
        return "\n".join(lines)

    def to_html(self) -> str:
        summary = self.stats_table.to_html(classes="stats-table")
        monthly = (
            self.monthly_returns.to_html(classes="monthly-returns")
            if not self.monthly_returns.empty
            else "<p>No monthly returns available.</p>"
        )
        return (
            "<html><head><meta charset='utf-8'><title>Tearsheet</title></head><body>"
            "<h1>Tearsheet</h1>"
            "<h2>Summary</h2>"
            f"{summary}"
            "<h2>Monthly Returns</h2>"
            f"{monthly}"
            "</body></html>"
        )


def monthly_return_table(balance: pd.DataFrame) -> pd.DataFrame:
    if balance.empty or "% change" not in balance.columns:
        return pd.DataFrame()
    rets = balance["% change"].dropna()
    if rets.empty:
        return pd.DataFrame()
    monthly = (1.0 + rets).groupby(pd.Grouper(freq="ME")).prod() - 1.0
    out = monthly.to_frame(name="return")
    out["year"] = out.index.year
    out["month"] = out.index.month
    return out.pivot(index="year", columns="month", values="return").sort_index()


def drawdown_series(balance: pd.DataFrame) -> pd.Series:
    if balance.empty or "total capital" not in balance.columns:
        return pd.Series(dtype=float)
    total = balance["total capital"].dropna()
    if total.empty:
        return pd.Series(dtype=float)
    peak = total.cummax()
    return (total - peak) / peak


def build_tearsheet(
    balance: pd.DataFrame,
    trade_pnls=None,
    risk_free_rate: float = 0.0,
) -> TearsheetReport:
    trade_arr = None if trade_pnls is None else np.asarray(trade_pnls, dtype=float)
    stats = BacktestStats.from_balance(balance, trade_pnls=trade_arr, risk_free_rate=risk_free_rate)
    table = stats.to_dataframe()
    monthly = monthly_return_table(balance)
    dd = drawdown_series(balance)
    return TearsheetReport(
        stats=stats,
        stats_table=table,
        monthly_returns=monthly,
        drawdown_series=dd,
    )


================================================
FILE: options_portfolio_backtester/analytics/trade_log.py
================================================
"""Structured trade log — replaces MultiIndex trade log with per-trade P&L."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

import pandas as pd
import numpy as np

from options_portfolio_backtester.core.types import Order


@dataclass
class Trade:
    """A single round-trip trade (entry + exit)."""
    contract: str
    underlying: str
    option_type: str
    strike: float
    entry_date: Any
    exit_date: Any
    entry_price: float
    exit_price: float
    quantity: int
    shares_per_contract: int
    entry_order: Order
    exit_order: Order
    entry_commission: float = 0.0
    exit_commission: float = 0.0

    @property
    def gross_pnl(self) -> float:
        """P&L before commissions."""
        return (self.exit_price - self.entry_price) * self.quantity * self.shares_per_contract

    @property
    def net_pnl(self) -> float:
        """P&L after commissions."""
        return self.gross_pnl - self.entry_commission - self.exit_commission

    @property
    def return_pct(self) -> float:
        """Return as percentage of entry cost."""
        entry_cost = abs(self.entry_price * self.quantity * self.shares_per_contract)
        if entry_cost == 0:
            return 0.0
        return self.net_pnl / entry_cost


class TradeLog:
    """Structured collection of round-trip trades with analysis methods."""

    def __init__(self) -> None:
        self.trades: list[Trade] = []

    def add_trade(self, trade: Trade) -> None:
        self.trades.append(trade)

    @classmethod
    def from_legacy_trade_log(cls, trade_log: pd.DataFrame,
                              shares_per_contract: int = 100) -> "TradeLog":
        """Build a TradeLog from the legacy MultiIndex trade_log DataFrame."""
        tl = cls()
        if trade_log.empty:
            return tl

        first_leg: str = trade_log.columns.levels[0][0]

        order_bto = Order.BTO
        order_sto = Order.STO
        entry_mask = trade_log[first_leg].eval(
            "(order == @order_bto) | (order == @order_sto)"
        )
        entries = trade_log.loc[entry_mask]
        exits = trade_log.loc[~entry_mask]

        for contract in entries[first_leg]["contract"]:
            entry = entries.loc[entries[first_leg]["contract"] == contract]
            exit_ = exits.loc[exits[first_leg]["contract"] == contract]
            if entry.empty or exit_.empty:
                continue
            try:
                e_row = entry.iloc[0]
                x_row = exit_.iloc[0]
                trade = Trade(
                    contract=contract,
                    underlying=e_row[first_leg]["underlying"],
                    option_type=e_row[first_leg]["type"],
                    strike=e_row[first_leg]["strike"],
                    entry_date=e_row["totals"]["date"],
                    exit_date=x_row["totals"]["date"],
                    entry_price=abs(e_row[first_leg]["cost"]) / shares_per_contract,
                    exit_price=abs(x_row[first_leg]["cost"]) / shares_per_contract,
                    quantity=int(e_row["totals"]["qty"]),
                    shares_per_contract=shares_per_contract,
                    entry_order=e_row[first_leg]["order"],
                    exit_order=x_row[first_leg]["order"],
                )
                tl.add_trade(trade)
            except (IndexError, KeyError):
                continue

        return tl

    def to_dataframe(self) -> pd.DataFrame:
        """Convert to a flat DataFrame for analysis."""
        if not self.trades:
            return pd.DataFrame()
        rows = []
        for t in self.trades:
            rows.append({
                "contract": t.contract,
                "underlying": t.underlying,
                "type": t.option_type,
                "strike": t.strike,
                "entry_date": t.entry_date,
                "exit_date": t.exit_date,
                "entry_price": t.entry_price,
                "exit_price": t.exit_price,
                "quantity": t.quantity,
                "gross_pnl": t.gross_pnl,
                "net_pnl": t.net_pnl,
                "return_pct": t.return_pct,
                "entry_commission": t.entry_commission,
                "exit_commission": t.exit_commission,
            })
        return pd.DataFrame(rows)

    @property
    def net_pnls(self) -> np.ndarray:
        return np.array([t.net_pnl for t in self.trades])

    @property
    def winners(self) -> list[Trade]:
        return [t for t in self.trades if t.net_pnl > 0]

    @property
    def losers(self) -> list[Trade]:
        return [t for t in self.trades if t.net_pnl <= 0]

    def __len__(self) -> int:
        return len(self.trades)


================================================
FILE: options_portfolio_backtester/convexity/__init__.py
================================================
"""Convexity scanner: cross-asset tail protection scoring and allocation."""

from options_portfolio_backtester.convexity.allocator import (
    allocate_equal_weight,
    allocate_inverse_vol,
    pick_cheapest,
)
from options_portfolio_backtester.convexity.backtest import (
    BacktestResult,
    run_backtest,
    run_unhedged,
)
from options_portfolio_backtester.convexity.config import (
    BacktestConfig,
    InstrumentConfig,
    default_config,
)
from options_portfolio_backtester.convexity.scoring import compute_convexity_scores

__all__ = [
    "InstrumentConfig",
    "BacktestConfig",
    "default_config",
    "compute_convexity_scores",
    "BacktestResult",
    "run_backtest",
    "run_unhedged",
    "pick_cheapest",
    "allocate_equal_weight",
    "allocate_inverse_vol",
]


================================================
FILE: options_portfolio_backtester/convexity/_utils.py
================================================
"""Shared utilities for the convexity module."""

from __future__ import annotations

import numpy as np
import pandas as pd


def _to_ns(series: pd.Series) -> np.ndarray:
    """Convert a datetime Series to int64 nanosecond timestamps."""
    return series.values.astype("datetime64[ns]").view("int64").astype(np.int64)


================================================
FILE: options_portfolio_backtester/convexity/allocator.py
================================================
"""Allocation strategies: pick which instrument(s) to hedge."""

from __future__ import annotations


def pick_cheapest(scores: dict[str, float]) -> str:
    """Pick the instrument with the highest convexity ratio."""
    if not scores:
        raise ValueError("No scores to pick from")
    return max(scores, key=scores.get)


def allocate_equal_weight(symbols: list[str], budget: float) -> dict[str, float]:
    """Split budget equally across all instruments."""
    if not symbols:
        return {}
    per_symbol = budget / len(symbols)
    return {s: per_symbol for s in symbols}


def allocate_inverse_vol(vol_map: dict[str, float], budget: float) -> dict[str, float]:
    """Allocate more to lower-volatility instruments.

    Weight is proportional to 1/vol, normalized to sum to budget.
    """
    if not vol_map:
        return {}

    inv_vols = {}
    for sym, vol in vol_map.items():
        if vol > 0:
            inv_vols[sym] = 1.0 / vol

    if not inv_vols:
        return allocate_equal_weight(list(vol_map.keys()), budget)

    total_inv_vol = sum(inv_vols.values())
    return {sym: (iv / total_inv_vol) * budget for sym, iv in inv_vols.items()}


================================================
FILE: options_portfolio_backtester/convexity/backtest.py
================================================
"""Backtest: run the monthly rebalance loop via Rust backend."""

from __future__ import annotations

import logging
from dataclasses import dataclass

import numpy as np
import pandas as pd

from .config import BacktestConfig

log = logging.getLogger(__name__)


def _to_ns(series: pd.Series) -> np.ndarray:
    """Convert a datetime Series to int64 nanosecond timestamps."""
    return series.values.astype("datetime64[ns]").view("int64").astype(np.int64)


@dataclass
class BacktestResult:
    """Results from a single-instrument backtest."""

    records: pd.DataFrame  # monthly rebalance records
    daily_balance: pd.DataFrame  # daily portfolio values
    config: BacktestConfig


def run_backtest(
    options_data,
    stocks_data,
    config: BacktestConfig,
) -> BacktestResult:
    """Run the full backtest: monthly put overlay on equity portfolio.

    Takes HistoricalOptionsData and TiingoData from options_backtester.
    """
    from options_portfolio_backtester._ob_rust import run_convexity_backtest

    opt_df = options_data._data
    puts = opt_df[opt_df["type"] == "put"].sort_values("quotedate")
    stk_df = stocks_data._data.sort_values("date")

    if puts.empty or stk_df.empty:
        empty_records = pd.DataFrame()
        empty_daily = pd.DataFrame()
        return BacktestResult(records=empty_records, daily_balance=empty_daily, config=config)

    result = run_convexity_backtest(
        put_dates_ns=_to_ns(puts["quotedate"]),
        put_expirations_ns=_to_ns(puts["expiration"]),
        put_strikes=puts["strike"].values.astype(np.float64),
        put_bids=puts["bid"].values.astype(np.float64),
        put_asks=puts["ask"].values.astype(np.float64),
        put_deltas=puts["delta"].values.astype(np.float64),
        put_underlying=puts["underlying_last"].values.astype(np.float64),
        put_dtes=puts["dte"].values.astype(np.int32),
        put_ivs=puts["impliedvol"].values.astype(np.float64),
        stock_dates_ns=_to_ns(stk_df["date"]),
        stock_prices=stk_df["adjClose"].values.astype(np.float64),
        initial_capital=config.initial_capital,
        budget_pct=config.budget_pct,
        target_delta=config.target_delta,
        dte_min=config.dte_min,
        dte_max=config.dte_max,
        tail_drop=config.tail_drop,
    )

    # Build monthly records DataFrame
    rec = result["records"]
    records = pd.DataFrame(
        {
            "date": pd.to_datetime(rec["dates_ns"], unit="ns"),
            "shares": rec["shares"],
            "stock_price": rec["stock_prices"],
            "equity_value": rec["equity_values"],
            "put_cost": rec["put_costs"],
            "put_exit_value": rec["put_exit_values"],
            "put_pnl": rec["put_pnls"],
            "portfolio_value": rec["portfolio_values"],
            "convexity_ratio": rec["convexity_ratios"],
            "strike": rec["strikes"],
            "contracts": rec["contracts"],
        }
    ).set_index("date")

    # Build daily balance DataFrame
    daily = pd.DataFrame(
        {
            "date": pd.to_datetime(result["daily_dates_ns"], unit="ns"),
            "balance": result["daily_balances"],
        }
    ).set_index("date")
    daily["pct_change"] = daily["balance"].pct_change()

    log.info(
        "Backtest: %d months, final value $%.0f (started $%.0f)",
        len(records),
        daily["balance"].iloc[-1] if len(daily) > 0 else 0,
        config.initial_capital,
    )

    return BacktestResult(records=records, daily_balance=daily, config=config)


def run_unhedged(stocks_data, config: BacktestConfig) -> pd.DataFrame:
    """Run unhedged equity-only benchmark. Returns daily balance DataFrame."""
    stk_df = stocks_data._data.sort_values("date")
    if stk_df.empty:
        return pd.DataFrame()

    prices = stk_df["adjClose"].values.astype(np.float64)
    dates = stk_df["date"]

    initial_shares = config.initial_capital / prices[0]
    daily_balance = initial_shares * prices

    df = pd.DataFrame({"date": dates, "balance": daily_balance}).set_index("date")
    df["pct_change"] = df["balance"].pct_change()
    return df


================================================
FILE: options_portfolio_backtester/convexity/config.py
================================================
"""Configuration: instrument registry and backtest parameters."""

from __future__ import annotations

from dataclasses import dataclass, field


@dataclass(frozen=True)
class InstrumentConfig:
    """Configuration for a single instrument."""

    symbol: str
    options_file: str
    stocks_file: str
    target_delta: float = -0.10
    dte_min: int = 14
    dte_max: int = 60
    tail_drop: float = 0.20


@dataclass(frozen=True)
class BacktestConfig:
    """Global backtest parameters."""

    initial_capital: float = 1_000_000.0
    budget_pct: float = 0.005  # 0.5% of portfolio per month on puts
    target_delta: float = -0.10
    dte_min: int = 14
    dte_max: int = 60
    tail_drop: float = 0.20
    instruments: list[InstrumentConfig] = field(default_factory=list)


def default_config(
    options_file: str = "data/processed/options.csv",
    stocks_file: str = "data/processed/stocks.csv",
) -> BacktestConfig:
    """Default config with SPY only."""
    spy = InstrumentConfig(
        symbol="SPY",
        options_file=options_file,
        stocks_file=stocks_file,
    )
    return BacktestConfig(instruments=[spy])


================================================
FILE: options_portfolio_backtester/convexity/scoring.py
================================================
"""Scoring: compute convexity ratios via Rust backend."""

from __future__ import annotations

import logging

import numpy as np
import pandas as pd

from .config import BacktestConfig

log = logging.getLogger(__name__)


def _to_ns(series: pd.Series) -> np.ndarray:
    """Convert a datetime Series to int64 nanosecond timestamps."""
    return series.values.astype("datetime64[ns]").view("int64").astype(np.int64)


def compute_convexity_scores(
    options_data,
    config: BacktestConfig,
) -> pd.DataFrame:
    """Compute daily convexity ratio scores for an instrument.

    Takes an HistoricalOptionsData object from options_backtester and
    returns a DataFrame indexed by date with convexity_ratio and supporting fields.
    """
    from options_portfolio_backtester._ob_rust import compute_daily_scores

    df = options_data._data
    puts = df[df["type"] == "put"].sort_values("quotedate")

    if puts.empty:
        return pd.DataFrame()

    result = compute_daily_scores(
        dates_ns=_to_ns(puts["quotedate"]),
        strikes=puts["strike"].values.astype(np.float64),
        bids=puts["bid"].values.astype(np.float64),
        asks=puts["ask"].values.astype(np.float64),
        deltas=puts["delta"].values.astype(np.float64),
        underlying_prices=puts["underlying_last"].values.astype(np.float64),
        dtes=puts["dte"].values.astype(np.int32),
        implied_vols=puts["impliedvol"].values.astype(np.float64),
        target_delta=config.target_delta,
        dte_min=config.dte_min,
        dte_max=config.dte_max,
        tail_drop=config.tail_drop,
    )

    scores = pd.DataFrame(
        {
            "date": pd.to_datetime(result["dates_ns"], unit="ns"),
            "convexity_ratio": result["convexity_ratios"],
            "strike": result["strikes"],
            "ask": result["asks"],
            "bid": result["bids"],
            "delta": result["deltas"],
            "underlying_price": result["underlying_prices"],
            "implied_vol": result["implied_vols"],
            "dte": result["dtes"],
            "annual_cost": result["annual_costs"],
            "tail_payoff": result["tail_payoffs"],
        }
    ).set_index("date")

    log.info("Computed %d daily scores (%.1f years)", len(scores), len(scores) / 252)

    return scores


================================================
FILE: options_portfolio_backtester/convexity/viz.py
================================================
"""Visualization: Altair charts for scores, allocations, and P&L."""

from __future__ import annotations

import altair as alt
import pandas as pd


def convexity_scores_chart(scores_df: pd.DataFrame) -> alt.Chart:
    """Line chart of daily convexity ratios over time."""
    data = scores_df.reset_index()
    return (
        alt.Chart(data)
        .mark_line()
        .encode(
            x=alt.X("date:T", title="Date"),
            y=alt.Y("convexity_ratio:Q", title="Convexity Ratio"),
            tooltip=["date:T", "convexity_ratio:Q", "strike:Q", "underlying_price:Q", "implied_vol:Q"],
        )
        .properties(title="Daily Convexity Ratio", width=800, height=300)
    )


def monthly_pnl_chart(records: pd.DataFrame) -> alt.Chart:
    """Bar chart of monthly put P&L."""
    data = records.reset_index()
    return (
        alt.Chart(data)
        .mark_bar()
        .encode(
            x=alt.X("date:T", title="Date"),
            y=alt.Y("put_pnl:Q", title="Put P&L ($)"),
            color=alt.condition(
                alt.datum.put_pnl > 0,
                alt.value("steelblue"),
                alt.value("salmon"),
            ),
            tooltip=["date:T", "put_pnl:Q", "put_cost:Q", "put_exit_value:Q", "strike:Q", "contracts:Q"],
        )
        .properties(title="Monthly Put P&L", width=800, height=200)
    )


def cumulative_pnl_chart(results: dict[str, pd.DataFrame]) -> alt.Chart:
    """Cumulative portfolio value for multiple strategies."""
    frames = []
    for name, daily_df in results.items():
        df = daily_df[["balance"]].copy()
        df["strategy"] = name
        frames.append(df.reset_index())

    if not frames:
        return alt.Chart(pd.DataFrame()).mark_line()

    data = pd.concat(frames, ignore_index=True)

    return (
        alt.Chart(data)
        .mark_line()
        .encode(
            x=alt.X("date:T", title="Date"),
            y=alt.Y("balance:Q", title="Portfolio Value ($)", scale=alt.Scale(zero=False)),
            color=alt.Color("strategy:N", title="Strategy"),
            tooltip=["date:T", "balance:Q", "strategy:N"],
        )
        .properties(title="Cumulative Portfolio Value", width=800, height=400)
    )


================================================
FILE: options_portfolio_backtester/core/__init__.py
================================================


================================================
FILE: options_portfolio_backtester/core/types.py
================================================
"""Core domain types for options backtesting.

Direction is decoupled from column names — use Direction.price_column instead of
Direction.value to get the DataFrame column for pricing.
"""

from __future__ import annotations

from collections import namedtuple
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

class OptionType(Enum):
    CALL = "call"
    PUT = "put"

    def __invert__(self) -> OptionType:
        return OptionType.PUT if self == OptionType.CALL else OptionType.CALL


class Direction(Enum):
    """Trade direction. price_column gives the DataFrame column name."""
    BUY = "buy"
    SELL = "sell"

    @property
    def price_column(self) -> str:
        """Column name used for trade execution pricing."""
        return "ask" if self == Direction.BUY else "bid"

    def __invert__(self) -> Direction:
        return Direction.SELL if self == Direction.BUY else Direction.BUY


class Signal(Enum):
    ENTRY = "entry"
    EXIT = "exit"


class Order(Enum):
    BTO = "BTO"  # Buy to Open
    BTC = "BTC"  # Buy to Close
    STO = "STO"  # Sell to Open
    STC = "STC"  # Sell to Close

    def __invert__(self) -> Order:
        _inv = {Order.BTO: Order.STC, Order.STC: Order.BTO,
                Order.STO: Order.BTC, Order.BTC: Order.STO}
        return _inv[self]


def get_order(direction: Direction, signal: Signal) -> Order:
    """Map (direction, signal) to the appropriate Order type."""
    if direction == Direction.BUY:
        return Order.BTO if signal == Signal.ENTRY else Order.STC
    return Order.STO if signal == Signal.ENTRY else Order.BTC


# ---------------------------------------------------------------------------
# Value objects
# ---------------------------------------------------------------------------

@dataclass(frozen=True, slots=True)
class Greeks:
    """Option Greeks for a single contract or aggregated position.

    Supports addition (aggregation) and scalar multiplication (scaling by qty).
    """
    delta: float = 0.0
    gamma: float = 0.0
    theta: float = 0.0
    vega: float = 0.0

    def __add__(self, other: Greeks) -> Greeks:
        return Greeks(
            delta=self.delta + other.delta,
            gamma=self.gamma + other.gamma,
            theta=self.theta + other.theta,
            vega=self.vega + other.vega,
        )

    def __mul__(self, scalar: float) -> Greeks:
        return Greeks(
            delta=self.delta * scalar,
            gamma=self.gamma * scalar,
            theta=self.theta * scalar,
            vega=self.vega * scalar,
        )

    def __rmul__(self, scalar: float) -> Greeks:
        return self.__mul__(scalar)

    def __neg__(self) -> Greeks:
        return self * -1.0

    @property
    def as_dict(self) -> dict[str, float]:
        return {"delta": self.delta, "gamma": self.gamma,
                "theta": self.theta, "vega": self.vega}


@dataclass(frozen=True, slots=True)
class Fill:
    """A single execution fill.

    Captures price, quantity, commission, slippage, and computes notional.
    """
    price: float
    quantity: int
    direction: Direction
    shares_per_contract: int = 100
    commission: float = 0.0
    slippage: float = 0.0

    @property
    def direction_sign(self) -> int:
        return -1 if self.direction == Direction.BUY else 1

    @property
    def notional(self) -> float:
        """Net cash impact: direction_sign * (price * qty * spc) - commission - slippage."""
        raw = self.direction_sign * self.price * self.quantity * self.shares_per_contract
        return raw - self.commission - self.slippage


@dataclass(frozen=True, slots=True)
class OptionContract:
    """Identifies a specific option contract."""
    contract_id: str
    underlying: str
    expiration: Any  # pd.Timestamp or str
    option_type: OptionType
    strike: float


# Re-use namedtuple for backward compatibility
StockAllocation = namedtuple("StockAllocation", "symbol percentage")

# Backward-compatible aliases
Stock = StockAllocation
Type = OptionType


================================================
FILE: options_portfolio_backtester/data/__init__.py
================================================
"""Data module — schema and providers."""


================================================
FILE: options_portfolio_backtester/data/providers.py
================================================
"""Data providers — ABCs, CSV implementations, and data loaders."""

from __future__ import annotations

import os
from abc import ABC, abstractmethod
from typing import Any, Union

import pandas as pd

from .schema import Schema, Filter


class TiingoData:
    """Tiingo (stocks & indeces) Data container class."""
    def __init__(self, file: str, schema: Schema | None = None, **params: Any) -> None:
        if schema is None:
            self.schema = TiingoData.default_schema()

        file_extension = os.path.splitext(file)[1]

        if file_extension == '.h5':
            self._data: pd.DataFrame = pd.read_hdf(file, **params)
        elif file_extension == '.csv':
            params['parse_dates'] = [self.schema.date.mapping]
            self._data = pd.read_csv(file, **params)

        columns = self._data.columns
        assert all((col in columns for _key, col in self.schema))

        date_col = self.schema['date']

        self.start_date: pd.Timestamp = self._data[date_col].min()
        self.end_date: pd.Timestamp = self._data[date_col].max()

    def apply_filter(self, f: Filter) -> pd.DataFrame:
        """Apply Filter `f` to the data. Returns a `pd.DataFrame` with the filtered rows."""
        return self._data.query(f.query)

    def iter_dates(self) -> pd.core.groupby.DataFrameGroupBy:
        """Returns `pd.DataFrameGroupBy` that groups stocks by date"""
        return self._data.groupby(self.schema['date'])

    def iter_months(self) -> pd.core.groupby.DataFrameGroupBy:
        """Returns `pd.DataFrameGroupBy` that groups stocks by month"""
        date_col = self.schema['date']
        first_date_per_month = (
            self._data.groupby(self._data[date_col].dt.to_period('M'))[date_col]
            .min()
        )
        mask = self._data[date_col].isin(first_date_per_month.values)
        return self._data[mask].groupby(date_col)

    def __getattr__(self, attr: str) -> Any:
        """Pass method invocation to `self._data`"""

        method = getattr(self._data, attr)
        if hasattr(method, '__call__'):

            def df_method(*args: Any, **kwargs: Any) -> Any:
                return method(*args, **kwargs)

            return df_method
        else:
            return method

    def __getitem__(self, item: Union[str, pd.Series]) -> Union[pd.DataFrame, pd.Series]:
        if isinstance(item, pd.Series):
            return self._data[item]
        else:
            key = self.schema[item]
            return self._data[key]

    def __setitem__(self, key: str, value: Any) -> None:
        self._data[key] = value
        if key not in self.schema:
            self.schema.update({key: key})

    def __len__(self) -> int:
        return len(self._data)

    def __repr__(self) -> str:
        return self._data.__repr__()

    @staticmethod
    def default_schema() -> Schema:
        """Returns default schema for Tiingo Data"""
        return Schema.stocks()

    def sma(self, periods: int) -> None:
        sma = self._data.groupby('symbol', as_index=False).rolling(periods)['adjClose'].mean()
        sma = sma.fillna(0)
        sma.index = [index[1] for index in sma.index]
        self._data['sma'] = sma
        self.schema.update({'sma': 'sma'})


class HistoricalOptionsData:
    """Historical Options Data container class."""
    def __init__(self, file: str, schema: Schema | None = None, **params: Any) -> None:
        if schema is None:
            self.schema = HistoricalOptionsData.default_schema()

        file_extension = os.path.splitext(file)[1]

        if file_extension == '.h5':
            self._data: pd.DataFrame = pd.read_hdf(file, **params)
        elif file_extension == '.csv':
            params['parse_dates'] = [self.schema.expiration.mapping, self.schema.date.mapping]
            self._data = pd.read_csv(file, **params)

        columns = self._data.columns
        assert all((col in columns for _key, col in self.schema))

        date_col = self.schema['date']
        expiration_col = self.schema['expiration']

        self._data['dte'] = (self._data[expiration_col] - self._data[date_col]).dt.days
        self.schema.update({'dte': 'dte'})

        self.start_date: pd.Timestamp = self._data[date_col].min()
        self.end_date: pd.Timestamp = self._data[date_col].max()

    def apply_filter(self, f: Filter) -> pd.DataFrame:
        """Apply Filter `f` to the data. Returns a `pd.DataFrame` with the filtered rows."""
        return self._data.query(f.query)

    def iter_dates(self) -> pd.core.groupby.DataFrameGroupBy:
        """Returns `pd.DataFrameGroupBy` that groups contracts by date"""
        return self._data.groupby(self.schema['date'])

    def iter_months(self) -> pd.core.groupby.DataFrameGroupBy:
        """Returns `pd.DataFrameGroupBy` that groups contracts by month"""
        date_col = self.schema['date']
        first_date_per_month = (
            self._data.groupby(self._data[date_col].dt.to_period('M'))[date_col]
            .min()
        )
        mask = self._data[date_col].isin(first_date_per_month.values)
        return self._data[mask].groupby(date_col)

    def __getattr__(self, attr: str) -> Any:
        """Pass method invocation to `self._data`"""

        method = getattr(self._data, attr)
        if hasattr(method, '__call__'):

            def df_method(*args: Any, **kwargs: Any) -> Any:
                return method(*args, **kwargs)

            return df_method
        else:
            return method

    def __getitem__(self, item: Union[str, pd.Series]) -> Union[pd.DataFrame, pd.Series]:
        if isinstance(item, pd.Series):
            return self._data[item]
        else:
            key = self.schema[item]
            return self._data[key]

    def __setitem__(self, key: str, value: Any) -> None:
        self._data[key] = value
        if key not in self.schema:
            self.schema.update({key: key})

    def __len__(self) -> int:
        return len(self._data)

    def __repr__(self) -> str:
        return self._data.__repr__()

    @staticmethod
    def default_schema() -> Schema:
        """Returns default schema for Historical Options Data"""
        schema = Schema.options()
        schema.update({
            'contract': 'optionroot',
            'date': 'quotedate',
            'last': 'last',
            'open_interest': 'openinterest',
            'impliedvol': 'impliedvol',
            'delta': 'delta',
            'gamma': 'gamma',
            'theta': 'theta',
            'vega': 'vega'
        })
        return schema


# ---------------------------------------------------------------------------
# Abstract base classes
# ---------------------------------------------------------------------------

class DataProvider(ABC):
    """Base interface for all data providers."""

    @property
    @abstractmethod
    def schema(self) -> Schema:
        ...

    @property
    @abstractmethod
    def data(self) -> pd.DataFrame:
        ...

    @property
    @abstractmethod
    def start_date(self) -> pd.Timestamp:
        ...

    @property
    @abstractmethod
    def end_date(self) -> pd.Timestamp:
        ...

    @abstractmethod
    def apply_filter(self, f: Filter) -> pd.DataFrame:
        ...

    @abstractmethod
    def iter_dates(self) -> Any:
        ...

    @abstractmethod
    def iter_months(self) -> Any:
        ...


class OptionsDataProvider(DataProvider):
    """Options-specific data provider interface."""
    pass


class StocksDataProvider(DataProvider):
    """Stocks-specific data provider interface."""

    @abstractmethod
    def sma(self, periods: int) -> None:
        ...


# ---------------------------------------------------------------------------
# CSV implementations (wrap existing loaders)
# ---------------------------------------------------------------------------

class CsvOptionsProvider(OptionsDataProvider):
    """Load options data from CSV files using the existing HistoricalOptionsData loader."""

    def __init__(self, file: str, schema: Schema | None = None, **params: Any) -> None:
        self._loader = HistoricalOptionsData(file, schema=schema, **params)

    @property
    def schema(self) -> Schema:
        return self._loader.schema

    @property
    def data(self) -> pd.DataFrame:
        return self._loader._data

    @property
    def start_date(self) -> pd.Timestamp:
        return self._loader.start_date

    @property
    def end_date(self) -> pd.Timestamp:
        return self._loader.end_date

    def apply_filter(self, f: Filter) -> pd.DataFrame:
        return self._loader.apply_filter(f)

    def iter_dates(self) -> Any:
        return self._loader.iter_dates()

    def iter_months(self) -> Any:
        return self._loader.iter_months()

    def __getitem__(self, item: Any) -> Any:
        return self._loader[item]

    def __setitem__(self, key: str, value: Any) -> None:
        self._loader[key] = value

    def __len__(self) -> int:
        return len(self._loader)

    @property
    def _data(self) -> pd.DataFrame:
        """Access to underlying DataFrame."""
        return self._loader._data


class CsvStocksProvider(StocksDataProvider):
    """Load stock data from CSV files using the existing TiingoData loader."""

    def __init__(self, file: str, schema: Schema | None = None, **params: Any) -> None:
        self._loader = TiingoData(file, schema=schema, **params)

    @property
    def schema(self) -> Schema:
        return self._loader.schema

    @property
    def data(self) -> pd.DataFrame:
        return self._loader._data

    @property
    def start_date(self) -> pd.Timestamp:
        return self._loader.start_date

    @property
    def end_date(self) -> pd.Timestamp:
        return self._loader.end_date

    def apply_filter(self, f: Filter) -> pd.DataFrame:
        return self._loader.apply_filter(f)

    def iter_dates(self) -> Any:
        return self._loader.iter_dates()

    def iter_months(self) -> Any:
        return self._loader.iter_months()

    def sma(self, periods: int) -> None:
        self._loader.sma(periods)

    def __getitem__(self, item: Any) -> Any:
        return self._loader[item]

    def __setitem__(self, key: str, value: Any) -> None:
        self._loader[key] = value

    def __len__(self) -> int:
        return len(self._loader)

    @property
    def _data(self) -> pd.DataFrame:
        """Access to underlying DataFrame."""
        return self._loader._data


================================================
FILE: options_portfolio_backtester/data/schema.py
================================================
"""Filter DSL — Schema, Field, and Filter for building query expressions."""

from __future__ import annotations

from typing import Any, Iterator, Union


class Schema:
    """Data schema class.
    Used provide uniform access to fields in the data set.
    """

    stock_columns = [
        "symbol", "date", "open", "close", "high", "low", "volume", "adjClose", "adjHigh", "adjLow", "adjOpen",
        "adjVolume", "divCash", "splitFactor"
    ]

    option_columns = [
        "underlying", "underlying_last", "date", "contract", "type", "expiration", "strike", "bid", "ask", "volume",
        "open_interest"
    ]

    @staticmethod
    def stocks() -> Schema:
        """Builder method that returns a `Schema` with default mappings for stocks"""
        mappings = {key: key for key in Schema.stock_columns}
        return Schema(mappings)

    @staticmethod
    def options() -> Schema:
        """Builder method that returns a `Schema` with default mappings for options"""
        mappings = {key: key for key in Schema.option_columns}
        return Schema(mappings)

    def __init__(self, mappings: dict[str, str]) -> None:
        assert all((key in mappings for key in Schema.stock_columns)) or all(
            (key in mappings for key in Schema.option_columns))

        self._mappings: dict[str, str] = mappings

    def update(self, mappings: dict[str, str]) -> Schema:
        """Update schema according to given `mappings`"""
        self._mappings.update(mappings)
        return self

    def __contains__(self, key: str) -> bool:
        """Returns True if key is in schema"""
        return key in self._mappings.keys()

    def __getattr__(self, key: str) -> Field:
        """Returns Field object used to build Filters"""
        return Field(key, self._mappings[key])

    def __setitem__(self, key: str, value: str) -> None:
        self._mappings[key] = value

    def __getitem__(self, key: str) -> str:
        """Returns mapping of given `key`"""
        return self._mappings[key]

    def __iter__(self) -> Iterator[tuple[str, str]]:
        return iter(self._mappings.items())

    def __repr__(self) -> str:
        return "Schema({})".format([Field(k, m) for k, m in self._mappings.items()])

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Schema):
            return NotImplemented
        return self._mappings == other._mappings


class Field:
    """Encapsulates data fields to build filters used by strategies"""

    __slots__ = ("name", "mapping")

    def __init__(self, name: str, mapping: str) -> None:
        self.name = name
        self.mapping = mapping

    def _create_filter(self, op: str, other: Union[Field, Any]) -> Filter:
        if isinstance(other, Field):

            query = Field._format_query(self.mapping, op, other.mapping)
        else:
            query = Field._format_query(self.mapping, op, other)
        return Filter(query)

    def _combine_fields(self, op: str, other: Union[Field, int, float], invert: bool = False) -> Field:
        if isinstance(other, Field):
            name = Field._format_query(self.name, op, other.name, invert)
            mapping = Field._format_query(self.mapping, op, other.mapping, invert)
        elif isinstance(other, (int, float)):
            name = Field._format_query(self.name, op, other, invert)
            mapping = Field._format_query(self.mapping, op, other, invert)
        else:
            raise TypeError

        return Field(name, mapping)

    @staticmethod
    def _format_query(left: Any, op: str, right: Any, invert: bool = False) -> str:
        if invert:
            left, right = right, left
        query = "{left} {op} {right}".format(left=left, op=op, right=right)
        return query

    def __add__(self, value: Union[Field, int, float]) -> Field:
        return self._combine_fields("+", value)

    def __radd__(self, value: Union[Field, int, float]) -> Field:
        return self._combine_fields("+", value, invert=True)

    def __sub__(self, value: Union[Field, int, float]) -> Field:
        return self._combine_fields("-", value)

    def __rsub__(self, value: Union[Field, int, float]) -> Field:
        return self._combine_fields("-", value, invert=True)

    def __mul__(self, value: Union[Field, int, float]) -> Field:
        return self._combine_fields("*", value)

    def __rmul__(self, value: Union[Field, int, float]) -> Field:
        return self._combine_fields("*", value, invert=True)

    def __truediv__(self, value: Union[Field, int, float]) -> Field:
        return self._combine_fields("/", value)

    def __rtruediv__(self, value: Union[Field, int, float]) -> Field:
        return self._combine_fields("/", value, invert=True)

    def __lt__(self, value: Union[Field, Any]) -> Filter:
        return self._create_filter("<", value)

    def __le__(self, value: Union[Field, Any]) -> Filter:
        return self._create_filter("<=", value)

    def __gt__(self, value: Union[Field, Any]) -> Filter:
        return self._create_filter(">", value)

    def __ge__(self, value: Union[Field, Any]) -> Filter:
        return self._create_filter(">=", value)

    def __eq__(self, value: Union[Field, Any]) -> Filter:  # type: ignore[override]
        if isinstance(value, str):
            value = "'{}'".format(value)
        return self._create_filter("==", value)

    def __ne__(self, value: Union[Field, Any]) -> Filter:  # type: ignore[override]
        return self._create_filter("!=", value)

    def __repr__(self) -> str:
        return "Field(name='{}', mapping='{}')".format(self.name, self.mapping)


class Filter:
    """This class determines entry/exit conditions for strategies"""

    __slots__ = ("query")

    def __init__(self, query: str) -> None:
        self.query = query

    def __and__(self, other: Filter) -> Filter:
        """Returns logical *and* between `self` and `other`"""
        assert isinstance(other, Filter)
        new_query = "({}) & ({})".format(self.query, other.query)
        return Filter(query=new_query)

    def __or__(self, other: Filter) -> Filter:
        """Returns logical *or* between `self` and `other`"""
        assert isinstance(other, Filter)
        new_query = "(({}) | ({}))".format(self.query, other.query)
        return Filter(query=new_query)

    def __invert__(self) -> Filter:
        """Negates filter"""
        return Filter("!({})".format(self.query))

    def __call__(self, data: 'pd.DataFrame') -> 'pd.Series':
        """Returns dataframe of filtered data"""
        return data.eval(self.query)

    def __repr__(self) -> str:
        return "Filter(query='{}')".format(self.query)


__all__ = ["Schema", "Field", "Filter"]


================================================
FILE: options_portfolio_backtester/engine/__init__.py
================================================


================================================
FILE: options_portfolio_backtester/engine/algo_adapters.py
================================================
"""Algo adapter layer to drive BacktestEngine with bt-style pipeline blocks."""

from __future__ import annotations

import math
from dataclasses import dataclass, field
from typing import Literal, Protocol

import pandas as pd

from options_portfolio_backtester.core.types import Greeks


StepStatus = Literal["continue", "skip_day", "stop"]


@dataclass(frozen=True)
class EngineStepDecision:
    """Decision emitted by one engine-algo step."""

    status: StepStatus = "continue"
    message: str = ""


@dataclass
class EnginePipelineContext:
    """Mutable run context shared by all engine algo steps for one rebalance date."""

    date: pd.Timestamp
    stocks: pd.DataFrame
    options: pd.DataFrame
    total_capital: float
    current_cash: float
    current_greeks: Greeks
    options_allocation: float
    entry_filters: list = field(default_factory=list)
    exit_threshold_override: tuple[float, float] | None = None


class EngineAlgo(Protocol):
    def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
        ...


class EngineRunMonthly:
    """Allow rebalances only on first rebalance day per month."""

    def __init__(self) -> None:
        self._last_month: tuple[int, int] | None = None

    def reset(self) -> None:
        self._last_month = None

    def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
        key = (ctx.date.year, ctx.date.month)
        if self._last_month == key:
            return EngineStepDecision(status="skip_day", message="not month-start")
        self._last_month = key
        return EngineStepDecision()


class BudgetPercent:
    """Set options allocation budget as percent of current total capital."""

    def __init__(self, pct: float) -> None:
        self.pct = float(pct)

    def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
        ctx.options_allocation = max(0.0, float(ctx.total_capital) * self.pct)
        return EngineStepDecision()


class RangeFilter:
    """Keep contracts where *column* falls within [min_val, max_val].

    Generic building block — use directly or via the convenience aliases
    ``SelectByDelta``, ``SelectByDTE``, ``IVRankFilter``.
    """

    def __init__(self, column: str, min_val: float, max_val: float) -> None:
        self.column = column
        self.min_val = float(min_val)
        self.max_val = float(max_val)

    def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
        lo, hi, col = self.min_val, self.max_val, self.column

        def _flt(df: pd.DataFrame) -> pd.Series:
            if col not in df.columns:
                return pd.Series(True, index=df.index)
            v = df[col]
            return (v >= lo) & (v <= hi)

        ctx.entry_filters.append(_flt)
        return EngineStepDecision()


def SelectByDelta(min_delta: float = -1.0, max_delta: float = 1.0, column: str = "delta") -> RangeFilter:
    """Keep contracts with delta within [min_delta, max_delta]."""
    return RangeFilter(column=column, min_val=min_delta, max_val=max_delta)


def SelectByDTE(min_dte: int = 0, max_dte: int = 10_000, column: str = "dte") -> RangeFilter:
    """Keep contracts with DTE within [min_dte, max_dte]."""
    return RangeFilter(column=column, min_val=float(min_dte), max_val=float(max_dte))


def IVRankFilter(min_rank: float = 0.0, max_rank: float = 1.0, column: str = "iv_rank") -> RangeFilter:
    """Keep contracts with IV rank within [min_rank, max_rank]."""
    return RangeFilter(column=column, min_val=min_rank, max_val=max_rank)


class MaxGreekExposure:
    """Skip new entries when current absolute greek exposure exceeds limits."""

    def __init__(
        self,
        max_abs_delta: float | None = None,
        max_abs_vega: float | None = None,
    ) -> None:
        self.max_abs_delta = float(max_abs_delta) if max_abs_delta is not None else None
        self.max_abs_vega = float(max_abs_vega) if max_abs_vega is not None else None

    def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
        if self.max_abs_delta is not None and abs(float(ctx.current_greeks.delta)) > self.max_abs_delta:
            return EngineStepDecision(
                status="skip_day",
                message=f"|delta|>{self.max_abs_delta}",
            )
        if self.max_abs_vega is not None and abs(float(ctx.current_greeks.vega)) > self.max_abs_vega:
            return EngineStepDecision(
                status="skip_day",
                message=f"|vega|>{self.max_abs_vega}",
            )
        return EngineStepDecision()


class ExitOnThreshold:
    """Override strategy exit profit/loss thresholds for this run.

    At least one of *profit_pct* or *loss_pct* must be finite, otherwise the
    algo is a no-op and likely a caller mistake.
    """

    def __init__(self, profit_pct: float = float("inf"), loss_pct: float = float("inf")) -> None:
        self.profit_pct = float(profit_pct)
        self.loss_pct = float(loss_pct)
        if math.isinf(self.profit_pct) and math.isinf(self.loss_pct):
            import warnings
            warnings.warn(
                "ExitOnThreshold created with both thresholds infinite — "
                "exit overrides will have no effect",
                stacklevel=2,
            )

    def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
        ctx.exit_threshold_override = (self.profit_pct, self.loss_pct)
        return EngineStepDecision()


================================================
FILE: options_portfolio_backtester/engine/clock.py
================================================
"""Trading clock — date iteration and rebalance scheduling."""

from __future__ import annotations

from typing import Generator

import pandas as pd


class TradingClock:
    """Generates (date, stocks_df, options_df) tuples for the backtest loop.

    Handles daily/monthly iteration and rebalance scheduling.
    """

    def __init__(
        self,
        stocks_data: pd.DataFrame,
        options_data: pd.DataFrame,
        stocks_date_col: str = "date",
        options_date_col: str = "quotedate",
        monthly: bool = False,
    ) -> None:
        self.stocks_data = stocks_data
        self.options_data = options_data
        self.stocks_date_col = stocks_date_col
        self.options_date_col = options_date_col
        self.monthly = monthly

    def iter_dates(self) -> Generator[tuple[pd.Timestamp, pd.DataFrame, pd.DataFrame], None, None]:
        """Iterate over trading dates, yielding (date, stocks, options) per step."""
        if self.monthly:
            stocks_iter = self._monthly_iter(self.stocks_data, self.stocks_date_col)
            options_iter = self._monthly_iter(self.options_data, self.options_date_col)
        else:
            stocks_iter = self.stocks_data.groupby(self.stocks_date_col)
            options_iter = self.options_data.groupby(self.options_date_col)

        for (date, stocks), (_, options) in zip(stocks_iter, options_iter):
            yield date, stocks, options

    def rebalance_dates(self, freq: int) -> pd.DatetimeIndex:
        """Compute rebalance dates using business-month-start frequency.

        Args:
            freq: Number of business months between rebalances.

        Returns:
            DatetimeIndex of rebalance dates present in the data.
        """
        if freq <= 0:
            return pd.DatetimeIndex([])

        dates = pd.DataFrame(
            self.options_data[[self.options_date_col, "volume"]]
        ).drop_duplicates(self.options_date_col).set_index(self.options_date_col)

        return pd.to_datetime(
            dates.groupby(pd.Grouper(freq=f"{freq}BMS"))
            .apply(lambda x: x.index.min())
            .values
        )

    @staticmethod
    def _monthly_iter(data: pd.DataFrame, date_col: str):
        first_date_per_month = (
            data.groupby(data[date_col].dt.to_period('M'))[date_col]
            .min()
        )
        mask = data[date_col].isin(first_date_per_month.values)
        return data[mask].groupby(date_col)

    @property
    def all_dates(self) -> pd.DatetimeIndex:
        return pd.DatetimeIndex(self.options_data[self.options_date_col].unique())


================================================
FILE: options_portfolio_backtester/engine/engine.py
================================================
"""BacktestEngine — thin orchestrator composing all framework components.

Replaces the monolithic Backtest class with a clean composition of:
- Data providers (stocks, options)
- Strategy (legs, filters, thresholds)
- Execution (cost model, fill model, sizer, signal selector)
- Portfolio (positions, cash, holdings)
- Risk management (constraints)
- Analytics (trade log, balance sheet)
"""

from __future__ import annotations

import hashlib
import json
import logging
import subprocess
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import numpy as np
import pandas as pd

from options_portfolio_backtester.core.types import (
    Direction, OptionType, Order, Signal, Greeks, Stock, StockAllocation,
    get_order,
)
from options_portfolio_backtester.execution.cost_model import TransactionCostModel, NoCosts
from options_portfolio_backtester.execution.fill_model import FillModel, MarketAtBidAsk
from options_portfolio_backtester.execution.sizer import PositionSizer, CapitalBased
from options_portfolio_backtester.execution.signal_selector import SignalSelector, FirstMatch
from options_portfolio_backtester.portfolio.risk import RiskManager
from options_portfolio_backtester.portfolio.portfolio import Portfolio
from options_portfolio_backtester import _ob_rust
from options_portfolio_backtester.engine.algo_adapters import (
    EngineAlgo,
    EnginePipelineContext,
)

from options_portfolio_backtester.data.providers import HistoricalOptionsData, TiingoData
from options_portfolio_backtester.data.schema import Schema
from options_portfolio_backtester.strategy.strategy import Strategy
from options_portfolio_backtester.strategy.strategy_leg import StrategyLeg

logger = logging.getLogger(__name__)


def _intrinsic_value(option_type: str, strike: float, underlying_price: float) -> float:
    """Compute intrinsic value of an option given spot price.

    For puts:  max(0, strike - spot)
    For calls: max(0, spot - strike)
    """
    if option_type == OptionType.CALL.value:
        return max(0.0, underlying_price - strike)
    return max(0.0, strike - underlying_price)


@dataclass
class _StrategySlot:
    """Configuration and runtime state for one strategy within a multi-strategy engine."""
    strategy: Strategy
    weight: float
    rebalance_freq: int
    rebalance_unit: str = 'BMS'
    check_exits_daily: bool = False
    name: str = ""
    inventory: pd.DataFrame = field(default=None, repr=False)
    rebalance_dates: pd.DatetimeIndex = field(default=None, repr=False)


class BacktestEngine:
    """Orchestrates backtest with pluggable execution components.

    Composes data providers, strategy legs, cost/fill/sizer/selector models,
    and risk constraints into a single backtest loop.  Dispatches to Rust
    for all supported configurations.
    """

    def __init__(
        self,
        allocation: dict[str, float],
        initial_capital: int = 1_000_000,
        shares_per_contract: int = 100,
        cost_model: TransactionCostModel | None = None,
        fill_model: FillModel | None = None,
        sizer: PositionSizer | None = None,
        signal_selector: SignalSelector | None = None,
        risk_manager: RiskManager | None = None,
        algos: list[EngineAlgo] | None = None,
        stop_if_broke: bool = False,
        max_notional_pct: float | None = None,
    ) -> None:
        assets = ("stocks", "options", "cash")
        self._raw_allocation = {a: allocation.get(a, 0.0) for a in assets}
        total_allocation = sum(self._raw_allocation.values())

        self.allocation: dict[str, float] = {}
        for asset in assets:
            self.allocation[asset] = self._raw_allocation[asset] / total_allocation

        self.initial_capital = initial_capital
        self.shares_per_contract = shares_per_contract
        self.cost_model = cost_model or NoCosts()
        self.fill_model = fill_model or MarketAtBidAsk()
        self.sizer = sizer or CapitalBased()
        self.signal_selector = signal_selector or FirstMatch()
        self.risk_manager = risk_manager or RiskManager()
        self.algos = list(algos or [])
        self.stop_if_broke = stop_if_broke
        self.max_notional_pct = max_notional_pct

        self.options_budget_pct: float | None = None
        self.options_budget_annual_pct: float | None = None
        self._stocks: list[Stock] = []
        self._options_strategy: Strategy | None = None
        self._stocks_data: TiingoData | None = None
        self._options_data: HistoricalOptionsData | None = None
        self.run_metadata: dict[str, Any] = {}
        self._event_log_rows: list[dict[str, Any]] = []

    # -- Properties (same API as original Backtest) --

    @property
    def stocks(self) -> list[Stock]:
        return self._stocks

    @stocks.setter
    def stocks(self, stocks: list[Stock]) -> None:
        assert np.isclose(sum(s.percentage for s in stocks), 1.0, atol=1e-6)
        self._stocks = list(stocks)

    @property
    def options_strategy(self) -> Strategy | None:
        return self._options_strategy

    @options_strategy.setter
    def options_strategy(self, strat: Strategy) -> None:
        self._options_strategy = strat

    @property
    def stocks_data(self) -> TiingoData | None:
        return self._stocks_data

    @stocks_data.setter
    def stocks_data(self, data: TiingoData) -> None:
        self._stocks_schema = data.schema
        self._stocks_data = data

    @property
    def options_data(self) -> HistoricalOptionsData | None:
        return self._options_data

    @options_data.setter
    def options_data(self, data: HistoricalOptionsData) -> None:
        self._options_schema = data.schema
        self._options_data = data

    # -- Multi-strategy API --

    def add_strategy(
        self,
        strategy: Strategy,
        weight: float,
        rebalance_freq: int,
        rebalance_unit: str = 'BMS',
        check_exits_daily: bool = False,
        name: str | None = None,
    ) -> None:
        """Register a strategy slot for multi-strategy mode.

        Args:
            strategy: The Strategy object (legs + exit thresholds).
            weight: Fraction of options allocation for this strategy.
            rebalance_freq: Rebalance every N periods.
            rebalance_unit: Pandas offset alias (default 'BMS').
            check_exits_daily: Check exits on non-rebalance days.
            name: Human-readable name (auto-generated if omitted).
        """
        if not hasattr(self, '_strategy_slots'):
            self._strategy_slots: list[_StrategySlot] = []
        slot_name = name or f"strategy_{len(self._strategy_slots)}"
        self._strategy_slots.append(_StrategySlot(
            strategy=strategy,
            weight=weight,
            rebalance_freq=rebalance_freq,
            rebalance_unit=rebalance_unit,
            check_exits_daily=check_exits_daily,
            name=slot_name,
        ))

    @property
    def _is_multi_strategy(self) -> bool:
        return hasattr(self, '_strategy_slots') and len(self._strategy_slots) > 0

    # -- Main entry point --

    def run(self, rebalance_freq: int = 0, monthly: bool = False,
            sma_days: int | None = None,
            rebalance_unit: str = 'BMS',
            check_exits_daily: bool = False) -> pd.DataFrame:
        """Run the backtest. Returns the trade log DataFrame.

        Args:
            check_exits_daily: When True, evaluate exit filters on every trading
                day (not just rebalancing days).  Positions that match the exit
                filter are closed and cash is updated, but no new entries or
                stock reallocation occurs outside rebalancing days.
        """
        self._event_log_rows = []
        for algo in self.algos:
            if hasattr(algo, "reset"):
                algo.reset()
        assert self._stocks_data, "Stock data not set"
        assert all(
            stock.symbol in self._stocks_data["symbol"].values
            for stock in self._stocks
        ), "Ensure all stocks in portfolio are present in the data"
        assert self._options_data, "Options data not set"

        # Multi-strategy mode
        if self._is_multi_strategy:
            total_weight = sum(s.weight for s in self._strategy_slots)
            assert abs(total_weight - 1.0) < 1e-6, (
                f"Strategy weights must sum to 1.0, got {total_weight}"
            )
            for slot in self._strategy_slots:
                assert self._options_data.schema == slot.strategy.schema
            return self._run_rust_multi(
                monthly=monthly, sma_days=sma_days,
                check_exits_daily=check_exits_daily,
            )

        assert self._options_strategy, "Options Strategy not set"
        assert self._options_data.schema == self._options_strategy.schema

        option_dates = self._options_data["date"].unique()
        stock_dates = self.stocks_data["date"].unique()
        assert np.array_equal(stock_dates, option_dates)

        # Translate algos to Rust-compatible config fields before dispatch.
        if self.algos:
            self._translate_algos_to_config()

        return self._run_rust(
            rebalance_freq,
            monthly=monthly,
            sma_days=sma_days,
            rebalance_unit=rebalance_unit,
            check_exits_daily=check_exits_daily,
        )

    def events_dataframe(self) -> pd.DataFrame:
        """Structured execution event log for debugging and audit.

        The ``data`` dict from each event is flattened into top-level columns
        so that the result can be filtered directly (e.g.
        ``df[df["cash"] > 0]``).
        """
        if not self._event_log_rows:
            return pd.DataFrame(columns=["date", "event", "status"])
        flat = []
        for row in self._event_log_rows:
            entry = {"date": row["date"], "event": row["event"], "status": row["status"]}
            entry.update(row.get("data", {}))
            flat.append(entry)
        return pd.DataFrame(flat)

    def _translate_algos_to_config(self) -> None:
        """Translate algo pipeline into Rust-compatible engine config fields.

        Each algo type maps to an existing Rust feature:
          - EngineRunMonthly → rebalance_unit='BMS' + rebalance_freq=1 (already handled)
          - BudgetPercent → options_budget_pct
          - RangeFilter/SelectByDelta/SelectByDTE/IVRankFilter → entry filter conjunction
          - MaxGreekExposure → risk_constraints (MaxDelta/MaxVega)
          - ExitOnThreshold → profit_pct/loss_pct on strategy

        After translation, self.algos is cleared so the Rust gate passes.
        """
        from options_portfolio_backtester.engine.algo_adapters import (
            EngineRunMonthly, BudgetPercent, RangeFilter,
            MaxGreekExposure, ExitOnThreshold,
        )
        from options_portfolio_backtester.portfolio.risk import RiskManager

        for algo in self.algos:
            if isinstance(algo, EngineRunMonthly):
                # Already handled by rebalance_unit='BMS' + rebalance_freq=1.
                # If user set algos=[EngineRunMonthly()], it's a no-op for Rust.
                pass
            elif isinstance(algo, BudgetPercent):
                self.options_budget_pct = algo.pct
            elif isinstance(algo, RangeFilter):
                # Append range condition to each leg's entry filter as conjunction.
                col, lo, hi = algo.column, algo.min_val, algo.max_val
                clause = f"({col} >= {lo}) & ({col} <= {hi})"
                for leg in self._options_strategy.legs:
                    existing = leg.entry_filter.query
                    if existing:
                        leg.entry_filter.query = f"({existing}) & ({clause})"
                    else:
                        leg.entry_filter.query = clause
            elif isinstance(algo, MaxGreekExposure):
                if algo.max_abs_delta is not None:
                    self.risk_manager.add_constraint(
                        type("MaxDelta", (), {
                            "to_rust_config": lambda self_: {"type": "MaxDelta", "limit": algo.max_abs_delta},
                            "is_allowed": lambda self_, cg, pg, pv, pk: (
                                abs(cg.delta + pg.delta) <= algo.max_abs_delta, ""
                            ),
                        })()
                    )
                if algo.max_abs_vega is not None:
                    self.risk_manager.add_constraint(
                        type("MaxVega", (), {
                            "to_rust_config": lambda self_: {"type": "MaxVega", "limit": algo.max_abs_vega},
                            "is_allowed": lambda self_, cg, pg, pv, pk: (
                                abs(cg.vega + pg.vega) <= algo.max_abs_vega, ""
                            ),
                        })()
                    )
            elif isinstance(algo, ExitOnThreshold):
                import math
                if not math.isinf(algo.profit_pct):
                    self._options_strategy.add_exit_thresholds(
                        profit_pct=algo.profit_pct,
                        loss_pct=self._options_strategy.exit_thresholds[1],
                    )
                if not math.isinf(algo.loss_pct):
                    self._options_strategy.add_exit_thresholds(
                        profit_pct=self._options_strategy.exit_thresholds[0],
                        loss_pct=algo.loss_pct,
                    )
            else:
                raise ValueError(
                    f"Unsupported algo type for Rust dispatch: {type(algo).__name__}. "
                    f"All execution runs through Rust; translate to config fields."
                )
        self.algos.clear()

    def _run_rust(
        self,
        rebalance_freq: int,
        monthly: bool,
        sma_days: int | None,
        rebalance_unit: str = 'BMS',
        check_exits_daily: bool = False,
    ) -> pd.DataFrame:
        """Run the backtest using the Rust full-loop implementation."""
        import math
        import pyarrow as pa
        import polars as pl

        strategy = self._options_strategy

        # Compute rebalance dates for the Rust backtest loop.
        dates_df = (
            pd.DataFrame(self.options_data._data[["quotedate", "volume"]])
            .drop_duplicates("quotedate")
            .set_index("quotedate")
        )
        if rebalance_freq:
            rebalancing_days = pd.to_datetime(
                dates_df.groupby(pd.Grouper(freq=f"{rebalance_freq}{rebalance_unit}"))
                .apply(lambda x: x.index.min())
                
Download .txt
gitextract_dxl517zn/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .python-version
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── benchmarks/
│   ├── benchmark_large_pipeline.py
│   ├── benchmark_matrix.py
│   ├── benchmark_rust_vs_python.py
│   ├── benchmark_sweep.py
│   └── compare_with_bt.py
├── data/
│   ├── README.md
│   ├── convert_optionsdx.py
│   ├── fetch_data.py
│   └── fetch_signals.py
├── flake.nix
├── options_portfolio_backtester/
│   ├── __init__.py
│   ├── analytics/
│   │   ├── __init__.py
│   │   ├── charts.py
│   │   ├── optimization.py
│   │   ├── stats.py
│   │   ├── summary.py
│   │   ├── tearsheet.py
│   │   └── trade_log.py
│   ├── convexity/
│   │   ├── __init__.py
│   │   ├── _utils.py
│   │   ├── allocator.py
│   │   ├── backtest.py
│   │   ├── config.py
│   │   ├── scoring.py
│   │   └── viz.py
│   ├── core/
│   │   ├── __init__.py
│   │   └── types.py
│   ├── data/
│   │   ├── __init__.py
│   │   ├── providers.py
│   │   └── schema.py
│   ├── engine/
│   │   ├── __init__.py
│   │   ├── algo_adapters.py
│   │   ├── clock.py
│   │   ├── engine.py
│   │   ├── multi_strategy.py
│   │   ├── pipeline.py
│   │   └── strategy_tree.py
│   ├── execution/
│   │   ├── __init__.py
│   │   ├── _rust_bridge.py
│   │   ├── cost_model.py
│   │   ├── fill_model.py
│   │   ├── signal_selector.py
│   │   └── sizer.py
│   ├── portfolio/
│   │   ├── __init__.py
│   │   ├── greeks.py
│   │   ├── portfolio.py
│   │   ├── position.py
│   │   └── risk.py
│   └── strategy/
│       ├── __init__.py
│       ├── presets.py
│       ├── strategy.py
│       └── strategy_leg.py
├── pyproject.toml
├── rust/
│   ├── .cargo/
│   │   └── config.toml
│   ├── Cargo.toml
│   ├── ob_core/
│   │   ├── Cargo.toml
│   │   ├── benches/
│   │   │   └── hot_paths.rs
│   │   └── src/
│   │       ├── backtest.rs
│   │       ├── balance.rs
│   │       ├── convexity_backtest.rs
│   │       ├── convexity_scoring.rs
│   │       ├── cost_model.rs
│   │       ├── entries.rs
│   │       ├── exits.rs
│   │       ├── fill_model.rs
│   │       ├── filter.rs
│   │       ├── inventory.rs
│   │       ├── lib.rs
│   │       ├── risk.rs
│   │       ├── signal_selector.rs
│   │       ├── stats.rs
│   │       └── types.rs
│   └── ob_python/
│       ├── Cargo.toml
│       └── src/
│           ├── arrow_bridge.rs
│           ├── lib.rs
│           ├── py_backtest.rs
│           ├── py_balance.rs
│           ├── py_convexity.rs
│           ├── py_entries.rs
│           ├── py_execution.rs
│           ├── py_exits.rs
│           ├── py_filter.rs
│           ├── py_stats.rs
│           └── py_sweep.rs
├── setup.cfg
└── tests/
    ├── __init__.py
    ├── analytics/
    │   ├── __init__.py
    │   ├── test_analytics_pbt.py
    │   ├── test_charts.py
    │   ├── test_optimization.py
    │   ├── test_stats.py
    │   ├── test_stats_python_path.py
    │   ├── test_summary.py
    │   ├── test_tearsheet.py
    │   └── test_trade_log.py
    ├── bench/
    │   ├── __init__.py
    │   ├── _test_helpers.py
    │   ├── extract_prod_slices.py
    │   ├── generate_test_data.py
    │   ├── test_edge_cases.py
    │   ├── test_execution_models.py
    │   ├── test_invariants.py
    │   ├── test_multi_leg.py
    │   ├── test_partial_exits.py
    │   └── test_sweep.py
    ├── compat/
    │   ├── __init__.py
    │   └── test_bt_overlap_gate.py
    ├── conftest.py
    ├── convexity/
    │   ├── __init__.py
    │   ├── conftest.py
    │   ├── test_allocator.py
    │   ├── test_backtest.py
    │   └── test_config.py
    ├── core/
    │   ├── __init__.py
    │   ├── test_types.py
    │   └── test_types_pbt.py
    ├── data/
    │   ├── __init__.py
    │   ├── test_filter.py
    │   ├── test_property_based.py
    │   ├── test_providers.py
    │   ├── test_providers_extended.py
    │   └── test_schema.py
    ├── engine/
    │   ├── __init__.py
    │   ├── test_algo_adapters.py
    │   ├── test_capital_conservation.py
    │   ├── test_chaos.py
    │   ├── test_clock.py
    │   ├── test_engine.py
    │   ├── test_engine_deep.py
    │   ├── test_engine_unit.py
    │   ├── test_full_liquidation.py
    │   ├── test_max_notional.py
    │   ├── test_multi_strategy.py
    │   ├── test_multi_strategy_engine.py
    │   ├── test_per_leg_overrides.py
    │   ├── test_pipeline.py
    │   ├── test_portfolio_integration.py
    │   ├── test_regression_snapshots.py
    │   ├── test_risk_wiring.py
    │   ├── test_rust_parity.py
    │   ├── test_signal_selector_wiring.py
    │   └── test_strategy_tree.py
    ├── execution/
    │   ├── __init__.py
    │   ├── test_cost_model.py
    │   ├── test_execution_deep.py
    │   ├── test_execution_pbt.py
    │   ├── test_fill_model.py
    │   ├── test_rust_parity_execution.py
    │   ├── test_signal_selector.py
    │   └── test_sizer.py
    ├── portfolio/
    │   ├── __init__.py
    │   ├── test_greeks_aggregation.py
    │   ├── test_portfolio.py
    │   ├── test_position.py
    │   ├── test_property_based.py
    │   └── test_risk.py
    ├── strategy/
    │   ├── __init__.py
    │   ├── test_presets.py
    │   ├── test_strangle.py
    │   ├── test_strategy.py
    │   ├── test_strategy_deep.py
    │   ├── test_strategy_leg.py
    │   └── test_strategy_pbt.py
    ├── test_cleanup.py
    ├── test_data/
    │   ├── ivy_5assets_data.csv
    │   ├── ivy_portfolio.csv
    │   ├── options_data.csv
    │   ├── test_data_options.csv
    │   └── test_data_stocks.csv
    ├── test_deep_analytics_convexity.py
    ├── test_intrinsic_sign.py
    ├── test_intrinsic_value.py
    ├── test_property_based.py
    └── test_smoke.py
Download .txt
Showing preview only (238K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2830 symbols across 139 files)

FILE: benchmarks/benchmark_large_pipeline.py
  class BenchResult (line 44) | class BenchResult:
  function parse_args (line 56) | def parse_args() -> argparse.Namespace:
  function _load_data (line 75) | def _load_data():
  function _strategy (line 90) | def _strategy(schema, dte_min, dte_max, dte_exit):
  function _copy_data (line 103) | def _copy_data(stocks_data, options_data):
  function run_engine (line 115) | def run_engine(
  function run_legacy (line 165) | def run_legacy(stocks_data, options_data, args, runs) -> BenchResult:
  function print_result (line 204) | def print_result(r: BenchResult, indent: str = "  ") -> None:
  function print_comparison (line 215) | def print_comparison(a: BenchResult, b: BenchResult) -> None:
  function main (line 231) | def main() -> None:

FILE: benchmarks/benchmark_matrix.py
  class Scenario (line 26) | class Scenario:
  function parse_args (line 34) | def parse_args() -> argparse.Namespace:
  function parse_csv_list (line 47) | def parse_csv_list(s: str, cast):
  function normalize_weights (line 51) | def normalize_weights(symbols: list[str], raw_weights: str | None) -> li...
  function compute_metrics (line 63) | def compute_metrics(total_capital: pd.Series) -> tuple[float, float, flo...
  function slice_stocks_data (line 79) | def slice_stocks_data(stocks_file: str, start: pd.Timestamp, end: pd.Tim...
  function run_options_portfolio_backtester (line 88) | def run_options_portfolio_backtester(
  function run_bt (line 119) | def run_bt(
  function overlap_parity (line 166) | def overlap_parity(ob_eq: pd.Series, bt_eq: pd.Series | None) -> dict[st...
  function build_scenarios (line 182) | def build_scenarios(args: argparse.Namespace) -> list[Scenario]:
  function main (line 209) | def main() -> None:

FILE: benchmarks/benchmark_rust_vs_python.py
  class BenchResult (line 46) | class BenchResult:
  function parse_args (line 55) | def parse_args() -> argparse.Namespace:
  function _stocks (line 64) | def _stocks(use_prod: bool = False):
  function _load_data (line 71) | def _load_data(use_prod: bool):
  function _buy_strategy (line 94) | def _buy_strategy(schema):
  function run_engine_python (line 107) | def run_engine_python(stocks_data, options_data, stocks, rebalance_freq,...
  function run_engine_rust (line 152) | def run_engine_rust(stocks_data, options_data, stocks, rebalance_freq, r...
  function run_legacy_python (line 196) | def run_legacy_python(stocks_data, options_data, stocks, rebalance_freq,...
  function run_bt_stock_only (line 233) | def run_bt_stock_only(stocks_file, symbols, weights, initial_capital, ru...
  function run_ob_stock_only (line 276) | def run_ob_stock_only(stocks_file, symbols, weights, initial_capital, ru...
  function print_result (line 309) | def print_result(r: BenchResult) -> None:
  function print_comparison (line 318) | def print_comparison(a: BenchResult, b: BenchResult) -> None:
  function main (line 330) | def main() -> None:

FILE: benchmarks/benchmark_sweep.py
  function parse_args (line 46) | def parse_args():
  function _load_data (line 55) | def _load_data(use_prod: bool):
  function _build_param_grid (line 78) | def _build_param_grid(n: int, underlying: str = "SPX") -> list[dict]:
  function _build_rust_config (line 109) | def _build_rust_config(stocks_data, options_data, stocks, underlying="SP...
  function run_rust_sweep (line 181) | def run_rust_sweep(opts_pl, stocks_pl, config, schema_mapping, param_gri...
  function run_python_sequential (line 197) | def run_python_sequential(stocks_data, options_data, stocks, param_grid,...
  function run_rust_single (line 256) | def run_rust_single(opts_pl, stocks_pl, config, schema_mapping, runs):
  function run_python_single (line 268) | def run_python_single(stocks_data, options_data, stocks, runs, underlyin...
  function main (line 308) | def main():

FILE: benchmarks/compare_with_bt.py
  class RunResult (line 29) | class RunResult:
  function parse_args (line 43) | def parse_args() -> argparse.Namespace:
  function normalize_weights (line 55) | def normalize_weights(symbols: list[str], raw_weights: str | None) -> li...
  function compute_metrics (line 67) | def compute_metrics(total_capital: pd.Series) -> tuple[float, float, flo...
  function run_options_portfolio_backtester (line 86) | def run_options_portfolio_backtester(
  function run_bt (line 127) | def run_bt(
  function print_result (line 178) | def print_result(r: RunResult) -> None:
  function print_overlap_parity (line 189) | def print_overlap_parity(a: RunResult, b: RunResult) -> None:
  function main (line 203) | def main() -> None:

FILE: data/convert_optionsdx.py
  function make_optionroot (line 64) | def make_optionroot(expire_dates, option_type, strikes):
  function convert (line 76) | def convert(input_path, output_path):
  function main (line 113) | def main():

FILE: data/fetch_data.py
  function _download (line 47) | def _download(url, dest, force=False):
  function download_options_parquet (line 70) | def download_options_parquet(symbol, force=False):
  function download_underlying (line 87) | def download_underlying(symbol, force=False):
  function read_underlying_prices (line 132) | def read_underlying_prices(symbol, und_path, start, end):
  function underlying_to_tiingo (line 142) | def underlying_to_tiingo(symbol, und_path, start, end):
  function fetch_yfinance (line 171) | def fetch_yfinance(symbol, start, end):
  function fetch_options (line 213) | def fetch_options(symbols, start, end, output, force=False):
  function fetch_stocks (line 298) | def fetch_stocks(symbols, start, end, output, force=False):
  function align_dates (line 337) | def align_dates(stocks_path, options_path):
  function main (line 369) | def main():

FILE: data/fetch_signals.py
  function fetch_fred (line 36) | def fetch_fred(series_id: str) -> pd.Series:
  function main (line 50) | def main():

FILE: options_portfolio_backtester/analytics/charts.py
  function returns_chart (line 9) | def returns_chart(report: pd.DataFrame) -> alt.VConcatChart:
  function returns_histogram (line 46) | def returns_histogram(report: pd.DataFrame) -> alt.Chart:
  function monthly_returns_heatmap (line 54) | def monthly_returns_heatmap(report: pd.DataFrame) -> alt.Chart:
  function weights_chart (line 68) | def weights_chart(balance: pd.DataFrame, figsize: tuple[float, float] = ...

FILE: options_portfolio_backtester/analytics/optimization.py
  class OptimizationResult (line 17) | class OptimizationResult:
  function grid_sweep (line 24) | def grid_sweep(
  function walk_forward (line 64) | def walk_forward(
  function rust_grid_sweep (line 110) | def rust_grid_sweep(

FILE: options_portfolio_backtester/analytics/stats.py
  class PeriodStats (line 25) | class PeriodStats:
  class LookbackReturns (line 38) | class LookbackReturns:
  class BacktestStats (line 51) | class BacktestStats:
    method from_balance_range (line 94) | def from_balance_range(
    method from_balance (line 115) | def from_balance(
    method to_dataframe (line 184) | def to_dataframe(self) -> pd.DataFrame:
    method summary (line 252) | def summary(self) -> str:
    method lookback_table (line 275) | def lookback_table(self) -> pd.DataFrame:

FILE: options_portfolio_backtester/analytics/summary.py
  function summary (line 11) | def summary(trade_log: pd.DataFrame, balance: pd.DataFrame) -> pd.io.for...

FILE: options_portfolio_backtester/analytics/tearsheet.py
  class TearsheetReport (line 15) | class TearsheetReport:
    method to_dict (line 23) | def to_dict(self) -> dict[str, object]:
    method to_csv (line 31) | def to_csv(self, directory: str | Path) -> dict[str, Path]:
    method to_markdown (line 46) | def to_markdown(self) -> str:
    method to_html (line 62) | def to_html(self) -> str:
  function monthly_return_table (line 80) | def monthly_return_table(balance: pd.DataFrame) -> pd.DataFrame:
  function drawdown_series (line 93) | def drawdown_series(balance: pd.DataFrame) -> pd.Series:
  function build_tearsheet (line 103) | def build_tearsheet(

FILE: options_portfolio_backtester/analytics/trade_log.py
  class Trade (line 15) | class Trade:
    method gross_pnl (line 33) | def gross_pnl(self) -> float:
    method net_pnl (line 38) | def net_pnl(self) -> float:
    method return_pct (line 43) | def return_pct(self) -> float:
  class TradeLog (line 51) | class TradeLog:
    method __init__ (line 54) | def __init__(self) -> None:
    method add_trade (line 57) | def add_trade(self, trade: Trade) -> None:
    method from_legacy_trade_log (line 61) | def from_legacy_trade_log(cls, trade_log: pd.DataFrame,
    method to_dataframe (line 106) | def to_dataframe(self) -> pd.DataFrame:
    method net_pnls (line 131) | def net_pnls(self) -> np.ndarray:
    method winners (line 135) | def winners(self) -> list[Trade]:
    method losers (line 139) | def losers(self) -> list[Trade]:
    method __len__ (line 142) | def __len__(self) -> int:

FILE: options_portfolio_backtester/convexity/_utils.py
  function _to_ns (line 9) | def _to_ns(series: pd.Series) -> np.ndarray:

FILE: options_portfolio_backtester/convexity/allocator.py
  function pick_cheapest (line 6) | def pick_cheapest(scores: dict[str, float]) -> str:
  function allocate_equal_weight (line 13) | def allocate_equal_weight(symbols: list[str], budget: float) -> dict[str...
  function allocate_inverse_vol (line 21) | def allocate_inverse_vol(vol_map: dict[str, float], budget: float) -> di...

FILE: options_portfolio_backtester/convexity/backtest.py
  function _to_ns (line 16) | def _to_ns(series: pd.Series) -> np.ndarray:
  class BacktestResult (line 22) | class BacktestResult:
  function run_backtest (line 30) | def run_backtest(
  function run_unhedged (line 107) | def run_unhedged(stocks_data, config: BacktestConfig) -> pd.DataFrame:

FILE: options_portfolio_backtester/convexity/config.py
  class InstrumentConfig (line 9) | class InstrumentConfig:
  class BacktestConfig (line 22) | class BacktestConfig:
  function default_config (line 34) | def default_config(

FILE: options_portfolio_backtester/convexity/scoring.py
  function _to_ns (line 15) | def _to_ns(series: pd.Series) -> np.ndarray:
  function compute_convexity_scores (line 20) | def compute_convexity_scores(

FILE: options_portfolio_backtester/convexity/viz.py
  function convexity_scores_chart (line 9) | def convexity_scores_chart(scores_df: pd.DataFrame) -> alt.Chart:
  function monthly_pnl_chart (line 24) | def monthly_pnl_chart(records: pd.DataFrame) -> alt.Chart:
  function cumulative_pnl_chart (line 44) | def cumulative_pnl_chart(results: dict[str, pd.DataFrame]) -> alt.Chart:

FILE: options_portfolio_backtester/core/types.py
  class OptionType (line 19) | class OptionType(Enum):
    method __invert__ (line 23) | def __invert__(self) -> OptionType:
  class Direction (line 27) | class Direction(Enum):
    method price_column (line 33) | def price_column(self) -> str:
    method __invert__ (line 37) | def __invert__(self) -> Direction:
  class Signal (line 41) | class Signal(Enum):
  class Order (line 46) | class Order(Enum):
    method __invert__ (line 52) | def __invert__(self) -> Order:
  function get_order (line 58) | def get_order(direction: Direction, signal: Signal) -> Order:
  class Greeks (line 70) | class Greeks:
    method __add__ (line 80) | def __add__(self, other: Greeks) -> Greeks:
    method __mul__ (line 88) | def __mul__(self, scalar: float) -> Greeks:
    method __rmul__ (line 96) | def __rmul__(self, scalar: float) -> Greeks:
    method __neg__ (line 99) | def __neg__(self) -> Greeks:
    method as_dict (line 103) | def as_dict(self) -> dict[str, float]:
  class Fill (line 109) | class Fill:
    method direction_sign (line 122) | def direction_sign(self) -> int:
    method notional (line 126) | def notional(self) -> float:
  class OptionContract (line 133) | class OptionContract:

FILE: options_portfolio_backtester/data/providers.py
  class TiingoData (line 14) | class TiingoData:
    method __init__ (line 16) | def __init__(self, file: str, schema: Schema | None = None, **params: ...
    method apply_filter (line 36) | def apply_filter(self, f: Filter) -> pd.DataFrame:
    method iter_dates (line 40) | def iter_dates(self) -> pd.core.groupby.DataFrameGroupBy:
    method iter_months (line 44) | def iter_months(self) -> pd.core.groupby.DataFrameGroupBy:
    method __getattr__ (line 54) | def __getattr__(self, attr: str) -> Any:
    method __getitem__ (line 67) | def __getitem__(self, item: Union[str, pd.Series]) -> Union[pd.DataFra...
    method __setitem__ (line 74) | def __setitem__(self, key: str, value: Any) -> None:
    method __len__ (line 79) | def __len__(self) -> int:
    method __repr__ (line 82) | def __repr__(self) -> str:
    method default_schema (line 86) | def default_schema() -> Schema:
    method sma (line 90) | def sma(self, periods: int) -> None:
  class HistoricalOptionsData (line 98) | class HistoricalOptionsData:
    method __init__ (line 100) | def __init__(self, file: str, schema: Schema | None = None, **params: ...
    method apply_filter (line 124) | def apply_filter(self, f: Filter) -> pd.DataFrame:
    method iter_dates (line 128) | def iter_dates(self) -> pd.core.groupby.DataFrameGroupBy:
    method iter_months (line 132) | def iter_months(self) -> pd.core.groupby.DataFrameGroupBy:
    method __getattr__ (line 142) | def __getattr__(self, attr: str) -> Any:
    method __getitem__ (line 155) | def __getitem__(self, item: Union[str, pd.Series]) -> Union[pd.DataFra...
    method __setitem__ (line 162) | def __setitem__(self, key: str, value: Any) -> None:
    method __len__ (line 167) | def __len__(self) -> int:
    method __repr__ (line 170) | def __repr__(self) -> str:
    method default_schema (line 174) | def default_schema() -> Schema:
  class DataProvider (line 195) | class DataProvider(ABC):
    method schema (line 200) | def schema(self) -> Schema:
    method data (line 205) | def data(self) -> pd.DataFrame:
    method start_date (line 210) | def start_date(self) -> pd.Timestamp:
    method end_date (line 215) | def end_date(self) -> pd.Timestamp:
    method apply_filter (line 219) | def apply_filter(self, f: Filter) -> pd.DataFrame:
    method iter_dates (line 223) | def iter_dates(self) -> Any:
    method iter_months (line 227) | def iter_months(self) -> Any:
  class OptionsDataProvider (line 231) | class OptionsDataProvider(DataProvider):
  class StocksDataProvider (line 236) | class StocksDataProvider(DataProvider):
    method sma (line 240) | def sma(self, periods: int) -> None:
  class CsvOptionsProvider (line 248) | class CsvOptionsProvider(OptionsDataProvider):
    method __init__ (line 251) | def __init__(self, file: str, schema: Schema | None = None, **params: ...
    method schema (line 255) | def schema(self) -> Schema:
    method data (line 259) | def data(self) -> pd.DataFrame:
    method start_date (line 263) | def start_date(self) -> pd.Timestamp:
    method end_date (line 267) | def end_date(self) -> pd.Timestamp:
    method apply_filter (line 270) | def apply_filter(self, f: Filter) -> pd.DataFrame:
    method iter_dates (line 273) | def iter_dates(self) -> Any:
    method iter_months (line 276) | def iter_months(self) -> Any:
    method __getitem__ (line 279) | def __getitem__(self, item: Any) -> Any:
    method __setitem__ (line 282) | def __setitem__(self, key: str, value: Any) -> None:
    method __len__ (line 285) | def __len__(self) -> int:
    method _data (line 289) | def _data(self) -> pd.DataFrame:
  class CsvStocksProvider (line 294) | class CsvStocksProvider(StocksDataProvider):
    method __init__ (line 297) | def __init__(self, file: str, schema: Schema | None = None, **params: ...
    method schema (line 301) | def schema(self) -> Schema:
    method data (line 305) | def data(self) -> pd.DataFrame:
    method start_date (line 309) | def start_date(self) -> pd.Timestamp:
    method end_date (line 313) | def end_date(self) -> pd.Timestamp:
    method apply_filter (line 316) | def apply_filter(self, f: Filter) -> pd.DataFrame:
    method iter_dates (line 319) | def iter_dates(self) -> Any:
    method iter_months (line 322) | def iter_months(self) -> Any:
    method sma (line 325) | def sma(self, periods: int) -> None:
    method __getitem__ (line 328) | def __getitem__(self, item: Any) -> Any:
    method __setitem__ (line 331) | def __setitem__(self, key: str, value: Any) -> None:
    method __len__ (line 334) | def __len__(self) -> int:
    method _data (line 338) | def _data(self) -> pd.DataFrame:

FILE: options_portfolio_backtester/data/schema.py
  class Schema (line 8) | class Schema:
    method stocks (line 24) | def stocks() -> Schema:
    method options (line 30) | def options() -> Schema:
    method __init__ (line 35) | def __init__(self, mappings: dict[str, str]) -> None:
    method update (line 41) | def update(self, mappings: dict[str, str]) -> Schema:
    method __contains__ (line 46) | def __contains__(self, key: str) -> bool:
    method __getattr__ (line 50) | def __getattr__(self, key: str) -> Field:
    method __setitem__ (line 54) | def __setitem__(self, key: str, value: str) -> None:
    method __getitem__ (line 57) | def __getitem__(self, key: str) -> str:
    method __iter__ (line 61) | def __iter__(self) -> Iterator[tuple[str, str]]:
    method __repr__ (line 64) | def __repr__(self) -> str:
    method __eq__ (line 67) | def __eq__(self, other: object) -> bool:
  class Field (line 73) | class Field:
    method __init__ (line 78) | def __init__(self, name: str, mapping: str) -> None:
    method _create_filter (line 82) | def _create_filter(self, op: str, other: Union[Field, Any]) -> Filter:
    method _combine_fields (line 90) | def _combine_fields(self, op: str, other: Union[Field, int, float], in...
    method _format_query (line 103) | def _format_query(left: Any, op: str, right: Any, invert: bool = False...
    method __add__ (line 109) | def __add__(self, value: Union[Field, int, float]) -> Field:
    method __radd__ (line 112) | def __radd__(self, value: Union[Field, int, float]) -> Field:
    method __sub__ (line 115) | def __sub__(self, value: Union[Field, int, float]) -> Field:
    method __rsub__ (line 118) | def __rsub__(self, value: Union[Field, int, float]) -> Field:
    method __mul__ (line 121) | def __mul__(self, value: Union[Field, int, float]) -> Field:
    method __rmul__ (line 124) | def __rmul__(self, value: Union[Field, int, float]) -> Field:
    method __truediv__ (line 127) | def __truediv__(self, value: Union[Field, int, float]) -> Field:
    method __rtruediv__ (line 130) | def __rtruediv__(self, value: Union[Field, int, float]) -> Field:
    method __lt__ (line 133) | def __lt__(self, value: Union[Field, Any]) -> Filter:
    method __le__ (line 136) | def __le__(self, value: Union[Field, Any]) -> Filter:
    method __gt__ (line 139) | def __gt__(self, value: Union[Field, Any]) -> Filter:
    method __ge__ (line 142) | def __ge__(self, value: Union[Field, Any]) -> Filter:
    method __eq__ (line 145) | def __eq__(self, value: Union[Field, Any]) -> Filter:  # type: ignore[...
    method __ne__ (line 150) | def __ne__(self, value: Union[Field, Any]) -> Filter:  # type: ignore[...
    method __repr__ (line 153) | def __repr__(self) -> str:
  class Filter (line 157) | class Filter:
    method __init__ (line 162) | def __init__(self, query: str) -> None:
    method __and__ (line 165) | def __and__(self, other: Filter) -> Filter:
    method __or__ (line 171) | def __or__(self, other: Filter) -> Filter:
    method __invert__ (line 177) | def __invert__(self) -> Filter:
    method __call__ (line 181) | def __call__(self, data: 'pd.DataFrame') -> 'pd.Series':
    method __repr__ (line 185) | def __repr__(self) -> str:

FILE: options_portfolio_backtester/engine/algo_adapters.py
  class EngineStepDecision (line 18) | class EngineStepDecision:
  class EnginePipelineContext (line 26) | class EnginePipelineContext:
  class EngineAlgo (line 40) | class EngineAlgo(Protocol):
    method __call__ (line 41) | def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
  class EngineRunMonthly (line 45) | class EngineRunMonthly:
    method __init__ (line 48) | def __init__(self) -> None:
    method reset (line 51) | def reset(self) -> None:
    method __call__ (line 54) | def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
  class BudgetPercent (line 62) | class BudgetPercent:
    method __init__ (line 65) | def __init__(self, pct: float) -> None:
    method __call__ (line 68) | def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
  class RangeFilter (line 73) | class RangeFilter:
    method __init__ (line 80) | def __init__(self, column: str, min_val: float, max_val: float) -> None:
    method __call__ (line 85) | def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
  function SelectByDelta (line 98) | def SelectByDelta(min_delta: float = -1.0, max_delta: float = 1.0, colum...
  function SelectByDTE (line 103) | def SelectByDTE(min_dte: int = 0, max_dte: int = 10_000, column: str = "...
  function IVRankFilter (line 108) | def IVRankFilter(min_rank: float = 0.0, max_rank: float = 1.0, column: s...
  class MaxGreekExposure (line 113) | class MaxGreekExposure:
    method __init__ (line 116) | def __init__(
    method __call__ (line 124) | def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:
  class ExitOnThreshold (line 138) | class ExitOnThreshold:
    method __init__ (line 145) | def __init__(self, profit_pct: float = float("inf"), loss_pct: float =...
    method __call__ (line 156) | def __call__(self, ctx: EnginePipelineContext) -> EngineStepDecision:

FILE: options_portfolio_backtester/engine/clock.py
  class TradingClock (line 10) | class TradingClock:
    method __init__ (line 16) | def __init__(
    method iter_dates (line 30) | def iter_dates(self) -> Generator[tuple[pd.Timestamp, pd.DataFrame, pd...
    method rebalance_dates (line 42) | def rebalance_dates(self, freq: int) -> pd.DatetimeIndex:
    method _monthly_iter (line 65) | def _monthly_iter(data: pd.DataFrame, date_col: str):
    method all_dates (line 74) | def all_dates(self) -> pd.DatetimeIndex:

FILE: options_portfolio_backtester/engine/engine.py
  function _intrinsic_value (line 50) | def _intrinsic_value(option_type: str, strike: float, underlying_price: ...
  class _StrategySlot (line 62) | class _StrategySlot:
  class BacktestEngine (line 74) | class BacktestEngine:
    method __init__ (line 82) | def __init__(
    method stocks (line 127) | def stocks(self) -> list[Stock]:
    method stocks (line 131) | def stocks(self, stocks: list[Stock]) -> None:
    method options_strategy (line 136) | def options_strategy(self) -> Strategy | None:
    method options_strategy (line 140) | def options_strategy(self, strat: Strategy) -> None:
    method stocks_data (line 144) | def stocks_data(self) -> TiingoData | None:
    method stocks_data (line 148) | def stocks_data(self, data: TiingoData) -> None:
    method options_data (line 153) | def options_data(self) -> HistoricalOptionsData | None:
    method options_data (line 157) | def options_data(self, data: HistoricalOptionsData) -> None:
    method add_strategy (line 163) | def add_strategy(
    method _is_multi_strategy (line 195) | def _is_multi_strategy(self) -> bool:
    method run (line 200) | def run(self, rebalance_freq: int = 0, monthly: bool = False,
    method events_dataframe (line 255) | def events_dataframe(self) -> pd.DataFrame:
    method _translate_algos_to_config (line 271) | def _translate_algos_to_config(self) -> None:
    method _run_rust (line 344) | def _run_rust(
    method _run_rust_multi (line 523) | def _run_rust_multi(
    method _attach_run_metadata (line 697) | def _attach_run_metadata(
    method _build_run_metadata (line 712) | def _build_run_metadata(
    method _data_snapshot (line 741) | def _data_snapshot(self) -> dict[str, Any]:
    method _sha256_json (line 756) | def _sha256_json(payload: dict[str, Any]) -> str:
    method _git_sha (line 761) | def _git_sha() -> str:
    method _flat_trade_log_to_multiindex (line 775) | def _flat_trade_log_to_multiindex(self, flat_df: pd.DataFrame) -> pd.D...
    method _initialize_inventories (line 791) | def _initialize_inventories(self) -> None:
    method _current_options_capital (line 808) | def _current_options_capital(self, options, stocks):
    method _get_current_option_quotes (line 836) | def _get_current_option_quotes(self, options):
    method __repr__ (line 854) | def __repr__(self) -> str:

FILE: options_portfolio_backtester/engine/multi_strategy.py
  class StrategyAllocation (line 16) | class StrategyAllocation:
    method __init__ (line 19) | def __init__(
  class MultiStrategyEngine (line 30) | class MultiStrategyEngine:
    method __init__ (line 37) | def __init__(
    method run (line 47) | def run(self, rebalance_freq: int = 0, monthly: bool = False,
    method _build_combined_balance (line 71) | def _build_combined_balance(self) -> None:

FILE: options_portfolio_backtester/engine/pipeline.py
  class StepDecision (line 21) | class StepDecision:
  class PipelineContext (line 29) | class PipelineContext:
  class PipelineLogRow (line 44) | class PipelineLogRow:
  class Algo (line 51) | class Algo(Protocol):
    method __call__ (line 54) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunMonthly (line 62) | class RunMonthly:
    method __init__ (line 65) | def __init__(self) -> None:
    method reset (line 68) | def reset(self) -> None:
    method __call__ (line 71) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunWeekly (line 79) | class RunWeekly:
    method __init__ (line 82) | def __init__(self) -> None:
    method reset (line 85) | def reset(self) -> None:
    method __call__ (line 88) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunQuarterly (line 96) | class RunQuarterly:
    method __init__ (line 99) | def __init__(self) -> None:
    method reset (line 102) | def reset(self) -> None:
    method __call__ (line 105) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunYearly (line 113) | class RunYearly:
    method __init__ (line 116) | def __init__(self) -> None:
    method reset (line 119) | def reset(self) -> None:
    method __call__ (line 122) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunDaily (line 129) | class RunDaily:
    method __call__ (line 132) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunOnce (line 136) | class RunOnce:
    method __init__ (line 139) | def __init__(self) -> None:
    method reset (line 142) | def reset(self) -> None:
    method __call__ (line 145) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunOnDate (line 152) | class RunOnDate:
    method __init__ (line 155) | def __init__(self, dates: Sequence[str | pd.Timestamp]) -> None:
    method __call__ (line 158) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunAfterDate (line 164) | class RunAfterDate:
    method __init__ (line 167) | def __init__(self, date: str | pd.Timestamp) -> None:
    method __call__ (line 170) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunEveryNPeriods (line 176) | class RunEveryNPeriods:
    method __init__ (line 179) | def __init__(self, n: int) -> None:
    method reset (line 183) | def reset(self) -> None:
    method __call__ (line 186) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunAfterDays (line 193) | class RunAfterDays:
    method __init__ (line 196) | def __init__(self, n: int) -> None:
    method reset (line 200) | def reset(self) -> None:
    method __call__ (line 203) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RunIfOutOfBounds (line 210) | class RunIfOutOfBounds:
    method __init__ (line 218) | def __init__(self, tolerance: float = 0.05) -> None:
    method reset (line 222) | def reset(self) -> None:
    method __call__ (line 225) | def __call__(self, ctx: PipelineContext) -> StepDecision:
    method update_target (line 245) | def update_target(self, weights: dict[str, float]) -> None:
  class Or (line 250) | class Or:
    method __init__ (line 253) | def __init__(self, *algos: Algo) -> None:
    method reset (line 256) | def reset(self) -> None:
    method __call__ (line 261) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class Not (line 269) | class Not:
    method __init__ (line 272) | def __init__(self, algo: Algo) -> None:
    method reset (line 275) | def reset(self) -> None:
    method __call__ (line 279) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectThese (line 290) | class SelectThese:
    method __init__ (line 293) | def __init__(self, symbols: list[str]) -> None:
    method __call__ (line 296) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectAll (line 304) | class SelectAll:
    method __call__ (line 307) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectHasData (line 315) | class SelectHasData:
    method __init__ (line 318) | def __init__(self, min_days: int = 1) -> None:
    method __call__ (line 321) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectMomentum (line 336) | class SelectMomentum:
    method __init__ (line 339) | def __init__(self, n: int, lookback: int = 252, sort_descending: bool ...
    method __call__ (line 344) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectN (line 368) | class SelectN:
    method __init__ (line 371) | def __init__(self, n: int) -> None:
    method __call__ (line 374) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectRandomly (line 381) | class SelectRandomly:
    method __init__ (line 384) | def __init__(self, n: int, seed: int | None = None) -> None:
    method __call__ (line 388) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectActive (line 399) | class SelectActive:
    method __call__ (line 402) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectRegex (line 414) | class SelectRegex:
    method __init__ (line 417) | def __init__(self, pattern: str) -> None:
    method __call__ (line 420) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class SelectWhere (line 429) | class SelectWhere:
    method __init__ (line 432) | def __init__(self, fn: Callable[[str, PipelineContext], bool]) -> None:
    method __call__ (line 435) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class WeighSpecified (line 449) | class WeighSpecified:
    method __init__ (line 452) | def __init__(self, weights: dict[str, float]) -> None:
    method __call__ (line 455) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class WeighEqually (line 466) | class WeighEqually:
    method __call__ (line 469) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class WeighRandomly (line 477) | class WeighRandomly:
    method __init__ (line 483) | def __init__(self, seed: int | None = None) -> None:
    method __call__ (line 486) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class WeighTarget (line 494) | class WeighTarget:
    method __init__ (line 501) | def __init__(self, weights_df: pd.DataFrame) -> None:
    method __call__ (line 504) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class WeighInvVol (line 525) | class WeighInvVol:
    method __init__ (line 532) | def __init__(self, lookback: int = 252) -> None:
    method __call__ (line 535) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class WeighMeanVar (line 559) | class WeighMeanVar:
    method __init__ (line 566) | def __init__(self, lookback: int = 252, risk_free_rate: float = 0.0) -...
    method __call__ (line 570) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class WeighERC (line 614) | class WeighERC:
    method __init__ (line 621) | def __init__(self, lookback: int = 252, max_iter: int = 100) -> None:
    method __call__ (line 625) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class TargetVol (line 659) | class TargetVol:
    method __init__ (line 666) | def __init__(self, target: float = 0.10, lookback: int = 252) -> None:
    method __call__ (line 670) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class LimitWeights (line 697) | class LimitWeights:
    method __init__ (line 700) | def __init__(self, limit: float = 0.25) -> None:
    method __call__ (line 703) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class LimitDeltas (line 725) | class LimitDeltas:
    method __init__ (line 733) | def __init__(self, limit: float = 0.10) -> None:
    method __call__ (line 736) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class ScaleWeights (line 771) | class ScaleWeights:
    method __init__ (line 778) | def __init__(self, scale: float) -> None:
    method __call__ (line 781) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class CapitalFlow (line 792) | class CapitalFlow:
    method __init__ (line 799) | def __init__(self, flows: dict[str | pd.Timestamp, float] | Callable[[...
    method __call__ (line 806) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class MaxDrawdownGuard (line 818) | class MaxDrawdownGuard:
    method __init__ (line 821) | def __init__(self, max_drawdown_pct: float) -> None:
    method reset (line 825) | def reset(self) -> None:
    method __call__ (line 828) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class HedgeRisks (line 838) | class HedgeRisks:
    method __init__ (line 851) | def __init__(
    method __call__ (line 861) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class Margin (line 914) | class Margin:
    method __init__ (line 922) | def __init__(
    method reset (line 933) | def reset(self) -> None:
    method __call__ (line 936) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class CouponPayingPosition (line 963) | class CouponPayingPosition:
    method __init__ (line 977) | def __init__(
    method reset (line 993) | def reset(self) -> None:
    method __call__ (line 996) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class ReplayTransactions (line 1026) | class ReplayTransactions:
    method __init__ (line 1034) | def __init__(self, blotter: pd.DataFrame) -> None:
    method __call__ (line 1042) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class CloseDead (line 1074) | class CloseDead:
    method __call__ (line 1080) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class ClosePositionsAfterDates (line 1092) | class ClosePositionsAfterDates:
    method __init__ (line 1098) | def __init__(self, schedule: dict[str, str | pd.Timestamp]) -> None:
    method __call__ (line 1101) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class Require (line 1112) | class Require:
    method __init__ (line 1120) | def __init__(self, algo: Algo) -> None:
    method reset (line 1123) | def reset(self) -> None:
    method __call__ (line 1127) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class Rebalance (line 1138) | class Rebalance:
    method __call__ (line 1144) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class RebalanceOverTime (line 1164) | class RebalanceOverTime:
    method __init__ (line 1171) | def __init__(self, n: int = 5) -> None:
    method reset (line 1176) | def reset(self) -> None:
    method __call__ (line 1180) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  class AlgoPipelineBacktester (line 1233) | class AlgoPipelineBacktester:
    method __init__ (line 1236) | def __init__(
    method run (line 1247) | def run(self) -> pd.DataFrame:
    method set_date_range (line 1320) | def set_date_range(self, start=None, end=None):
    method logs_dataframe (line 1325) | def logs_dataframe(self) -> pd.DataFrame:
  class RandomBenchmarkResult (line 1341) | class RandomBenchmarkResult:
    method mean_random (line 1349) | def mean_random(self) -> float:
    method std_random (line 1353) | def std_random(self) -> float:
  function benchmark_random (line 1357) | def benchmark_random(

FILE: options_portfolio_backtester/engine/strategy_tree.py
  class StrategyTreeNode (line 13) | class StrategyTreeNode:
    method __post_init__ (line 22) | def __post_init__(self) -> None:
    method is_leaf (line 29) | def is_leaf(self) -> bool:
    method to_dot (line 32) | def to_dot(self) -> str:
    method _dot_recursive (line 43) | def _dot_recursive(self, lines: list[str], parent_id: str | None) -> N...
  class StrategyTreeEngine (line 56) | class StrategyTreeEngine:
    method __init__ (line 59) | def __init__(self, root: StrategyTreeNode, initial_capital: int = 1_00...
    method to_dot (line 64) | def to_dot(self) -> str:
    method _leaf_shares (line 68) | def _leaf_shares(self, node: StrategyTreeNode, parent_share: float) ->...
    method run (line 84) | def run(self, rebalance_freq: int = 0, monthly: bool = False, sma_days...

FILE: options_portfolio_backtester/execution/cost_model.py
  class TransactionCostModel (line 12) | class TransactionCostModel(ABC):
    method option_cost (line 16) | def option_cost(self, price: float, quantity: int, shares_per_contract...
    method stock_cost (line 21) | def stock_cost(self, price: float, quantity: float) -> float:
  class NoCosts (line 26) | class NoCosts(TransactionCostModel):
    method option_cost (line 29) | def option_cost(self, price: float, quantity: int, shares_per_contract...
    method stock_cost (line 32) | def stock_cost(self, price: float, quantity: float) -> float:
    method to_rust_config (line 35) | def to_rust_config(self) -> dict:
  class PerContractCommission (line 39) | class PerContractCommission(TransactionCostModel):
    method __init__ (line 42) | def __init__(self, rate: float = 0.65, stock_rate: float = 0.005) -> N...
    method option_cost (line 46) | def option_cost(self, price: float, quantity: int, shares_per_contract...
    method stock_cost (line 49) | def stock_cost(self, price: float, quantity: float) -> float:
    method to_rust_config (line 52) | def to_rust_config(self) -> dict:
  class TieredCommission (line 56) | class TieredCommission(TransactionCostModel):
    method __init__ (line 63) | def __init__(self, tiers: list[tuple[int, float]] | None = None,
    method option_cost (line 73) | def option_cost(self, price: float, quantity: int, shares_per_contract...
    method stock_cost (line 76) | def stock_cost(self, price: float, quantity: float) -> float:
    method to_rust_config (line 79) | def to_rust_config(self) -> dict:
  class SpreadSlippage (line 87) | class SpreadSlippage(TransactionCostModel):
    method __init__ (line 94) | def __init__(self, pct: float = 0.5) -> None:
    method option_cost (line 98) | def option_cost(self, price: float, quantity: int, shares_per_contract...
    method stock_cost (line 103) | def stock_cost(self, price: float, quantity: float) -> float:
    method slippage (line 106) | def slippage(self, bid: float, ask: float, quantity: int, shares_per_c...

FILE: options_portfolio_backtester/execution/fill_model.py
  class FillModel (line 13) | class FillModel(ABC):
    method get_fill_price (line 17) | def get_fill_price(self, row: pd.Series, direction: Direction) -> float:
  class MarketAtBidAsk (line 22) | class MarketAtBidAsk(FillModel):
    method get_fill_price (line 25) | def get_fill_price(self, row: pd.Series, direction: Direction) -> float:
    method to_rust_config (line 28) | def to_rust_config(self) -> dict:
  class MidPrice (line 32) | class MidPrice(FillModel):
    method get_fill_price (line 35) | def get_fill_price(self, row: pd.Series, direction: Direction) -> float:
    method to_rust_config (line 40) | def to_rust_config(self) -> dict:
  class VolumeAwareFill (line 44) | class VolumeAwareFill(FillModel):
    method __init__ (line 51) | def __init__(self, full_volume_threshold: int = 100) -> None:
    method get_fill_price (line 54) | def get_fill_price(self, row: pd.Series, direction: Direction) -> float:
    method to_rust_config (line 62) | def to_rust_config(self) -> dict:

FILE: options_portfolio_backtester/execution/signal_selector.py
  class SignalSelector (line 14) | class SignalSelector(ABC):
    method column_requirements (line 18) | def column_requirements(self) -> list[str]:
    method select (line 23) | def select(self, candidates: pd.DataFrame) -> pd.Series:
  class FirstMatch (line 35) | class FirstMatch(SignalSelector):
    method select (line 38) | def select(self, candidates: pd.DataFrame) -> pd.Series:
    method to_rust_config (line 41) | def to_rust_config(self) -> dict:
  class NearestDelta (line 45) | class NearestDelta(SignalSelector):
    method __init__ (line 51) | def __init__(self, target_delta: float = -0.30, delta_column: str = "d...
    method column_requirements (line 56) | def column_requirements(self) -> list[str]:
    method select (line 59) | def select(self, candidates: pd.DataFrame) -> pd.Series:
    method to_rust_config (line 66) | def to_rust_config(self) -> dict:
  class MaxOpenInterest (line 70) | class MaxOpenInterest(SignalSelector):
    method __init__ (line 76) | def __init__(self, oi_column: str = "openinterest") -> None:
    method column_requirements (line 80) | def column_requirements(self) -> list[str]:
    method select (line 83) | def select(self, candidates: pd.DataFrame) -> pd.Series:
    method to_rust_config (line 90) | def to_rust_config(self) -> dict:

FILE: options_portfolio_backtester/execution/sizer.py
  class PositionSizer (line 8) | class PositionSizer(ABC):
    method size (line 12) | def size(self, cost_per_contract: float, available_capital: float,
  class CapitalBased (line 24) | class CapitalBased(PositionSizer):
    method size (line 30) | def size(self, cost_per_contract: float, available_capital: float,
  class FixedQuantity (line 37) | class FixedQuantity(PositionSizer):
    method __init__ (line 40) | def __init__(self, quantity: int = 1) -> None:
    method size (line 43) | def size(self, cost_per_contract: float, available_capital: float,
  class FixedDollar (line 50) | class FixedDollar(PositionSizer):
    method __init__ (line 53) | def __init__(self, amount: float = 10_000.0) -> None:
    method size (line 56) | def size(self, cost_per_contract: float, available_capital: float,
  class PercentOfPortfolio (line 64) | class PercentOfPortfolio(PositionSizer):
    method __init__ (line 67) | def __init__(self, pct: float = 0.01) -> None:
    method size (line 71) | def size(self, cost_per_contract: float, available_capital: float,

FILE: options_portfolio_backtester/portfolio/greeks.py
  function aggregate_greeks (line 9) | def aggregate_greeks(

FILE: options_portfolio_backtester/portfolio/portfolio.py
  class StockHolding (line 18) | class StockHolding:
  class Portfolio (line 25) | class Portfolio:
    method __init__ (line 32) | def __init__(self, initial_cash: float = 0.0) -> None:
    method next_position_id (line 38) | def next_position_id(self) -> int:
    method add_option_position (line 45) | def add_option_position(self, pos: OptionPosition) -> None:
    method remove_option_position (line 48) | def remove_option_position(self, position_id: int) -> OptionPosition |...
    method options_value (line 51) | def options_value(self, current_prices: dict[int, dict[str, float]],
    method set_stock_holding (line 67) | def set_stock_holding(self, symbol: str, quantity: float,
    method clear_stock_holdings (line 73) | def clear_stock_holdings(self) -> None:
    method stocks_value (line 76) | def stocks_value(self, current_prices: dict[str, float]) -> float:
    method total_value (line 86) | def total_value(self, stock_prices: dict[str, float],
    method portfolio_greeks (line 94) | def portfolio_greeks(self,

FILE: options_portfolio_backtester/portfolio/position.py
  class PositionLeg (line 14) | class PositionLeg:
    method exit_order (line 27) | def exit_order(self) -> Order:
    method current_value (line 30) | def current_value(self, current_price: float, quantity: int,
  class OptionPosition (line 42) | class OptionPosition:
    method add_leg (line 53) | def add_leg(self, leg: PositionLeg) -> None:
    method current_value (line 56) | def current_value(self, current_prices: dict[str, float],
    method greeks (line 70) | def greeks(self, leg_greeks: dict[str, Greeks]) -> Greeks:

FILE: options_portfolio_backtester/portfolio/risk.py
  class RiskConstraint (line 12) | class RiskConstraint(ABC):
    method check (line 16) | def check(self, current_greeks: Greeks, proposed_greeks: Greeks,
    method describe (line 22) | def describe(self) -> str:
  function _greeks_list (line 27) | def _greeks_list(g: Greeks) -> list[float]:
  class MaxDelta (line 31) | class MaxDelta(RiskConstraint):
    method __init__ (line 34) | def __init__(self, limit: float = 100.0) -> None:
    method check (line 37) | def check(self, current_greeks: Greeks, proposed_greeks: Greeks,
    method describe (line 45) | def describe(self) -> str:
    method to_rust_config (line 48) | def to_rust_config(self) -> dict:
  class MaxVega (line 52) | class MaxVega(RiskConstraint):
    method __init__ (line 55) | def __init__(self, limit: float = 50.0) -> None:
    method check (line 58) | def check(self, current_greeks: Greeks, proposed_greeks: Greeks,
    method describe (line 66) | def describe(self) -> str:
    method to_rust_config (line 69) | def to_rust_config(self) -> dict:
  class MaxDrawdown (line 73) | class MaxDrawdown(RiskConstraint):
    method __init__ (line 76) | def __init__(self, max_dd_pct: float = 0.20) -> None:
    method check (line 79) | def check(self, current_greeks: Greeks, proposed_greeks: Greeks,
    method describe (line 87) | def describe(self) -> str:
    method to_rust_config (line 90) | def to_rust_config(self) -> dict:
  class RiskManager (line 94) | class RiskManager:
    method __init__ (line 97) | def __init__(self, constraints: list[RiskConstraint] | None = None) ->...
    method add_constraint (line 100) | def add_constraint(self, constraint: RiskConstraint) -> None:
    method is_allowed (line 103) | def is_allowed(self, current_greeks: Greeks, proposed_greeks: Greeks,

FILE: options_portfolio_backtester/strategy/presets.py
  function strangle (line 15) | def strangle(
  function iron_condor (line 56) | def iron_condor(
  function covered_call (line 114) | def covered_call(
  function cash_secured_put (line 143) | def cash_secured_put(
  function collar (line 172) | def collar(
  function butterfly (line 214) | def butterfly(
  class Strangle (line 263) | class Strangle(Strategy):
    method __init__ (line 266) | def __init__(

FILE: options_portfolio_backtester/strategy/strategy.py
  class Strategy (line 20) | class Strategy:
    method __init__ (line 27) | def __init__(
    method add_leg (line 42) | def add_leg(self, leg: "StrategyLeg") -> "Strategy":
    method add_legs (line 48) | def add_legs(self, legs: list["StrategyLeg"]) -> "Strategy":
    method remove_leg (line 53) | def remove_leg(self, leg_number: int) -> "Strategy":
    method clear_legs (line 57) | def clear_legs(self) -> "Strategy":
    method add_exit_thresholds (line 61) | def add_exit_thresholds(self, profit_pct: float = math.inf,
    method filter_thresholds (line 67) | def filter_thresholds(self, entry_cost: pd.Series,
    method __repr__ (line 73) | def __repr__(self) -> str:

FILE: options_portfolio_backtester/strategy/strategy_leg.py
  class StrategyLeg (line 19) | class StrategyLeg:
    method __init__ (line 26) | def __init__(
    method entry_filter (line 47) | def entry_filter(self) -> "Filter":
    method entry_filter (line 51) | def entry_filter(self, flt: "Filter") -> None:
    method exit_filter (line 55) | def exit_filter(self) -> "Filter":
    method exit_filter (line 59) | def exit_filter(self, flt: "Filter") -> None:
    method _base_entry_filter (line 62) | def _base_entry_filter(self) -> "Filter":
    method _base_exit_filter (line 67) | def _base_exit_filter(self) -> "Filter":
    method __repr__ (line 70) | def __repr__(self) -> str:

FILE: rust/ob_core/benches/hot_paths.rs
  function make_options_df (line 11) | fn make_options_df(n: usize) -> DataFrame {
  function bench_inventory_join (line 36) | fn bench_inventory_join(c: &mut Criterion) {
  function bench_filter_compile_and_apply (line 78) | fn bench_filter_compile_and_apply(c: &mut Criterion) {
  function bench_filter_compile (line 94) | fn bench_filter_compile(c: &mut Criterion) {
  function bench_entry_computation (line 106) | fn bench_entry_computation(c: &mut Criterion) {
  function bench_exit_mask (line 130) | fn bench_exit_mask(c: &mut Criterion) {
  function bench_stats_computation (line 152) | fn bench_stats_computation(c: &mut Criterion) {
  function bench_entry_qty (line 167) | fn bench_entry_qty(c: &mut Criterion) {

FILE: rust/ob_core/src/backtest.rs
  type BacktestConfig (line 30) | pub struct BacktestConfig {
  type BacktestResult (line 71) | pub struct BacktestResult {
  type StrategySlotConfig (line 80) | pub struct StrategySlotConfig {
  type Position (line 90) | struct Position {
  type StockHolding (line 103) | struct StockHolding {
  type TradeRow (line 110) | struct TradeRow {
  type LegTradeData (line 117) | struct LegTradeData {
  type BalanceDay (line 128) | struct BalanceDay {
  function ns_to_datestring (line 144) | fn ns_to_datestring(ns: i64) -> String {
  function parse_datestring_to_ns (line 153) | fn parse_datestring_to_ns(s: &str) -> Option<i64> {
  function extract_date_ns (line 164) | fn extract_date_ns(col: &Column, idx: usize) -> i64 {
  function column_value_to_string (line 188) | fn column_value_to_string(col: &Column, idx: usize) -> String {
  type DayOptions (line 215) | struct DayOptions {
    method new (line 222) | fn new(df: DataFrame, contract_col: &str) -> Self {
    method get_f64 (line 238) | fn get_f64(&self, contract: &str, field: &str) -> Option<f64> {
    method get_str (line 252) | fn get_str(&self, contract: &str, field: &str) -> Option<String> {
    method height (line 259) | fn height(&self) -> usize {
  type DayStocks (line 265) | struct DayStocks {
    method get_price (line 270) | fn get_price(&self, symbol: &str) -> Option<f64> {
  type PartitionedData (line 276) | pub struct PartitionedData {
  type SchemaMapping (line 285) | pub struct SchemaMapping {
  function run_backtest (line 301) | pub fn run_backtest(
  type PrecompiledFilters (line 313) | pub struct PrecompiledFilters {
    method from_config (line 320) | pub fn from_config(config: &BacktestConfig) -> Self {
  function run_backtest_prepartitioned (line 332) | pub fn run_backtest_prepartitioned(
  function run_backtest_with_filters (line 342) | pub fn run_backtest_with_filters(
  function run_multi_strategy (line 610) | pub fn run_multi_strategy(
  function compute_balance_period_multi (line 878) | fn compute_balance_period_multi(
  function prepartition_data (line 951) | pub fn prepartition_data(
  function execute_exits (line 1059) | fn execute_exits(
  function liquidate_all_positions (line 1172) | fn liquidate_all_positions(
  function execute_entries (line 1237) | fn execute_entries(
  function compute_balance_period (line 1486) | fn compute_balance_period(
  function compute_sma_map (line 1566) | fn compute_sma_map(
  function build_result (line 1606) | fn build_result(
  function intrinsic_value (line 1726) | fn intrinsic_value(opt_type: &str, strike: f64, underlying_price: f64) -...
  function compute_position_exit_cost (line 1735) | fn compute_position_exit_cost(pos: &Position, day_opts: &DayOptions, spc...
  function compute_stock_capital (line 1752) | fn compute_stock_capital(holdings: &[StockHolding], day_stocks: Option<&...
  function compute_options_capital (line 1762) | fn compute_options_capital(
  function compute_portfolio_greeks_from_market (line 1773) | fn compute_portfolio_greeks_from_market(
  function buy_stocks (line 1798) | fn buy_stocks(
  function compute_daily_returns (line 1834) | fn compute_daily_returns(totals: &[f64]) -> Vec<f64> {
  function daily_returns_basic (line 1844) | fn daily_returns_basic() {
  function daily_returns_empty (line 1853) | fn daily_returns_empty() {
  function ns_to_datestring_epoch (line 1859) | fn ns_to_datestring_epoch() {
  function ns_roundtrip (line 1864) | fn ns_roundtrip() {
  function intrinsic_value_put_itm (line 1871) | fn intrinsic_value_put_itm() {
  function intrinsic_value_put_otm (line 1877) | fn intrinsic_value_put_otm() {
  function intrinsic_value_call_itm (line 1883) | fn intrinsic_value_call_itm() {
  function intrinsic_value_call_otm (line 1889) | fn intrinsic_value_call_otm() {
  function intrinsic_value_atm (line 1895) | fn intrinsic_value_atm() {
  function exit_cost_uses_intrinsic_when_missing (line 1902) | fn exit_cost_uses_intrinsic_when_missing() {

FILE: rust/ob_core/src/balance.rs
  type LegInventory (line 13) | pub struct LegInventory {
  type StockInventory (line 23) | pub struct StockInventory {
  function compute_balance (line 35) | pub fn compute_balance(
  function compute_stock_values (line 150) | fn compute_stock_values(
  function compute_balance_empty_legs (line 191) | fn compute_balance_empty_legs() {

FILE: rust/ob_core/src/convexity_backtest.rs
  type Position (line 12) | struct Position {
  type MonthRecord (line 19) | pub struct MonthRecord {
  type BacktestResult (line 33) | pub struct BacktestResult {
  function ns_to_year_month (line 39) | fn ns_to_year_month(ns: i64) -> (i32, u32) {
  function monthly_rebalance_dates (line 46) | fn monthly_rebalance_dates(stock_dates_ns: &[i64]) -> Vec<i64> {
  function lower_bound (line 67) | fn lower_bound(arr: &[i64], target: i64) -> usize {
  function find_date_range (line 72) | fn find_date_range(dates_ns: &[i64], target: i64) -> (usize, usize) {
  function stock_price_on (line 85) | fn stock_price_on(stock_dates_ns: &[i64], stock_prices: &[f64], target_n...
  function close_position (line 97) | fn close_position(
  function run_backtest (line 128) | pub fn run_backtest(
  function make_ts (line 319) | fn make_ts(year: i32, month: u32, day: u32) -> i64 {
  function test_monthly_rebalance_dates (line 329) | fn test_monthly_rebalance_dates() {
  function test_stock_price_on_exact (line 346) | fn test_stock_price_on_exact() {
  function test_stock_price_on_before (line 353) | fn test_stock_price_on_before() {
  function test_run_backtest_no_options (line 360) | fn test_run_backtest_no_options() {

FILE: rust/ob_core/src/convexity_scoring.rs
  type DailyScore (line 3) | pub struct DailyScore {
  function find_target_put (line 19) | pub fn find_target_put(
  function convexity_ratio (line 53) | pub fn convexity_ratio(strike: f64, underlying: f64, ask: f64, tail_drop...
  function compute_daily_scores (line 67) | pub fn compute_daily_scores(
  function test_convexity_ratio_basic (line 136) | fn test_convexity_ratio_basic() {
  function test_convexity_ratio_otm_after_crash (line 144) | fn test_convexity_ratio_otm_after_crash() {
  function test_find_target_put (line 151) | fn test_find_target_put() {
  function test_find_target_put_dte_filter (line 161) | fn test_find_target_put_dte_filter() {
  function test_find_target_put_skips_zero_ask (line 171) | fn test_find_target_put_skips_zero_ask() {
  function test_compute_daily_scores (line 181) | fn test_compute_daily_scores() {

FILE: rust/ob_core/src/cost_model.rs
  type CostModel (line 6) | pub enum CostModel {
    method option_cost (line 20) | pub fn option_cost(&self, _price: f64, quantity: f64, _spc: i64) -> f64 {
    method stock_cost (line 54) | pub fn stock_cost(&self, _price: f64, quantity: f64) -> f64 {
  function no_costs (line 69) | fn no_costs() {
  function per_contract (line 76) | fn per_contract() {
  function tiered_within_first_tier (line 84) | fn tiered_within_first_tier() {
  function tiered_spanning_tiers (line 94) | fn tiered_spanning_tiers() {
  function tiered_beyond_all (line 105) | fn tiered_beyond_all() {
  function tiered_stock_cost (line 116) | fn tiered_stock_cost() {

FILE: rust/ob_core/src/entries.rs
  function compute_leg_entries (line 21) | pub fn compute_leg_entries(
  function compute_entry_qty (line 80) | pub fn compute_entry_qty(total_costs: &Series, allocation: f64) -> Polar...
  function sample_options (line 93) | fn sample_options() -> DataFrame {
  function compute_entries_excludes_held (line 108) | fn compute_entries_excludes_held() {
  function compute_qty (line 131) | fn compute_qty() {

FILE: rust/ob_core/src/exits.rs
  function threshold_exit_mask (line 15) | pub fn threshold_exit_mask(
  function combine_masks_or (line 58) | pub fn combine_masks_or(masks: &[BooleanChunked]) -> BooleanChunked {
  function threshold_profit_exit (line 74) | fn threshold_profit_exit() {
  function threshold_loss_exit (line 84) | fn threshold_loss_exit() {
  function combine_masks (line 94) | fn combine_masks() {

FILE: rust/ob_core/src/fill_model.rs
  type FillModel (line 6) | pub enum FillModel {
    method fill_price (line 21) | pub fn fill_price(&self, bid: f64, ask: f64, volume: Option<f64>, is_b...
  function market_at_bid_ask_buy (line 50) | fn market_at_bid_ask_buy() {
  function market_at_bid_ask_sell (line 56) | fn market_at_bid_ask_sell() {
  function mid_price (line 62) | fn mid_price() {
  function volume_aware_full_volume (line 69) | fn volume_aware_full_volume() {
  function volume_aware_zero_volume (line 77) | fn volume_aware_zero_volume() {
  function volume_aware_half_volume (line 86) | fn volume_aware_half_volume() {
  function volume_aware_no_volume_data (line 97) | fn volume_aware_no_volume_data() {

FILE: rust/ob_core/src/filter.rs
  type FilterError (line 16) | pub enum FilterError {
  type Value (line 25) | pub enum Value {
  type CmpOp (line 34) | pub enum CmpOp {
  type ArithOp (line 45) | pub enum ArithOp {
  type FilterExpr (line 54) | pub enum FilterExpr {
  type Token (line 66) | enum Token {
  function tokenize (line 88) | fn tokenize(input: &str) -> Result<Vec<Token>, FilterError> {
  type Parser (line 198) | struct Parser {
    method new (line 204) | fn new(tokens: Vec<Token>) -> Self {
    method peek (line 208) | fn peek(&self) -> Option<&Token> {
    method advance (line 212) | fn advance(&mut self) -> Option<Token> {
    method expect (line 218) | fn expect(&mut self, expected: &Token) -> Result<(), FilterError> {
    method parse_expr (line 227) | fn parse_expr(&mut self) -> Result<FilterExpr, FilterError> {
    method parse_or (line 232) | fn parse_or(&mut self) -> Result<FilterExpr, FilterError> {
    method parse_and (line 243) | fn parse_and(&mut self) -> Result<FilterExpr, FilterError> {
    method parse_unary (line 254) | fn parse_unary(&mut self) -> Result<FilterExpr, FilterError> {
    method parse_primary (line 264) | fn parse_primary(&mut self) -> Result<FilterExpr, FilterError> {
    method parse_comparison (line 276) | fn parse_comparison(&mut self) -> Result<FilterExpr, FilterError> {
    method parse_value_expr (line 307) | fn parse_value_expr(&mut self) -> Result<ValueExpr, FilterError> {
    method try_parse_arith (line 336) | fn try_parse_arith(&mut self) -> Option<(ArithOp, Value)> {
    method parse_cmp_op (line 354) | fn parse_cmp_op(&mut self) -> Result<CmpOp, FilterError> {
    method value_expr_to_value (line 366) | fn value_expr_to_value(&self, ve: ValueExpr) -> Result<Value, FilterEr...
  type ValueExpr (line 378) | enum ValueExpr {
  function flip_cmp (line 384) | fn flip_cmp(op: CmpOp) -> CmpOp {
  function parse (line 395) | pub fn parse(query: &str) -> Result<FilterExpr, FilterError> {
  function to_polars_expr (line 409) | pub fn to_polars_expr(filter: &FilterExpr) -> Expr {
  function value_to_lit (line 440) | fn value_to_lit(value: &Value) -> Expr {
  function apply_cmp (line 450) | fn apply_cmp(left: Expr, op: CmpOp, right: Expr) -> Expr {
  type CompiledFilter (line 462) | pub struct CompiledFilter {
    method new (line 468) | pub fn new(query: &str) -> Result<Self, FilterError> {
    method apply (line 474) | pub fn apply(&self, df: &DataFrame) -> PolarsResult<DataFrame> {
    method eval_row (line 486) | pub fn eval_row(&self, df: &DataFrame, row_idx: usize) -> bool {
  function read_f64 (line 493) | fn read_f64(col: &Column, row: usize) -> Option<f64> {
  function read_str (line 508) | fn read_str<'a>(col: &'a Column, row: usize) -> Option<&'a str> {
  function cmp_f64 (line 514) | fn cmp_f64(lhs: f64, op: CmpOp, rhs: f64) -> bool {
  function resolve_f64 (line 527) | fn resolve_f64(val: &Value, df: &DataFrame, row: usize) -> Option<f64> {
  function eval_expr_row (line 537) | fn eval_expr_row(expr: &FilterExpr, df: &DataFrame, row: usize) -> bool {
  function parse_simple_eq (line 599) | fn parse_simple_eq() {
  function parse_simple_gte (line 608) | fn parse_simple_gte() {
  function parse_and (line 617) | fn parse_and() {
  function parse_col_arith (line 635) | fn parse_col_arith() {
  function parse_chained_and (line 650) | fn parse_chained_and() {
  function compiled_filter_apply (line 660) | fn compiled_filter_apply() {
  function compiled_filter_dte_range (line 673) | fn compiled_filter_dte_range() {
  function parse_scientific_notation (line 686) | fn parse_scientific_notation() {
  function parse_scientific_notation_positive_exp (line 698) | fn parse_scientific_notation_positive_exp() {
  function parse_scientific_notation_no_sign (line 710) | fn parse_scientific_notation_no_sign() {
  function compiled_filter_scientific_notation (line 721) | fn compiled_filter_scientific_notation() {
  function parse_negative_float_literal (line 732) | fn parse_negative_float_literal() {
  function parse_negative_int_literal (line 741) | fn parse_negative_int_literal() {
  function parse_negative_delta_range (line 750) | fn parse_negative_delta_range() {
  function compiled_filter_negative_delta (line 769) | fn compiled_filter_negative_delta() {
  function eval_row_simple_eq (line 782) | fn eval_row_simple_eq() {
  function eval_row_and_filter (line 795) | fn eval_row_and_filter() {
  function eval_row_col_arith (line 808) | fn eval_row_col_arith() {
  function eval_row_negative_delta (line 822) | fn eval_row_negative_delta() {
  function eval_row_matches_apply (line 836) | fn eval_row_matches_apply() {

FILE: rust/ob_core/src/inventory.rs
  function join_inventory_to_market (line 16) | pub fn join_inventory_to_market(
  function aggregate_by_type (line 124) | pub fn aggregate_by_type(
  function sample_options (line 153) | fn sample_options() -> DataFrame {
  function join_computes_values (line 164) | fn join_computes_values() {
  function aggregate_splits_calls_puts (line 192) | fn aggregate_splits_calls_puts() {

FILE: rust/ob_core/src/risk.rs
  type RiskConstraint (line 8) | pub enum RiskConstraint {
    method check (line 21) | pub fn check(
  function check_all (line 49) | pub fn check_all(
  function max_delta_allows (line 64) | fn max_delta_allows() {
  function max_delta_rejects (line 72) | fn max_delta_rejects() {
  function max_delta_negative (line 80) | fn max_delta_negative() {
  function max_vega_allows (line 88) | fn max_vega_allows() {
  function max_vega_rejects (line 96) | fn max_vega_rejects() {
  function max_drawdown_allows (line 104) | fn max_drawdown_allows() {
  function max_drawdown_rejects (line 112) | fn max_drawdown_rejects() {
  function max_drawdown_zero_peak (line 120) | fn max_drawdown_zero_peak() {
  function check_all_passes (line 127) | fn check_all_passes() {
  function check_all_fails_one (line 138) | fn check_all_fails_one() {
  function check_all_empty_passes (line 150) | fn check_all_empty_passes() {

FILE: rust/ob_core/src/signal_selector.rs
  type SignalSelector (line 8) | pub enum SignalSelector {
    method column_requirements (line 20) | pub fn column_requirements(&self) -> Vec<&str> {
    method select_index (line 30) | pub fn select_index(&self, candidates: &DataFrame) -> usize {
  function sample_candidates (line 109) | fn sample_candidates() -> DataFrame {
  function first_match (line 119) | fn first_match() {
  function nearest_delta (line 126) | fn nearest_delta() {
  function nearest_delta_between (line 136) | fn nearest_delta_between() {
  function max_open_interest (line 147) | fn max_open_interest() {
  function missing_column_falls_back (line 156) | fn missing_column_falls_back() {
  function column_requirements_check (line 166) | fn column_requirements_check() {

FILE: rust/ob_core/src/stats.rs
  constant TRADING_DAYS_PER_YEAR (line 7) | const TRADING_DAYS_PER_YEAR: f64 = 252.0;
  constant MONTHS_PER_YEAR (line 8) | const MONTHS_PER_YEAR: f64 = 12.0;
  type PeriodStats (line 16) | pub struct PeriodStats {
  type LookbackReturns (line 29) | pub struct LookbackReturns {
  type FullStats (line 42) | pub struct FullStats {
  type Stats (line 89) | pub struct Stats {
  function compute_stats (line 107) | pub fn compute_stats(
  function compute_full_stats (line 155) | pub fn compute_full_stats(
  function cum_return (line 253) | fn cum_return(returns: &[f64]) -> f64 {
  function annualize (line 257) | fn annualize(total_return: f64, years: f64) -> f64 {
  function mean (line 265) | fn mean(values: &[f64]) -> f64 {
  function std_dev (line 272) | fn std_dev(values: &[f64]) -> f64 {
  function skewness (line 282) | fn skewness(values: &[f64]) -> f64 {
  function kurtosis_excess (line 299) | fn kurtosis_excess(values: &[f64]) -> f64 {
  function sharpe (line 317) | fn sharpe(returns: &[f64], risk_free_rate: f64, periods_per_year: f64) -...
  function sortino (line 330) | fn sortino(returns: &[f64], risk_free_rate: f64, periods_per_year: f64) ...
  function percentile (line 347) | fn percentile(values: &[f64], pct: f64) -> f64 {
  type DrawdownResult (line 366) | struct DrawdownResult {
  function compute_drawdown_full (line 373) | fn compute_drawdown_full(daily_returns: &[f64]) -> DrawdownResult {
  function compute_period_stats (line 437) | fn compute_period_stats(returns: &[f64], risk_free_rate: f64, periods_pe...
  type ResampleFreq (line 456) | enum ResampleFreq {
  function resample_returns (line 462) | fn resample_returns(
  function period_key (line 505) | fn period_key(ts_ns: i64, freq: ResampleFreq) -> (i32, u32) {
  function days_to_ymd (line 517) | fn days_to_ymd(days: i32) -> (i32, u32, u32) {
  function compute_lookback (line 534) | fn compute_lookback(total_capital: &[f64], timestamps_ns: &[i64]) -> Loo...
  function ymd_to_ns (line 583) | fn ymd_to_ns(year: i32, month: u32, day: u32) -> i64 {
  function subtract_months_ns (line 595) | fn subtract_months_ns(ts_ns: i64, months: u32) -> i64 {
  function compute_turnover (line 609) | fn compute_turnover(stock_weights: &[f64], n_stocks: usize) -> f64 {
  function compute_herfindahl (line 632) | fn compute_herfindahl(stock_weights: &[f64], n_stocks: usize) -> f64 {
  type TradeStatsResult (line 656) | struct TradeStatsResult {
  function compute_trade_stats (line 669) | fn compute_trade_stats(pnls: &[f64]) -> TradeStatsResult {
  function stats_empty (line 750) | fn stats_empty() {
  function stats_simple_returns (line 756) | fn stats_simple_returns() {
  function drawdown_calculation (line 764) | fn drawdown_calculation() {
  function profit_factor_calculation (line 771) | fn profit_factor_calculation() {
  function full_stats_empty (line 780) | fn full_stats_empty() {
  function full_stats_basic (line 787) | fn full_stats_basic() {
  function full_stats_drawdown_avg (line 807) | fn full_stats_drawdown_avg() {
  function full_stats_trade_pnls (line 826) | fn full_stats_trade_pnls() {
  function full_stats_turnover (line 840) | fn full_stats_turnover() {
  function full_stats_herfindahl (line 853) | fn full_stats_herfindahl() {
  function percentile_basic (line 861) | fn percentile_basic() {
  function days_to_ymd_epoch (line 872) | fn days_to_ymd_epoch() {
  function days_to_ymd_known_date (line 878) | fn days_to_ymd_known_date() {
  function ymd_roundtrip (line 885) | fn ymd_roundtrip() {
  function lookback_basic (line 893) | fn lookback_basic() {
  function monthly_resample (line 912) | fn monthly_resample() {

FILE: rust/ob_core/src/types.rs
  type Direction (line 4) | pub enum Direction {
    method sign (line 11) | pub fn sign(self) -> f64 {
    method price_column (line 19) | pub fn price_column(self) -> &'static str {
    method invert (line 27) | pub fn invert(self) -> Direction {
  type OptionType (line 36) | pub enum OptionType {
    method as_str (line 42) | pub fn as_str(self) -> &'static str {
  type LegConfig (line 52) | pub struct LegConfig {
  type Greeks (line 68) | pub struct Greeks {
    method new (line 76) | pub fn new(delta: f64, gamma: f64, theta: f64, vega: f64) -> Self {
    method scale (line 80) | pub fn scale(self, s: f64) -> Self {
    type Output (line 91) | type Output = Self;
    method add (line 92) | fn add(self, rhs: Self) -> Self {
    method add_assign (line 103) | fn add_assign(&mut self, rhs: Self) {
  type BalanceRow (line 113) | pub struct BalanceRow {
  function direction_sign (line 128) | fn direction_sign() {
  function direction_invert (line 134) | fn direction_invert() {
  function greeks_add (line 140) | fn greeks_add() {
  function greeks_scale (line 149) | fn greeks_scale() {

FILE: rust/ob_python/src/arrow_bridge.rs
  function py_to_polars (line 10) | pub fn py_to_polars(py_df: PyDataFrame) -> DataFrame {
  function polars_to_py (line 15) | pub fn polars_to_py(df: DataFrame) -> PyDataFrame {

FILE: rust/ob_python/src/lib.rs
  function _ob_rust (line 15) | fn _ob_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {

FILE: rust/ob_python/src/py_backtest.rs
  function parse_schema (line 17) | pub fn parse_schema(schema: &Bound<'_, PyDict>) -> PyResult<SchemaMappin...
  function parse_cost_model (line 37) | pub fn parse_cost_model(d: &Bound<'_, PyDict>) -> PyResult<CostModel> {
  function parse_fill_model (line 67) | pub fn parse_fill_model(d: &Bound<'_, PyDict>) -> PyResult<FillModel> {
  function parse_signal_selector (line 88) | pub fn parse_signal_selector(d: &Bound<'_, PyDict>) -> PyResult<SignalSe...
  function parse_risk_constraint (line 113) | pub fn parse_risk_constraint(d: &Bound<'_, PyDict>) -> PyResult<RiskCons...
  function parse_config_from_dict (line 135) | pub fn parse_config_from_dict(config: &Bound<'_, PyDict>) -> PyResult<Ba...
  function run_backtest_py (line 295) | pub fn run_backtest_py(
  function parse_leg_config (line 337) | pub fn parse_leg_config(d: &Bound<'_, PyDict>) -> PyResult<LegConfig> {
  function parse_slot_config (line 379) | fn parse_slot_config(d: &Bound<'_, PyDict>) -> PyResult<StrategySlotConf...
  function run_multi_strategy_py (line 425) | pub fn run_multi_strategy_py(
  function get_str (line 481) | pub fn get_str(d: &Bound<'_, PyDict>, key: &str, default: &str) -> PyRes...
  function get_f64 (line 484) | pub fn get_f64(d: &Bound<'_, PyDict>, key: &str, default: f64) -> PyResu...
  function get_i64 (line 487) | pub fn get_i64(d: &Bound<'_, PyDict>, key: &str, default: i64) -> PyResu...

FILE: rust/ob_python/src/py_balance.rs
  function update_balance (line 21) | pub fn update_balance(

FILE: rust/ob_python/src/py_convexity.rs
  function compute_daily_scores (line 13) | pub fn compute_daily_scores<'py>(
  function run_convexity_backtest (line 67) | pub fn run_convexity_backtest<'py>(

FILE: rust/ob_python/src/py_entries.rs
  function compute_entries (line 23) | pub fn compute_entries(

FILE: rust/ob_python/src/py_execution.rs
  function rust_option_cost (line 23) | pub fn rust_option_cost(
  function rust_stock_cost (line 47) | pub fn rust_stock_cost(
  function rust_fill_price (line 78) | pub fn rust_fill_price(
  function rust_nearest_delta_index (line 107) | pub fn rust_nearest_delta_index(values: Vec<f64>, target: f64) -> usize {
  function rust_max_value_index (line 130) | pub fn rust_max_value_index(values: Vec<f64>) -> usize {
  function rust_risk_check (line 160) | pub fn rust_risk_check(

FILE: rust/ob_python/src/py_exits.rs
  function compute_exit_mask (line 11) | pub fn compute_exit_mask(

FILE: rust/ob_python/src/py_filter.rs
  type CompiledFilter (line 12) | pub struct CompiledFilter {
    method new (line 19) | fn new(query: &str) -> PyResult<Self> {
    method apply (line 25) | fn apply(&self, data: PyDataFrame) -> PyResult<PyDataFrame> {
    method __repr__ (line 34) | fn __repr__(&self) -> String {
  function compile_filter (line 41) | pub fn compile_filter(query: &str) -> PyResult<CompiledFilter> {
  function apply_filter (line 47) | pub fn apply_filter(query: &str, data: PyDataFrame) -> PyResult<PyDataFr...

FILE: rust/ob_python/src/py_stats.rs
  function compute_stats (line 10) | pub fn compute_stats(
  function compute_full_stats (line 44) | pub fn compute_full_stats(
  function set_opt (line 147) | fn set_opt(dict: &Bound<'_, pyo3::types::PyDict>, key: &str, val: Option...

FILE: rust/ob_python/src/py_sweep.rs
  type SweepOverrides (line 28) | struct SweepOverrides {
  type SweepResult (line 46) | struct SweepResult {
  function merge_config (line 55) | fn merge_config(base: &BacktestConfig, overrides: &SweepOverrides) -> Ba...
  function run_single_sweep (line 112) | fn run_single_sweep(
  function parse_opt_f64 (line 154) | fn parse_opt_f64(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Option...
  function parse_overrides (line 163) | fn parse_overrides(dict: &Bound<'_, PyDict>) -> PyResult<SweepOverrides> {
  function parallel_sweep (line 275) | pub fn parallel_sweep(

FILE: tests/analytics/test_analytics_pbt.py
  function _make_balance (line 27) | def _make_balance(returns, initial=100_000.0):
  class TestStatsInvariantsPBT (line 43) | class TestStatsInvariantsPBT:
    method test_max_drawdown_non_negative (line 46) | def test_max_drawdown_non_negative(self, returns, cap):
    method test_max_drawdown_at_most_one (line 53) | def test_max_drawdown_at_most_one(self, returns, cap):
    method test_volatility_non_negative (line 60) | def test_volatility_non_negative(self, returns, cap):
    method test_total_return_matches_endpoints (line 67) | def test_total_return_matches_endpoints(self, returns, cap):
    method test_all_positive_returns_positive_total (line 76) | def test_all_positive_returns_positive_total(self, returns, cap):
    method test_all_negative_returns_negative_total (line 83) | def test_all_negative_returns_negative_total(self, returns, cap):
    method test_all_positive_zero_drawdown (line 90) | def test_all_positive_zero_drawdown(self, returns, cap):
    method test_max_drawdown_duration_non_negative (line 98) | def test_max_drawdown_duration_non_negative(self, returns, cap):
    method test_calmar_sign_matches_return (line 105) | def test_calmar_sign_matches_return(self, returns, cap):
  class TestStatsEmptyEdgePBT (line 116) | class TestStatsEmptyEdgePBT:
    method test_empty_balance (line 117) | def test_empty_balance(self):
    method test_single_row (line 124) | def test_single_row(self, cap):
  class TestTradeStatsPBT (line 135) | class TestTradeStatsPBT:
    method test_wins_plus_losses_equals_total (line 138) | def test_wins_plus_losses_equals_total(self, pnls):
    method test_total_trades_matches_input (line 146) | def test_total_trades_matches_input(self, pnls):
    method test_all_winners (line 155) | def test_all_winners(self, pnls):
    method test_all_losers (line 166) | def test_all_losers(self, pnls):
    method test_largest_win_gte_avg_win (line 176) | def test_largest_win_gte_avg_win(self, pnls):
    method test_largest_loss_lte_avg_loss (line 185) | def test_largest_loss_lte_avg_loss(self, pnls):
    method test_profit_factor_non_negative (line 194) | def test_profit_factor_non_negative(self, pnls):
  class TestSharpePBT (line 206) | class TestSharpePBT:
    method test_finite (line 209) | def test_finite(self, returns, rf):
    method test_higher_rf_lower_sharpe (line 216) | def test_higher_rf_lower_sharpe(self, returns):
    method test_fewer_than_two_returns_zero (line 224) | def test_fewer_than_two_returns_zero(self):
  class TestSortinoPBT (line 231) | class TestSortinoPBT:
    method test_finite (line 234) | def test_finite(self, returns, rf):
    method test_all_positive_returns_zero_sortino (line 241) | def test_all_positive_returns_zero_sortino(self, returns):
  class TestPeriodStatsPBT (line 253) | class TestPeriodStatsPBT:
    method test_best_gte_worst (line 256) | def test_best_gte_worst(self, returns, rf):
    method test_vol_non_negative (line 263) | def test_vol_non_negative(self, returns, rf):
    method test_mean_between_best_and_worst (line 270) | def test_mean_between_best_and_worst(self, returns, rf):
  class TestLookbackPBT (line 281) | class TestLookbackPBT:
    method test_mtd_always_computed (line 284) | def test_mtd_always_computed(self, returns, cap):
    method test_ytd_always_computed (line 291) | def test_ytd_always_computed(self, returns, cap):
    method test_all_positive_lookbacks_positive (line 298) | def test_all_positive_lookbacks_positive(self, returns, cap):
  class TestTurnoverHerfindahlPBT (line 316) | class TestTurnoverHerfindahlPBT:
    method test_turnover_non_negative (line 319) | def test_turnover_non_negative(self, returns, cap):
    method test_herfindahl_non_negative (line 326) | def test_herfindahl_non_negative(self, returns, cap):
    method test_no_stock_cols_zero_turnover (line 331) | def test_no_stock_cols_zero_turnover(self):
  class TestBalanceRangePBT (line 343) | class TestBalanceRangePBT:
    method test_range_subset_shorter (line 346) | def test_range_subset_shorter(self, returns, cap):
  class TestOutputFormattingPBT (line 364) | class TestOutputFormattingPBT:
    method test_to_dataframe_not_empty (line 367) | def test_to_dataframe_not_empty(self, returns, cap):
    method test_summary_not_empty (line 376) | def test_summary_not_empty(self, returns, cap):

FILE: tests/analytics/test_charts.py
  function _make_balance (line 17) | def _make_balance() -> pd.DataFrame:
  function test_weights_chart_returns_fig_ax (line 28) | def test_weights_chart_returns_fig_ax():
  function test_weights_chart_no_positions (line 36) | def test_weights_chart_no_positions():
  function test_weights_chart_single_symbol (line 47) | def test_weights_chart_single_symbol():
  function _make_balance_report (line 61) | def _make_balance_report(days=90):
  function test_returns_chart_returns_vconcat (line 76) | def test_returns_chart_returns_vconcat():
  function test_returns_chart_has_two_panels (line 83) | def test_returns_chart_has_two_panels():
  function test_returns_chart_serializes_to_dict (line 90) | def test_returns_chart_serializes_to_dict():
  function test_returns_histogram_returns_chart (line 99) | def test_returns_histogram_returns_chart():
  function test_returns_histogram_serializes (line 106) | def test_returns_histogram_serializes():
  function test_monthly_returns_heatmap_returns_chart (line 114) | def test_monthly_returns_heatmap_returns_chart():
  function test_monthly_returns_heatmap_serializes (line 121) | def test_monthly_returns_heatmap_serializes():
  function test_returns_chart_has_interval_and_point_params (line 129) | def test_returns_chart_has_interval_and_point_params():
  function test_returns_chart_data_included (line 142) | def test_returns_chart_data_included():

FILE: tests/analytics/test_optimization.py
  function _dummy_run_fn (line 12) | def _dummy_run_fn(param_a=1, param_b=2):
  function _failing_run_fn (line 24) | def _failing_run_fn(param_a=1):
  function _dummy_wf_fn (line 31) | def _dummy_wf_fn(start_date, end_date):
  class TestOptimizationResult (line 43) | class TestOptimizationResult:
    method test_fields (line 44) | def test_fields(self):
  class TestGridSweep (line 52) | class TestGridSweep:
    method test_returns_results_for_all_combos (line 53) | def test_returns_results_for_all_combos(self):
    method test_sorted_by_sharpe_descending (line 61) | def test_sorted_by_sharpe_descending(self):
    method test_single_combo (line 70) | def test_single_combo(self):
    method test_failing_fn_skipped (line 78) | def test_failing_fn_skipped(self):
  class TestWalkForward (line 88) | class TestWalkForward:
    method test_returns_splits (line 89) | def test_returns_splits(self):
    method test_single_split (line 97) | def test_single_split(self):
    method test_failing_wf_fn_skipped (line 102) | def test_failing_wf_fn_skipped(self):
    method test_custom_in_sample_pct (line 118) | def test_custom_in_sample_pct(self):

FILE: tests/analytics/test_stats.py
  function _make_balance (line 12) | def _make_balance(returns: list[float], initial: float = 100_000.0) -> p...
  class TestProfitFactor (line 23) | class TestProfitFactor:
    method test_profit_factor_dollar_ratio (line 26) | def test_profit_factor_dollar_ratio(self):
    method test_profit_factor_not_count_ratio (line 35) | def test_profit_factor_not_count_ratio(self):
    method test_profit_factor_no_losses (line 44) | def test_profit_factor_no_losses(self):
    method test_profit_factor_no_wins (line 50) | def test_profit_factor_no_wins(self):
  class TestReturnMetrics (line 57) | class TestReturnMetrics:
    method test_total_return (line 58) | def test_total_return(self):
    method test_zero_return (line 64) | def test_zero_return(self):
    method test_sharpe_positive (line 69) | def test_sharpe_positive(self):
  class TestDrawdown (line 78) | class TestDrawdown:
    method test_max_drawdown (line 79) | def test_max_drawdown(self):
    method test_no_drawdown (line 86) | def test_no_drawdown(self):
    method test_drawdown_duration (line 92) | def test_drawdown_duration(self):
  class TestTradeStats (line 100) | class TestTradeStats:
    method test_wins_losses_count (line 101) | def test_wins_losses_count(self):
    method test_win_pct (line 109) | def test_win_pct(self):
    method test_empty_balance (line 115) | def test_empty_balance(self):
    method test_no_trade_pnls (line 121) | def test_no_trade_pnls(self):
  class TestToDataframe (line 128) | class TestToDataframe:
    method test_shape (line 129) | def test_shape(self):
    method test_summary_string (line 137) | def test_summary_string(self):
  class TestPeriodStats (line 145) | class TestPeriodStats:
    method test_daily_stats_computed (line 146) | def test_daily_stats_computed(self):
    method test_monthly_stats_computed (line 157) | def test_monthly_stats_computed(self):
    method test_yearly_stats_computed (line 166) | def test_yearly_stats_computed(self):
    method test_skew_kurtosis_with_enough_data (line 175) | def test_skew_kurtosis_with_enough_data(self):
    method test_skew_kurtosis_not_computed_with_few_points (line 183) | def test_skew_kurtosis_not_computed_with_few_points(self):
  class TestAvgDrawdown (line 190) | class TestAvgDrawdown:
    method test_avg_drawdown_depth (line 191) | def test_avg_drawdown_depth(self):
    method test_avg_drawdown_duration (line 198) | def test_avg_drawdown_duration(self):
  class TestLookbackReturns (line 206) | class TestLookbackReturns:
    method test_mtd_and_ytd (line 207) | def test_mtd_and_ytd(self):
    method test_one_year_return (line 215) | def test_one_year_return(self):
    method test_lookback_table (line 222) | def test_lookback_table(self):
    method test_short_series_lookback_equals_total (line 231) | def test_short_series_lookback_equals_total(self):
  class TestTurnover (line 240) | class TestTurnover:
    method test_turnover_zero_for_no_stocks (line 241) | def test_turnover_zero_for_no_stocks(self):
    method test_turnover_computed_with_stocks (line 246) | def test_turnover_computed_with_stocks(self):
  class TestHerfindahl (line 261) | class TestHerfindahl:
    method test_single_stock_hhi_is_one (line 262) | def test_single_stock_hhi_is_one(self):
    method test_two_equal_stocks_hhi (line 273) | def test_two_equal_stocks_hhi(self):
  class TestFromBalanceRange (line 288) | class TestFromBalanceRange:
    method test_slice_start (line 289) | def test_slice_start(self):
    method test_slice_end (line 297) | def test_slice_end(self):
    method test_slice_both (line 305) | def test_slice_both(self):
    method test_empty_balance (line 313) | def test_empty_balance(self):
    method test_no_slice (line 318) | def test_no_slice(self):

FILE: tests/analytics/test_stats_python_path.py
  function _make_balance (line 14) | def _make_balance(returns, initial=100_000.0, start="2020-01-01"):
  function _make_balance_with_stocks (line 24) | def _make_balance_with_stocks(returns, initial=100_000.0, start="2020-01...
  class TestFromBalanceReturnMetrics (line 35) | class TestFromBalanceReturnMetrics:
    method test_total_return (line 36) | def test_total_return(self):
    method test_annualized_return (line 43) | def test_annualized_return(self):
    method test_volatility (line 49) | def test_volatility(self):
    method test_sharpe_and_sortino (line 55) | def test_sharpe_and_sortino(self):
  class TestFromBalanceDrawdown (line 64) | class TestFromBalanceDrawdown:
    method test_drawdown_with_losses (line 65) | def test_drawdown_with_losses(self):
    method test_avg_drawdown (line 72) | def test_avg_drawdown(self):
    method test_calmar_ratio (line 79) | def test_calmar_ratio(self):
    method test_no_drawdown (line 85) | def test_no_drawdown(self):
  class TestFromBalanceTailRatio (line 93) | class TestFromBalanceTailRatio:
    method test_tail_ratio_enough_data (line 94) | def test_tail_ratio_enough_data(self):
    method test_tail_ratio_insufficient_data (line 101) | def test_tail_ratio_insufficient_data(self):
  class TestFromBalancePeriodStats (line 108) | class TestFromBalancePeriodStats:
    method test_daily_stats (line 109) | def test_daily_stats(self):
    method test_skew_kurtosis_need_8_returns (line 119) | def test_skew_kurtosis_need_8_returns(self):
    method test_skew_kurtosis_with_enough_data (line 126) | def test_skew_kurtosis_with_enough_data(self):
  class TestFromBalanceLookback (line 135) | class TestFromBalanceLookback:
    method test_lookback_mtd_ytd (line 136) | def test_lookback_mtd_ytd(self):
    method test_lookback_trailing_periods (line 143) | def test_lookback_trailing_periods(self):
  class TestFromBalanceTurnoverHerfindahl (line 153) | class TestFromBalanceTurnoverHerfindahl:
    method test_turnover_with_stocks (line 154) | def test_turnover_with_stocks(self):
    method test_herfindahl_with_stocks (line 160) | def test_herfindahl_with_stocks(self):
    method test_turnover_no_stocks (line 166) | def test_turnover_no_stocks(self):
    method test_herfindahl_no_stocks (line 172) | def test_herfindahl_no_stocks(self):
  class TestFromBalanceTradeStats (line 179) | class TestFromBalanceTradeStats:
    method test_trade_stats_full (line 180) | def test_trade_stats_full(self):
    method test_trade_stats_all_wins (line 195) | def test_trade_stats_all_wins(self):
    method test_trade_stats_all_losses (line 202) | def test_trade_stats_all_losses(self):
    method test_trade_stats_none (line 211) | def test_trade_stats_none(self):
  class TestSummaryText (line 218) | class TestSummaryText:
    method test_summary_minimal (line 219) | def test_summary_minimal(self):
    method test_summary_with_turnover (line 226) | def test_summary_with_turnover(self):
  class TestLookbackTable (line 234) | class TestLookbackTable:
    method test_lookback_table_nonempty (line 235) | def test_lookback_table_nonempty(self):
    method test_lookback_table_empty_when_no_data (line 243) | def test_lookback_table_empty_when_no_data(self):
  class TestToDataframe (line 249) | class TestToDataframe:
    method test_has_expected_rows (line 250) | def test_has_expected_rows(self):
  class TestFromBalanceSharpe (line 260) | class TestFromBalanceSharpe:
    method test_positive_returns_with_variance (line 261) | def test_positive_returns_with_variance(self):
    method test_all_positive_sortino_zero (line 268) | def test_all_positive_sortino_zero(self):
    method test_mixed_returns_sortino_nonzero (line 275) | def test_mixed_returns_sortino_nonzero(self):
  class TestFromBalanceDispatch (line 282) | class TestFromBalanceDispatch:
    method test_from_balance_empty (line 285) | def test_from_balance_empty(self):
    method test_from_balance_basic (line 290) | def test_from_balance_basic(self):
    method test_from_balance_with_trade_pnls (line 297) | def test_from_balance_with_trade_pnls(self):
  class TestFromBalanceRange (line 305) | class TestFromBalanceRange:
    method test_empty_balance (line 308) | def test_empty_balance(self):
    method test_full_range (line 313) | def test_full_range(self):
    method test_with_start (line 319) | def test_with_start(self):
    method test_with_end (line 326) | def test_with_end(self):
    method test_with_start_and_end (line 332) | def test_with_start_and_end(self):
    method test_out_of_range_returns_empty (line 338) | def test_out_of_range_returns_empty(self):

FILE: tests/analytics/test_summary.py
  function _make_trade_log_and_balance (line 10) | def _make_trade_log_and_balance():
  class TestSummary (line 49) | class TestSummary:
    method test_returns_styler (line 50) | def test_returns_styler(self):
    method test_summary_has_expected_rows (line 55) | def test_summary_has_expected_rows(self):
    method test_total_trades_count (line 64) | def test_total_trades_count(self):
    method test_win_metrics (line 71) | def test_win_metrics(self):
    method test_summary_with_missing_exit (line 79) | def test_summary_with_missing_exit(self):

FILE: tests/analytics/test_tearsheet.py
  function _balance (line 16) | def _balance(periods: int = 40) -> pd.DataFrame:
  function test_build_tearsheet_has_expected_artifacts (line 30) | def test_build_tearsheet_has_expected_artifacts():
  function test_build_tearsheet_no_trades (line 39) | def test_build_tearsheet_no_trades():
  function test_build_tearsheet_with_risk_free_rate (line 44) | def test_build_tearsheet_with_risk_free_rate():
  function test_tearsheet_to_dict_shape (line 53) | def test_tearsheet_to_dict_shape():
  function test_tearsheet_exports (line 66) | def test_tearsheet_exports(tmp_path: Path):
  function test_csv_creates_directories (line 76) | def test_csv_creates_directories(tmp_path: Path):
  function test_html_contains_tables (line 84) | def test_html_contains_tables():
  function test_tearsheet_markdown_fallback_without_tabulate (line 95) | def test_tearsheet_markdown_fallback_without_tabulate():
  function test_monthly_return_table_has_year_month_structure (line 108) | def test_monthly_return_table_has_year_month_structure():
  function test_monthly_return_table_empty_balance (line 116) | def test_monthly_return_table_empty_balance():
  function test_monthly_return_table_no_pct_change (line 121) | def test_monthly_return_table_no_pct_change():
  function test_drawdown_series_shape (line 130) | def test_drawdown_series_shape():
  function test_drawdown_series_peak_at_start (line 137) | def test_drawdown_series_peak_at_start():
  function test_drawdown_series_empty (line 145) | def test_drawdown_series_empty():
  function test_drawdown_series_no_total_capital (line 151) | def test_drawdown_series_no_total_capital():
  function test_build_tearsheet_single_day (line 161) | def test_build_tearsheet_single_day():
  function test_build_tearsheet_flat_returns (line 171) | def test_build_tearsheet_flat_returns():

FILE: tests/analytics/test_trade_log.py
  function _make_trade (line 10) | def _make_trade(pnl_sign: float = 1.0) -> Trade:
  class TestTrade (line 27) | class TestTrade:
    method test_gross_pnl (line 28) | def test_gross_pnl(self):
    method test_gross_pnl_loss (line 33) | def test_gross_pnl_loss(self):
    method test_net_pnl_with_commission (line 38) | def test_net_pnl_with_commission(self):
    method test_return_pct (line 49) | def test_return_pct(self):
  class TestTradeLog (line 55) | class TestTradeLog:
    method test_add_and_len (line 56) | def test_add_and_len(self):
    method test_winners_losers (line 62) | def test_winners_losers(self):
    method test_net_pnls (line 70) | def test_net_pnls(self):
    method test_to_dataframe (line 79) | def test_to_dataframe(self):
    method test_empty_to_dataframe (line 87) | def test_empty_to_dataframe(self):
    method test_from_legacy_empty (line 92) | def test_from_legacy_empty(self):
    method test_from_legacy_trade_log (line 96) | def test_from_legacy_trade_log(self):
    method test_return_pct_zero_entry (line 132) | def test_return_pct_zero_entry(self):

FILE: tests/bench/_test_helpers.py
  function _stocks_from_tuples (line 50) | def _stocks_from_tuples(tuples):
  function ivy_stocks (line 55) | def ivy_stocks():
  function generated_stocks (line 59) | def generated_stocks():
  function prod_spy_stocks (line 63) | def prod_spy_stocks():
  function slice_stocks (line 67) | def slice_stocks(slice_id):
  function load_small_stocks (line 73) | def load_small_stocks():
  function load_small_options (line 80) | def load_small_options():
  function load_large_stocks (line 94) | def load_large_stocks():
  function load_large_options (line 99) | def load_large_options():
  function load_generated_stocks (line 104) | def load_generated_stocks():
  function load_generated_options (line 109) | def load_generated_options():
  function load_prod_stocks (line 114) | def load_prod_stocks():
  function load_prod_options (line 119) | def load_prod_options():
  function slice_data_exists (line 124) | def slice_data_exists(slice_id):
  function load_slice_stocks (line 131) | def load_slice_stocks(slice_id):
  function load_slice_options (line 136) | def load_slice_options(slice_id):
  function buy_put_strategy (line 145) | def buy_put_strategy(schema, underlying="SPX", dte_min=60, dte_max=None,
  function buy_call_strategy (line 163) | def buy_call_strategy(schema, underlying="SPX", dte_min=60, dte_exit=30):
  function sell_put_strategy (line 177) | def sell_put_strategy(schema, underlying="SPX", dte_min=60, dte_exit=30):
  function sell_call_strategy (line 191) | def sell_call_strategy(schema, underlying="SPX", dte_min=60, dte_exit=30):
  function strangle_strategy (line 205) | def strangle_strategy(schema, underlying="SPX", dte_min=60, dte_exit=30):
  function straddle_strategy (line 221) | def straddle_strategy(schema, underlying="SPX", dte_min=60, dte_exit=30):
  function buy_put_spread_strategy (line 237) | def buy_put_spread_strategy(schema, underlying="SPX", dte_min=60, dte_ex...
  function sell_call_spread_strategy (line 253) | def sell_call_spread_strategy(schema, underlying="SPX", dte_min=60, dte_...
  function two_leg_strategy (line 269) | def two_leg_strategy(schema, dir1, type1, dir2, type2,
  function make_cost_model (line 306) | def make_cost_model(name):
  function make_fill_model (line 319) | def make_fill_model(name):
  function make_signal_selector (line 332) | def make_signal_selector(name):
  function _make_engine (line 347) | def _make_engine(alloc, capital, stocks, stocks_data, options_data,
  function run_backtest (line 366) | def run_backtest(alloc=None, capital=None, strategy_fn=None,
  function assert_invariants (line 386) | def assert_invariants(eng, min_trades=0, label="", allow_negative_capita...

FILE: tests/bench/extract_prod_slices.py
  function _convert_options (line 72) | def _convert_options(opts: pd.DataFrame, symbol: str,
  function _convert_underlying (line 107) | def _convert_underlying(und: pd.DataFrame, symbol: str) -> pd.DataFrame:
  function extract_slice (line 134) | def extract_slice(slice_id: str, spec: dict) -> None:
  function main (line 202) | def main():

FILE: tests/bench/generate_test_data.py
  function _generate_trading_dates (line 31) | def _generate_trading_dates(n: int, start: str = "2017-01-03") -> pd.Dat...
  function _random_walk (line 35) | def _random_walk(rng: np.random.Generator, initial: float, n: int,
  function generate_stocks (line 42) | def generate_stocks(dates: pd.DatetimeIndex, rng: np.random.Generator) -...
  function _bs_approx (line 64) | def _bs_approx(S: float, K: float, T: float, vol: float, is_call: bool) ...
  function generate_options (line 93) | def generate_options(dates: pd.DatetimeIndex, rng: np.random.Generator) ...
  function main (line 183) | def main():

FILE: tests/bench/test_edge_cases.py
  class TestAllocationEdgeCases (line 32) | class TestAllocationEdgeCases:
    method test_zero_options (line 34) | def test_zero_options(self):
    method test_high_options (line 40) | def test_high_options(self):
    method test_tiny_stocks (line 45) | def test_tiny_stocks(self):
  class TestCapitalEdgeCases (line 53) | class TestCapitalEdgeCases:
    method test_tiny_capital (line 55) | def test_tiny_capital(self):
    method test_huge_capital (line 59) | def test_huge_capital(self):
  class TestRebalanceEdgeCases (line 66) | class TestRebalanceEdgeCases:
    method test_weekly_rebalance (line 68) | def test_weekly_rebalance(self):
  class TestDirectionAndType (line 75) | class TestDirectionAndType:
    method test_sell_put (line 77) | def test_sell_put(self):
    method test_buy_call (line 83) | def test_buy_call(self):
  class TestSMAGating (line 90) | class TestSMAGating:
    method test_sma_50 (line 92) | def test_sma_50(self):
  class TestOptionsBudgetPct (line 99) | class TestOptionsBudgetPct:
    method test_budget_limits_spending (line 101) | def test_budget_limits_spending(self):
  class TestNoMatchingEntries (line 114) | class TestNoMatchingEntries:
    method test_filter_matches_nothing (line 116) | def test_filter_matches_nothing(self):

FILE: tests/bench/test_execution_models.py
  class TestCostModels (line 28) | class TestCostModels:
    method test_cost_model (line 30) | def test_cost_model(self, cost_name):
  class TestFillModels (line 37) | class TestFillModels:
    method test_fill_model (line 39) | def test_fill_model(self, fill_name):
  class TestSignalSelectors (line 46) | class TestSignalSelectors:
    method test_signal_selector (line 48) | def test_signal_selector(self, signal_name):
  class TestRiskConstraints (line 55) | class TestRiskConstraints:
    method test_max_delta (line 56) | def test_max_delta(self):
    method test_max_drawdown (line 63) | def test_max_drawdown(self):
  class TestExitThresholds (line 73) | class TestExitThresholds:
    method test_profit_exit (line 74) | def test_profit_exit(self):
    method test_loss_exit (line 86) | def test_loss_exit(self):
  class TestModelGrid (line 101) | class TestModelGrid:
    method test_model_combo (line 105) | def test_model_combo(self, cost_name, fill_name, signal_name):

FILE: tests/bench/test_invariants.py
  class TestBalanceSheetInvariants (line 39) | class TestBalanceSheetInvariants:
    method _engine (line 42) | def _engine(self):
    method test_total_capital_equals_parts (line 45) | def test_total_capital_equals_parts(self):
    method test_capital_never_negative (line 48) | def test_capital_never_negative(self):
    method test_initial_capital_correct (line 52) | def test_initial_capital_correct(self):
    method test_balance_dates_monotonic (line 56) | def test_balance_dates_monotonic(self):
    method test_balance_not_empty (line 59) | def test_balance_not_empty(self):
    method test_cash_column_exists (line 62) | def test_cash_column_exists(self):
  class TestTradeLogInvariants (line 66) | class TestTradeLogInvariants:
    method _engine (line 69) | def _engine(self):
    method test_trade_log_not_empty (line 72) | def test_trade_log_not_empty(self):
    method test_entry_costs_nonzero (line 75) | def test_entry_costs_nonzero(self):
    method test_qty_positive_on_entry (line 81) | def test_qty_positive_on_entry(self):
    method test_trade_dates_within_data_range (line 87) | def test_trade_dates_within_data_range(self):
  class TestBalanceColumns (line 97) | class TestBalanceColumns:
    method _engine (line 100) | def _engine(self):
    method test_required_columns (line 103) | def test_required_columns(self):
    method test_per_stock_columns (line 113) | def test_per_stock_columns(self):
  class TestGeneratedDataInvariants (line 121) | class TestGeneratedDataInvariants:
    method _engine (line 124) | def _engine(self):
    method test_invariants (line 131) | def test_invariants(self):
    method test_many_balance_rows (line 134) | def test_many_balance_rows(self):
    method test_initial_capital (line 137) | def test_initial_capital(self):
  class TestProductionDataInvariants (line 144) | class TestProductionDataInvariants:
    method _engine (line 147) | def _engine(self):
    method test_invariants (line 155) | def test_invariants(self):
    method test_capital_never_negative (line 158) | def test_capital_never_negative(self):

FILE: tests/bench/test_multi_leg.py
  class TestMultiLegStrategies (line 28) | class TestMultiLegStrategies:
    method test_strangle (line 30) | def test_strangle(self):
    method test_straddle (line 34) | def test_straddle(self):
    method test_put_spread (line 38) | def test_put_spread(self):
    method test_call_spread (line 42) | def test_call_spread(self):
  class TestMixedDirections (line 47) | class TestMixedDirections:
    method test_direction_combo (line 57) | def test_direction_combo(self, d1, t1, d2, t2):
  class TestPerLegOverrides (line 66) | class TestPerLegOverrides:
    method test_per_leg_signal_selector (line 68) | def test_per_leg_signal_selector(self):
    method test_per_leg_fill_model (line 77) | def test_per_leg_fill_model(self):

FILE: tests/bench/test_partial_exits.py
  class TestPartialExitGenerated (line 35) | class TestPartialExitGenerated:
    method _run (line 37) | def _run(self, strategy_fn):
    method test_buy_put (line 46) | def test_buy_put(self):
    method test_sell_put (line 50) | def test_sell_put(self):
    method test_strangle (line 55) | def test_strangle(self):
  class TestPartialExitProduction (line 61) | class TestPartialExitProduction:
    method _run (line 63) | def _run(self, strategy_fn):
    method test_buy_put_spy (line 72) | def test_buy_put_spy(self):
    method test_sell_put_spy (line 76) | def test_sell_put_spy(self):
  class TestPartialExitCashAccounting (line 82) | class TestPartialExitCashAccounting:
    method test_cash_never_deeply_negative (line 84) | def test_cash_never_deeply_negative(self):

FILE: tests/bench/test_sweep.py
  function _pd_to_pl (line 20) | def _pd_to_pl(df: pd.DataFrame) -> "pl.DataFrame":
  function _dates_to_ns (line 24) | def _dates_to_ns(dates: list[str]) -> list[int]:
  function _ensure_datetime_cols (line 28) | def _ensure_datetime_cols(df: pd.DataFrame, cols: list[str]) -> pd.DataF...
  function _make_test_data (line 36) | def _make_test_data():
  function _base_config (line 66) | def _base_config():
  function _schema (line 87) | def _schema():
  class TestSweep (line 97) | class TestSweep:
    method test_single_config_matches_direct (line 99) | def test_single_config_matches_direct(self):
    method test_multiple_configs (line 118) | def test_multiple_configs(self):
    method test_per_leg_filter_overrides (line 133) | def test_per_leg_filter_overrides(self):
    method test_bad_filter_returns_error (line 147) | def test_bad_filter_returns_error(self):
    method test_empty_param_grid (line 159) | def test_empty_param_grid(self):
    method test_deterministic (line 166) | def test_deterministic(self):
  class TestGridSweepWrapper (line 184) | class TestGridSweepWrapper:
    method test_sorted_by_sharpe (line 186) | def test_sorted_by_sharpe(self):

FILE: tests/compat/test_bt_overlap_gate.py
  function test_bt_overlap_gate_stock_only (line 11) | def test_bt_overlap_gate_stock_only():

FILE: tests/conftest.py
  function pytest_collection_modifyitems (line 8) | def pytest_collection_modifyitems(config, items):

FILE: tests/convexity/conftest.py
  class MockOptionsData (line 10) | class MockOptionsData:
    method __init__ (line 13) | def __init__(self, df: pd.DataFrame):
  class MockStocksData (line 17) | class MockStocksData:
    method __init__ (line 20) | def __init__(self, df: pd.DataFrame):
  function _make_put_row (line 24) | def _make_put_row(date, strike, bid, ask, delta, underlying, dte, iv, ex...
  function instrument_config (line 40) | def instrument_config():
  function backtest_config (line 53) | def backtest_config():
  function synthetic_options (line 61) | def synthetic_options():
  function synthetic_stocks (line 80) | def synthetic_stocks():
  function empty_options (line 90) | def empty_options():
  function empty_stocks (line 100) | def empty_stocks():

FILE: tests/convexity/test_allocator.py
  class TestPickCheapest (line 12) | class TestPickCheapest:
    method test_picks_highest_ratio (line 13) | def test_picks_highest_ratio(self):
    method test_single_instrument (line 17) | def test_single_instrument(self):
    method test_empty_raises (line 20) | def test_empty_raises(self):
  class TestEqualWeight (line 25) | class TestEqualWeight:
    method test_splits_evenly (line 26) | def test_splits_evenly(self):
    method test_single (line 30) | def test_single(self):
    method test_empty (line 34) | def test_empty(self):
  class TestInverseVol (line 38) | class TestInverseVol:
    method test_lower_vol_gets_more (line 39) | def test_lower_vol_gets_more(self):
    method test_equal_vol (line 44) | def test_equal_vol(self):
    method test_zero_vol_falls_back (line 49) | def test_zero_vol_falls_back(self):

FILE: tests/convexity/test_backtest.py
  class TestRunBacktest (line 13) | class TestRunBacktest:
    method test_returns_monthly_records (line 14) | def test_returns_monthly_records(
    method test_daily_balance_populated (line 21) | def test_daily_balance_populated(
    method test_budget_deducted (line 28) | def test_budget_deducted(
    method test_empty_options (line 35) | def test_empty_options(
  class TestRunUnhedged (line 43) | class TestRunUnhedged:
    method test_returns_correct_shape (line 44) | def test_returns_correct_shape(self, synthetic_stocks, backtest_config):
    method test_initial_value_matches_capital (line 52) | def test_initial_value_matches_capital(self, synthetic_stocks, backtes...

FILE: tests/convexity/test_config.py
  class TestConfig (line 10) | class TestConfig:
    method test_instrument_defaults (line 11) | def test_instrument_defaults(self):
    method test_backtest_defaults (line 18) | def test_backtest_defaults(self):
    method test_default_config (line 23) | def test_default_config(self):

FILE: tests/core/test_types.py
  class TestDirection (line 13) | class TestDirection:
    method test_buy_price_column_is_ask (line 14) | def test_buy_price_column_is_ask(self):
    method test_sell_price_column_is_bid (line 17) | def test_sell_price_column_is_bid(self):
    method test_invert_buy (line 20) | def test_invert_buy(self):
    method test_invert_sell (line 23) | def test_invert_sell(self):
    method test_decoupled_from_column_name (line 26) | def test_decoupled_from_column_name(self):
  class TestOptionType (line 36) | class TestOptionType:
    method test_invert_call (line 37) | def test_invert_call(self):
    method test_invert_put (line 40) | def test_invert_put(self):
  class TestOrder (line 48) | class TestOrder:
    method test_invert_bto (line 49) | def test_invert_bto(self):
    method test_invert_stc (line 52) | def test_invert_stc(self):
    method test_invert_sto (line 55) | def test_invert_sto(self):
    method test_invert_btc (line 58) | def test_invert_btc(self):
  class TestGetOrder (line 66) | class TestGetOrder:
    method test_buy_entry (line 67) | def test_buy_entry(self):
    method test_buy_exit (line 70) | def test_buy_exit(self):
    method test_sell_entry (line 73) | def test_sell_entry(self):
    method test_sell_exit (line 76) | def test_sell_exit(self):
  class TestGreeks (line 84) | class TestGreeks:
    method test_default_zeros (line 85) | def test_default_zeros(self):
    method test_addition (line 92) | def test_addition(self):
    method test_scalar_multiply (line 101) | def test_scalar_multiply(self):
    method test_rmul (line 107) | def test_rmul(self):
    method test_negation (line 112) | def test_negation(self):
    method test_as_dict (line 118) | def test_as_dict(self):
    method test_frozen (line 123) | def test_frozen(self):
  class TestFill (line 136) | class TestFill:
    method test_buy_fill_notional (line 137) | def test_buy_fill_notional(self):
    method test_sell_fill_notional (line 142) | def test_sell_fill_notional(self):
    method test_fill_with_commission (line 147) | def test_fill_with_commission(self):
    method test_fill_with_slippage (line 153) | def test_fill_with_slippage(self):
    method test_fill_with_commission_and_slippage (line 158) | def test_fill_with_commission_and_slippage(self):
  class TestOptionContract (line 168) | class TestOptionContract:
    method test_creation (line 169) | def test_creation(self):
  class TestStockAllocation (line 186) | class TestStockAllocation:
    method test_creation (line 187) | def test_creation(self):
    method test_stock_alias (line 192) | def test_stock_alias(self):

FILE: tests/core/test_types_pbt.py
  class TestDirectionPBT (line 48) | class TestDirectionPBT:
    method test_price_column_is_bid_or_ask (line 50) | def test_price_column_is_bid_or_ask(self, d):
    method test_buy_maps_to_ask (line 54) | def test_buy_maps_to_ask(self, d):
    method test_invert_changes_price_column (line 61) | def test_invert_changes_price_column(self, d):
    method test_double_invert_identity (line 65) | def test_double_invert_identity(self, d):
    method test_invert_is_different (line 69) | def test_invert_is_different(self, d):
  class TestOptionTypePBT (line 78) | class TestOptionTypePBT:
    method test_double_invert_identity (line 80) | def test_double_invert_identity(self, ot):
    method test_invert_is_different (line 84) | def test_invert_is_different(self, ot):
    method test_call_inverts_to_put (line 88) | def test_call_inverts_to_put(self, ot):
  class TestOrderPBT (line 100) | class TestOrderPBT:
    method test_double_invert_identity (line 102) | def test_double_invert_identity(self, o):
    method test_invert_changes_buy_sell (line 106) | def test_invert_changes_buy_sell(self, o):
    method test_get_order_exhaustive (line 112) | def test_get_order_exhaustive(self, d, s):
    method test_get_order_entry_exit_paired (line 118) | def test_get_order_entry_exit_paired(self, d, s):
    method test_buy_entry_is_bto (line 125) | def test_buy_entry_is_bto(self, d):
  class TestGreeksFieldsPBT (line 137) | class TestGreeksFieldsPBT:
    method test_has_four_fields (line 139) | def test_has_four_fields(self, g):
    method test_frozen (line 144) | def test_frozen(self, g):
    method test_construction (line 149) | def test_construction(self, d, ga, th, v):
  class TestGreeksAdditionPBT (line 157) | class TestGreeksAdditionPBT:
    method test_commutative (line 160) | def test_commutative(self, a, b):
    method test_associative (line 165) | def test_associative(self, a, b, c):
    method test_zero_identity (line 170) | def test_zero_identity(self, g):
    method test_inverse (line 175) | def test_inverse(self, g):
  class TestGreeksScalarMulPBT (line 179) | class TestGreeksScalarMulPBT:
    method test_componentwise (line 182) | def test_componentwise(self, g, s):
    method test_identity (line 191) | def test_identity(self, g):
    method test_zero (line 196) | def test_zero(self, g):
    method test_rmul (line 201) | def test_rmul(self, g, s):
    method test_distributes_over_addition (line 206) | def test_distributes_over_addition(self, a, b, s):
  class TestFillPBT (line 219) | class TestFillPBT:
    method test_direction_sign_matches (line 222) | def test_direction_sign_matches(self, p, q, d, s):
    method test_sell_notional_exceeds_buy (line 229) | def test_sell_notional_exceeds_buy(self, p, q, s, comm, slip):
    method test_zero_costs_notional (line 239) | def test_zero_costs_notional(self, p, q, d, s):
    method test_costs_reduce_notional (line 246) | def test_costs_reduce_notional(self, p, q, d, s, comm, slip):
    method test_higher_commission_lower_notional (line 256) | def test_higher_commission_lower_notional(self, p, q, d, s, c1, c2):
  class TestStockAllocationPBT (line 268) | class TestStockAllocationPBT:
    method test_named_tuple_fields (line 271) | def test_named_tuple_fields(self, sym, p):
  function _greeks_close (line 283) | def _greeks_close(a: Greeks, b: Greeks, tol: float = 1e-10) -> bool:

FILE: tests/data/test_filter.py
  function test_strike_eq_100 (line 6) | def test_strike_eq_100():
  function test_strike_lt_100 (line 13) | def test_strike_lt_100():
  function test_strike_ge_100 (line 20) | def test_strike_ge_100():
  function test_negate_filter (line 27) | def test_negate_filter():
  function test_compose_filters_with_and (line 35) | def test_compose_filters_with_and():
  function test_compose_filters_with_or (line 45) | def test_compose_filters_with_or():
  function test_compose_many_filters (line 54) | def test_compose_many_filters():
  function test_add_number_to_field (line 65) | def test_add_number_to_field():
  function test_subtract_number_from_field (line 73) | def test_subtract_number_from_field():
  function test_multiply_field_by_number (line 81) | def test_multiply_field_by_number():
  function test_multiply_on_left (line 89) | def test_multiply_on_left():
  function test_filter_from_combined_field (line 97) | def test_filter_from_combined_field():

FILE: tests/data/test_property_based.py
  function _make_df (line 19) | def _make_df(n_rows, col_name="strike", values=None):
  class TestFilterProperties (line 31) | class TestFilterProperties:
    method test_filter_returns_subset (line 37) | def test_filter_returns_subset(self, threshold, n_rows):
    method test_impossible_range_empty (line 49) | def test_impossible_range_empty(self, n_rows):
    method test_numeric_filter_bounds (line 62) | def test_numeric_filter_bounds(self, threshold, n_rows):
    method test_and_is_intersection (line 78) | def test_and_is_intersection(self, lo, hi, n_rows):
    method test_or_is_union (line 100) | def test_or_is_union(self, lo, hi, n_rows):

FILE: tests/data/test_providers.py
  function options_provider (line 19) | def options_provider():
  function stocks_provider (line 24) | def stocks_provider():
  class TestCsvOptionsProvider (line 28) | class TestCsvOptionsProvider:
    method test_is_data_provider (line 29) | def test_is_data_provider(self, options_provider):
    method test_has_schema (line 33) | def test_has_schema(self, options_provider):
    method test_data_is_dataframe (line 36) | def test_data_is_dataframe(self, options_provider):
    method test_start_end_dates (line 39) | def test_start_end_dates(self, options_provider):
    method test_len (line 44) | def test_len(self, options_provider):
    method test_iter_dates (line 47) | def test_iter_dates(self, options_provider):
  class TestCsvStocksProvider (line 52) | class TestCsvStocksProvider:
    method test_is_data_provider (line 53) | def test_is_data_provider(self, stocks_provider):
    method test_has_schema (line 57) | def test_has_schema(self, stocks_provider):
    method test_data_is_dataframe (line 60) | def test_data_is_dataframe(self, stocks_provider):
    method test_start_end_dates (line 63) | def test_start_end_dates(self, stocks_provider):
    method test_len (line 67) | def test_len(self, stocks_provider):
  class TestSchemaReExport (line 71) | class TestSchemaReExport:
    method test_schema_import (line 72) | def test_schema_import(self):
    method test_options_schema (line 78) | def test_options_schema(self):

FILE: tests/data/test_providers_extended.py
  function stocks_csv (line 15) | def stocks_csv(tmp_path):
  function options_csv (line 28) | def options_csv(tmp_path):
  class TestTiingoData (line 43) | class TestTiingoData:
    method test_len (line 44) | def test_len(self, stocks_csv):
    method test_getitem_schema_key (line 48) | def test_getitem_schema_key(self, stocks_csv):
    method test_setitem (line 54) | def test_setitem(self, stocks_csv):
    method test_repr (line 60) | def test_repr(self, stocks_csv):
    method test_start_end_dates (line 65) | def test_start_end_dates(self, stocks_csv):
    method test_iter_dates (line 70) | def test_iter_dates(self, stocks_csv):
    method test_apply_filter (line 75) | def test_apply_filter(self, stocks_csv):
    method test_getattr_passthrough_method (line 81) | def test_getattr_passthrough_method(self, stocks_csv):
    method test_getattr_passthrough_property (line 88) | def test_getattr_passthrough_property(self, stocks_csv):
    method test_iter_months (line 93) | def test_iter_months(self, stocks_csv):
    method test_sma (line 99) | def test_sma(self, stocks_csv):
  class TestHistoricalOptionsData (line 106) | class TestHistoricalOptionsData:
    method test_len (line 107) | def test_len(self, options_csv):
    method test_dte_column_added (line 111) | def test_dte_column_added(self, options_csv):
    method test_getitem_schema_key (line 116) | def test_getitem_schema_key(self, options_csv):
    method test_getitem_series_indexing (line 121) | def test_getitem_series_indexing(self, options_csv):
    method test_setitem (line 127) | def test_setitem(self, options_csv):
    method test_repr (line 132) | def test_repr(self, options_csv):
    method test_iter_dates (line 137) | def test_iter_dates(self, options_csv):
    method test_iter_months (line 142) | def test_iter_months(self, options_csv):
    method test_getattr_passthrough (line 147) | def test_getattr_passthrough(self, options_csv):
    method test_apply_filter (line 153) | def test_apply_filter(self, options_csv):
    method test_start_end_dates (line 159) | def test_start_end_dates(self, options_csv):
  class TestCsvStocksProvider (line 165) | class TestCsvStocksProvider:
    method test_data_property (line 166) | def test_data_property(self, stocks_csv):
    method test_underscore_data (line 171) | def test_underscore_data(self, stocks_csv):
    method test_schema (line 175) | def test_schema(self, stocks_csv):
    method test_setitem_getitem (line 179) | def test_setitem_getitem(self, stocks_csv):
    method test_len (line 184) | def test_len(self, stocks_csv):
    method test_iter_dates (line 188) | def test_iter_dates(self, stocks_csv):
    method test_iter_months (line 193) | def test_iter_months(self, stocks_csv):
    method test_apply_filter (line 198) | def test_apply_filter(self, stocks_csv):
    method test_start_end_date (line 204) | def test_start_end_date(self, stocks_csv):
    method test_sma (line 209) | def test_sma(self, stocks_csv):
  class TestCsvOptionsProvider (line 215) | class TestCsvOptionsProvider:
    method test_data_property (line 216) | def test_data_property(self, options_csv):
    method test_underscore_data (line 221) | def test_underscore_data(self, options_csv):
    method test_setitem_getitem (line 225) | def test_setitem_getitem(self, options_csv):
    method test_len (line 231) | def test_len(self, options_csv):
    method test_iter_dates (line 235) | def test_iter_dates(self, options_csv):
    method test_iter_months (line 240) | def test_iter_months(self, options_csv):
    method test_apply_filter (line 245) | def test_apply_filter(self, options_csv):
    method test_start_end_date (line 251) | def test_start_end_date(self, options_csv):
    method test_schema (line 256) | def test_schema(self, options_csv):

FILE: tests/data/test_schema.py
  class TestSchema (line 9) | class TestSchema:
    method test_stocks_factory (line 10) | def test_stocks_factory(self):
    method test_options_factory (line 16) | def test_options_factory(self):
    method test_getitem (line 23) | def test_getitem(self):
    method test_getattr_returns_field (line 27) | def test_getattr_returns_field(self):
    method test_update (line 33) | def test_update(self):
    method test_contains (line 38) | def test_contains(self):
    method test_setitem (line 43) | def test_setitem(self):
    method test_iter (line 48) | def test_iter(self):
    method test_repr (line 53) | def test_repr(self):
    method test_equality (line 58) | def test_equality(self):
    method test_inequality_different_schema (line 63) | def test_inequality_different_schema(self):
    method test_equality_with_non_schema (line 68) | def test_equality_with_non_schema(self):
  class TestField (line 73) | class TestField:
    method test_repr (line 74) | def test_repr(self):
    method test_comparison_operators (line 79) | def test_comparison_operators(self):
    method test_equality_operator_string (line 85) | def test_equality_operator_string(self):
    method test_arithmetic_field_field (line 91) | def test_arithmetic_field_field(self):
    method test_arithmetic_field_scalar (line 97) | def test_arithmetic_field_scalar(self):
    method test_radd (line 103) | def test_radd(self):
    method test_rsub (line 108) | def test_rsub(self):
    method test_rtruediv (line 113) | def test_rtruediv(self):
    method test_rmul (line 118) | def test_rmul(self):
    method test_ne_operator (line 123) | def test_ne_operator(self):
  class TestFilter (line 130) | class TestFilter:
    method test_and (line 131) | def test_and(self):
    method test_or (line 137) | def test_or(self):
    method test_invert (line 143) | def test_invert(self):
    method test_call_on_dataframe (line 149) | def test_call_on_dataframe(self):
    method test_repr (line 157) | def test_repr(self):

FILE: tests/engine/test_algo_adapters.py
  function _run_with_algos (line 26) | def _run_with_algos(algos):
  function _dummy_ctx (line 44) | def _dummy_ctx(**overrides) -> EnginePipelineContext:
  function test_engine_algo_monthly_gate_translates (line 62) | def test_engine_algo_monthly_gate_translates():
  function test_engine_run_monthly_reset (line 82) | def test_engine_run_monthly_reset():
  function test_budget_percent_zero_blocks_option_entries (line 96) | def test_budget_percent_zero_blocks_option_entries():
  function test_budget_percent_sets_allocation (line 102) | def test_budget_percent_sets_allocation():
  function test_budget_percent_clamps_negative_capital (line 109) | def test_budget_percent_clamps_negative_capital():
  function test_range_filter_appends_entry_filter (line 120) | def test_range_filter_appends_entry_filter():
  function test_range_filter_missing_column_passes_all (line 132) | def test_range_filter_missing_column_passes_all():
  function test_select_by_delta_returns_range_filter (line 145) | def test_select_by_delta_returns_range_filter():
  function test_select_by_dte_returns_range_filter (line 151) | def test_select_by_dte_returns_range_filter():
  function test_iv_rank_filter_returns_range_filter (line 159) | def test_iv_rank_filter_returns_range_filter():
  function test_select_by_dte_strict_filter_skips_candidates (line 165) | def test_select_by_dte_strict_filter_skips_candidates():
  function test_max_greek_exposure_delta_blocks (line 177) | def test_max_greek_exposure_delta_blocks():
  function test_max_greek_exposure_vega_blocks (line 185) | def test_max_greek_exposure_vega_blocks():
  function test_max_greek_exposure_within_limits_continues (line 193) | def test_max_greek_exposure_within_limits_continues():
  function test_max_greek_exposure_none_limits_pass (line 200) | def test_max_greek_exposure_none_limits_pass():
  function test_exit_on_threshold_sets_override (line 211) | def test_exit_on_threshold_sets_override():
  function test_exit_on_threshold_warns_on_all_inf (line 218) | def test_exit_on_threshold_warns_on_all_inf():
  function test_exit_on_threshold_no_warn_when_finite (line 226) | def test_exit_on_threshold_no_warn_when_finite():
  function test_events_dataframe_has_flattened_columns (line 237) | def test_events_dataframe_has_flattened_columns():
  function test_events_dataframe_contains_cash_from_rebalance_start (line 247) | def test_events_dataframe_contains_cash_from_rebalance_start():
  function test_events_dataframe_empty_when_no_events (line 256) | def test_events_dataframe_empty_when_no_events():

FILE: tests/engine/test_capital_conservation.py
  function _ivy_stocks (line 36) | def _ivy_stocks():
  function _stocks_data (line 43) | def _stocks_data():
  function _options_data (line 49) | def _options_data():
  function _buy_strategy (line 62) | def _buy_strategy(schema):
  function _assert_balance_components_sum (line 72) | def _assert_balance_components_sum(balance, rtol=1e-6):
  function _assert_no_capital_spike (line 90) | def _assert_no_capital_spike(balance, initial_capital, max_first_day_rat...
  class TestCapitalConservationAQR (line 103) | class TestCapitalConservationAQR:
    method setup (line 107) | def setup(self):
    method test_components_sum_to_total (line 123) | def test_components_sum_to_total(self):
    method test_no_first_day_spike (line 126) | def test_no_first_day_spike(self):
    method test_final_capital_plausible (line 129) | def test_final_capital_plausible(self):
  class TestCapitalConservationSpitznagel (line 138) | class TestCapitalConservationSpitznagel:
    method setup (line 142) | def setup(self):
    method test_components_sum_to_total (line 159) | def test_components_sum_to_total(self):
  class TestCapitalConservationNoTrades (line 163) | class TestCapitalConservationNoTrades:
    method setup (line 167) | def setup(self):
    method test_components_sum_to_total (line 190) | def test_components_sum_to_total(self):
    method test_options_capital_always_zero (line 193) | def test_options_capital_always_zero(self):
    method test_no_trades (line 196) | def test_no_trades(self):
  class TestCapitalConservationHighBudget (line 200) | class TestCapitalConservationHighBudget:
    method setup (line 204) | def setup(self):
    method test_components_sum_to_total (line 220) | def test_components_sum_to_total(self):
    method test_no_first_day_spike (line 223) | def test_no_first_day_spike(self):
  class TestSkipDayCashConservation (line 232) | class TestSkipDayCashConservation:
    method setup (line 244) | def setup(self):
    method test_components_sum_to_total (line 268) | def test_components_sum_to_total(self):
    method test_options_capital_always_zero (line 271) | def test_options_capital_always_zero(self):
    method test_cash_never_below_options_floor (line 276) | def test_cash_never_below_options_floor(self):
    method test_no_trades (line 295) | def test_no_trades(self):
    method test_total_stable_with_flat_prices (line 298) | def test_total_stable_with_flat_prices(self):
  class TestAQRDeploymentNeverExceedsTotal (line 311) | class TestAQRDeploymentNeverExceedsTotal:
    method engine (line 329) | def engine(self, request):
    method test_components_sum_to_total (line 346) | def test_components_sum_to_total(self, engine):
    method test_total_never_above_initial_on_flat_prices (line 349) | def test_total_never_above_initial_on_flat_prices(self, engine):
    method test_deployment_never_exceeds_total (line 363) | def test_deployment_never_exceeds_total(self, engine):
    method test_cash_never_negative (line 380) | def test_cash_never_negative(self, engine):
  class TestAQRRebalanceCycleAccounting (line 395) | class TestAQRRebalanceCycleAccounting:
    method setup (line 408) | def setup(self):
    method test_components_sum_to_total (line 424) | def test_components_sum_to_total(self):
    method test_total_change_equals_component_change (line 427) | def test_total_change_equals_component_change(self):
    method test_no_cash_leak_over_full_run (line 447) | def test_no_cash_leak_over_full_run(self):
  class TestAQRvsSpitznagelZeroBudget (line 466) | class TestAQRvsSpitznagelZeroBudget:
    method setup (line 483) | def setup(self):
    method test_both_conserve_capital (line 521) | def test_both_conserve_capital(self):
    method test_aqr_less_equity_than_spitznagel (line 525) | def test_aqr_less_equity_than_spitznagel(self):
    method test_aqr_return_leq_spitznagel (line 538) | def test_aqr_return_leq_spitznagel(self):
    method test_no_trades_in_either (line 548) | def test_no_trades_in_either(self):
    method test_aqr_has_cash_from_unspent_options (line 552) | def test_aqr_has_cash_from_unspent_options(self):
  class TestExternallyFundedNoLeakage (line 567) | class TestExternallyFundedNoLeakage:
    method engine (line 583) | def engine(self, request):
    method test_components_sum_to_total (line 601) | def test_components_sum_to_total(self, engine):
    method test_no_phantom_cash_growth (line 604) | def test_no_phantom_cash_growth(self, engine):
    method test_cash_never_negative (line 624) | def test_cash_never_negative(self, engine):
    method test_total_capital_bounded (line 631) | def test_total_capital_bounded(self, engine):

FILE: tests/engine/test_chaos.py
  function _ivy_stocks (line 39) | def _ivy_stocks():
  function _stocks_data (line 44) | def _stocks_data():
  function _options_data (line 50) | def _options_data():
  function _build_strategy (line 63) | def _build_strategy(schema, direction=Direction.BUY):
  function _run_chaos (line 72) | def _run_chaos(options_data, stocks_data=None, cost_model=None,
  function _assert_finite_or_error (line 96) | def _assert_finite_or_error(fn):
  class TestNaNInjection (line 111) | class TestNaNInjection:
    method test_nan_all_bids (line 114) | def test_nan_all_bids(self):
    method test_nan_all_asks (line 119) | def test_nan_all_asks(self):
    method test_nan_scattered_bid (line 124) | def test_nan_scattered_bid(self):
    method test_nan_scattered_ask (line 130) | def test_nan_scattered_ask(self):
    method test_nan_delta (line 136) | def test_nan_delta(self):
    method test_nan_volume (line 142) | def test_nan_volume(self):
  class TestNegativePrices (line 148) | class TestNegativePrices:
    method test_negative_bid (line 151) | def test_negative_bid(self):
    method test_negative_ask (line 156) | def test_negative_ask(self):
    method test_both_negative (line 161) | def test_both_negative(self):
  class TestInvertedBidAsk (line 168) | class TestInvertedBidAsk:
    method test_inverted_spread (line 171) | def test_inverted_spread(self):
    method test_bid_equals_ask (line 179) | def test_bid_equals_ask(self):
  class TestMissingColumns (line 185) | class TestMissingColumns:
    method test_missing_delta_column (line 188) | def test_missing_delta_column(self):
  class TestNoMatchingContracts (line 200) | class TestNoMatchingContracts:
    method test_all_dte_zero (line 203) | def test_all_dte_zero(self):
  class TestZeroVolume (line 212) | class TestZeroVolume:
    method test_zero_volume_fill (line 215) | def test_zero_volume_fill(self):
  class TestExtremeGreeks (line 225) | class TestExtremeGreeks:
    method test_extreme_delta_blocked (line 228) | def test_extreme_delta_blocked(self):
    method test_extreme_vega_blocked (line 239) | def test_extreme_vega_blocked(self):
    method test_extreme_delta_allowed (line 250) | def test_extreme_delta_allowed(self):
  class TestDuplicateDates (line 262) | class TestDuplicateDates:
    method test_duplicate_options_rows (line 265) | def test_duplicate_options_rows(self):
  class TestCapitalExhaustion (line 273) | class TestCapitalExhaustion:
    method test_tiny_capital (line 276) | def test_tiny_capital(self):
    method test_zero_capital (line 285) | def test_zero_capital(self):
  class TestMassiveSpread (line 295) | class TestMassiveSpread:
    method test_massive_spread (line 298) | def test_massive_spread(self):
    method test_massive_spread_mid_fill (line 304) | def test_massive_spread_mid_fill(self):
  class TestAllExpired (line 313) | class TestAllExpired:
    method test_all_expired_capital_preserved (line 316) | def test_all_expired_capital_preserved(self):
  class TestSingleDay (line 327) | class TestSingleDay:
    method test_single_date (line 330) | def test_single_date(self):

FILE: tests/engine/test_clock.py
  function _make_data (line 9) | def _make_data(n_dates=5):
  class TestDailyIteration (line 25) | class TestDailyIteration:
    method test_yields_correct_number_of_dates (line 26) | def test_yields_correct_number_of_dates(self):
    method test_yields_tuples_of_date_stocks_options (line 32) | def test_yields_tuples_of_date_stocks_options(self):
  class TestAllDates (line 41) | class TestAllDates:
    method test_returns_all_unique_dates (line 42) | def test_returns_all_unique_dates(self):
  class TestRebalanceDates (line 48) | class TestRebalanceDates:
    method test_zero_freq_returns_empty (line 49) | def test_zero_freq_returns_empty(self):
    method test_negative_freq_returns_empty (line 55) | def test_negative_freq_returns_empty(self):
    method test_positive_freq_returns_dates (line 61) | def test_positive_freq_returns_dates(self):
  class TestMonthlyIteration (line 80) | class TestMonthlyIteration:
    method test_monthly_mode_yields_first_of_month_dates (line 81) | def test_monthly_mode_yields_first_of_month_dates(self):

FILE: tests/engine/test_engine.py
  function _ivy_stocks (line 22) | def _ivy_stocks():
  function _stocks_data (line 27) | def _stocks_data():
  function _options_data (line 33) | def _options_data():
  function _buy_strategy (line 47) | def _buy_strategy(schema):
  function _run_engine (line 56) | def _run_engine(cost_model=None):
  class TestEngineRegressionValues (line 74) | class TestEngineRegressionValues:
    method setup (line 78) | def setup(self):
    method test_trade_log_not_empty (line 81) | def test_trade_log_not_empty(self):
    method test_balance_not_empty (line 84) | def test_balance_not_empty(self):
    method test_regression_costs (line 87) | def test_regression_costs(self):
    method test_regression_qtys (line 96) | def test_regression_qtys(self):
  class TestEngineWithCosts (line 106) | class TestEngineWithCosts:
    method test_commission_reduces_final_capital (line 109) | def test_commission_reduces_final_capital(self):
  class TestRunMetadata (line 118) | class TestRunMetadata:
    method test_metadata_attached_to_trade_log_and_balance (line 121) | def test_metadata_attached_to_trade_log_and_balance(self):
  class TestEngineInit (line 135) | class TestEngineInit:
    method test_default_allocation_normalized (line 138) | def test_default_allocation_normalized(self):
    method test_default_components (line 144) | def test_default_components(self):
    method test_stop_if_broke_flag (line 151) | def test_stop_if_broke_flag(self):

FILE: tests/engine/test_engine_deep.py
  function _ivy_stocks (line 58) | def _ivy_stocks():
  function _stocks_data (line 68) | def _stocks_data():
  function _options_data (line 74) | def _options_data():
  function _buy_strategy (line 87) | def _buy_strategy(schema):
  function _sell_strategy (line 96) | def _sell_strategy(schema):
  function _run_engine (line 105) | def _run_engine(**kwargs):
  class TestIntrinsicValue (line 135) | class TestIntrinsicValue:
    method test_call_itm (line 138) | def test_call_itm(self):
    method test_call_otm (line 141) | def test_call_otm(self):
    method test_put_itm (line 144) | def test_put_itm(self):
    method test_put_otm (line 147) | def test_put_otm(self):
    method test_atm_both (line 150) | def test_atm_both(self):
  class TestCapitalFlowInvariants (line 160) | class TestCapitalFlowInvariants:
    method test_total_capital_equals_sum_of_parts (line 163) | def test_total_capital_equals_sum_of_parts(self):
    method test_accumulated_return_consistent_with_pct_change (line 170) | def test_accumulated_return_consistent_with_pct_change(self):
    method test_initial_capital_preserved_in_first_row (line 177) | def test_initial_capital_preserved_in_first_row(self):
    method test_total_capital_never_negative_with_buy_only (line 181) | def test_total_capital_never_negative_with_buy_only(self):
    method test_stock_qty_columns_present_for_all_stocks (line 185) | def test_stock_qty_columns_present_for_all_stocks(self):
  class TestOptionsBudget (line 197) | class TestOptionsBudget:
    method test_budget_pct (line 200) | def test_budget_pct(self):
    method test_budget_preserves_raw_allocation (line 205) | def test_budget_preserves_raw_allocation(self):
    method test_budget_changes_trade_sizes_vs_no_budget (line 221) | def test_budget_changes_trade_sizes_vs_no_budget(self):
  class TestMonthlyMode (line 237) | class TestMonthlyMode:
    method test_monthly_mode_runs (line 240) | def test_monthly_mode_runs(self):
    method test_monthly_produces_fewer_balance_rows (line 244) | def test_monthly_produces_fewer_balance_rows(self):
  class TestCheckExitsDaily (line 255) | class TestCheckExitsDaily:
    method test_daily_exits_runs_without_error (line 258) | def test_daily_exits_runs_without_error(self):
    method test_daily_exits_may_close_positions_earlier (line 262) | def test_daily_exits_may_close_positions_earlier(self):
  class TestStopIfBroke (line 276) | class TestStopIfBroke:
    method test_completes_without_stopping (line 279) | def test_completes_without_stopping(self):
  class TestSMAGating (line 290) | class TestSMAGating:
    method test_sma_gating_runs (line 293) | def test_sma_gating_runs(self):
    method test_sma_gating_changes_stock_allocation (line 297) | def test_sma_gating_changes_stock_allocation(self):
  class TestEventLog (line 318) | class TestEventLog:
    method test_events_dataframe_returns_dataframe (line 325) | def test_events_dataframe_returns_dataframe(self):
    method test_event_log_has_required_columns (line 330) | def test_event_log_has_required_columns(self):
  class TestAllocationNormalization (line 343) | class TestAllocationNormalization:
    method test_unnormalized_sums_to_one (line 346) | def test_unnormalized_sums_to_one(self):
    method test_already_normalized (line 351) | def test_already_normalized(self):
    method test_missing_keys_default_to_zero (line 355) | def test_missing_keys_default_to_zero(self):
    method test_raw_allocation_preserved (line 360) | def test_raw_allocation_preserved(self):
  class TestMultiStrategy (line 371) | class TestMultiStrategy:
    method _make_multi_engine (line 374) | def _make_multi_engine(self):
    method test_two_strategies_equal_weight (line 386) | def test_two_strategies_equal_weight(self):
    method test_multi_strategy_weights_must_sum_to_one (line 394) | def test_multi_strategy_weights_must_sum_to_one(self):
    method test_multi_strategy_different_frequencies (line 401) | def test_multi_strategy_different_frequencies(self):
    method test_multi_strategy_with_daily_exit_checks (line 408) | def test_multi_strategy_with_daily_exit_checks(self):
    method test_multi_strategy_capital_identity (line 420) | def test_multi_strategy_capital_identity(self):
  class TestRiskManagementIntegration (line 436) | class TestRiskManagementIntegration:
    method test_max_delta_blocks_large_positions (line 439) | def test_max_delta_blocks_large_positions(self):
    method test_max_vega_blocks_entries (line 446) | def test_max_vega_blocks_entries(self):
    method test_max_drawdown_blocks_during_dd (line 451) | def test_max_drawdown_blocks_during_dd(self):
    method test_no_constraints_allows_all (line 456) | def test_no_constraints_allows_all(self):
    method test_compound_constraints (line 461) | def test_compound_constraints(self):
    method test_risk_events_logged_on_block (line 467) | def test_risk_events_logged_on_block(self):
  class TestExecutionCombinations (line 482) | class TestExecutionCombinations:
    method test_midprice_fill (line 485) | def test_midprice_fill(self):
    method test_volume_aware_fill (line 489) | def test_volume_aware_fill(self):
    method test_per_contract_commission (line 493) | def test_per_contract_commission(self):
    method test_tiered_commission (line 497) | def test_tiered_commission(self):
    method test_max_open_interest_selector (line 501) | def test_max_open_interest_selector(self):
    method test_commission_reduces_capital_consistently (line 505) | def test_commission_reduces_capital_consistently(self):
  class TestMaxNotionalPct (line 521) | class TestMaxNotionalPct:
    method test_max_notional_limits_sell_positions (line 524) | def test_max_notional_limits_sell_positions(self):
  class TestSellDirectionStrategy (line 545) | class TestSellDirectionStrategy:
    method test_sell_puts_run (line 548) | def test_sell_puts_run(self):
    method test_sell_entry_costs_are_negative (line 561) | def test_sell_entry_costs_are_negative(self):
  class TestExitThresholds (line 584) | class TestExitThresholds:
    method test_very_tight_profit_threshold (line 587) | def test_very_tight_profit_threshold(self):
    method test_very_tight_loss_threshold (line 603) | def test_very_tight_loss_threshold(self):
    method test_both_thresholds_at_zero_forces_immediate_exit (line 619) | def test_both_thresholds_at_zero_forces_immediate_exit(self):
  class TestRunMetadataDeep (line 642) | class TestRunMetadataDeep:
    method test_metadata_config_hash_deterministic (line 645) | def test_metadata_config_hash_deterministic(self):
    method test_metadata_data_snapshot_hash_deterministic (line 651) | def test_metadata_data_snapshot_hash_deterministic(self):
    method test_metadata_has_data_snapshot (line 656) | def test_metadata_has_data_snapshot(self):
    method test_metadata_has_framework_key (line 663) | def test_metadata_has_framework_key(self):
    method test_multi_strategy_has_metadata (line 667) | def test_multi_strategy_has_metadata(self):
  class TestRebalanceFrequency (line 689) | class TestRebalanceFrequency:
    method test_rebalance_freq_zero_means_no_rebalance (line 692) | def test_rebalance_freq_zero_means_no_rebalance(self):
    method test_high_rebalance_freq (line 707) | def test_high_rebalance_freq(self):
    method test_rebalance_freq_1_vs_2_differ (line 711) | def test_rebalance_freq_1_vs_2_differ(self):
  class TestPerLegOverrides (line 728) | class TestPerLegOverrides:
    method test_per_leg_signal_selector (line 731) | def test_per_leg_signal_selector(self):
    method test_per_leg_fill_model (line 754) | def test_per_leg_fill_model(self):
    method test_midprice_fill_produces_different_costs (line 778) | def test_midprice_fill_produces_different_costs(self):
  class TestEngineEdgeCases (line 794) | class TestEngineEdgeCases:
    method test_all_cash_allocation (line 797) | def test_all_cash_allocation(self):
    method test_tiny_initial_capital (line 810) | def test_tiny_initial_capital(self):
    method test_large_initial_capital (line 824) | def test_large_initial_capital(self):

FILE: tests/engine/test_engine_unit.py
  class TestBacktestEngineRepr (line 8) | class TestBacktestEngineRepr:
    method test_repr_basic (line 9) | def test_repr_basic(self):
    method test_repr_with_custom_cost_model (line 19) | def test_repr_with_custom_cost_model(self):
  class TestSha256Json (line 29) | class TestSha256Json:
    method test_deterministic (line 30) | def test_deterministic(self):
    method test_different_inputs_different_hashes (line 37) | def test_different_inputs_different_hashes(self):
    method test_key_order_independent (line 42) | def test_key_order_independent(self):
  class TestGitSha (line 48) | class TestGitSha:
    method test_returns_string (line 49) | def test_returns_string(self):
  class TestFlatTradeLogToMultiIndex (line 56) | class TestFlatTradeLogToMultiIndex:
    method test_empty_dataframe (line 57) | def test_empty_dataframe(self):
    method test_converts_double_underscore_columns (line 65) | def test_converts_double_underscore_columns(self):
  class TestEventsDataframe (line 81) | class TestEventsDataframe:
    method test_empty_events (line 82) | def test_empty_events(self):
  class TestAllocationNormalization (line 91) | class TestAllocationNormalization:
    method test_normalizes_to_sum_one (line 92) | def test_normalizes_to_sum_one(self):
    method test_missing_keys_default_to_zero (line 99) | def test_missing_keys_default_to_zero(self):

FILE: tests/engine/test_full_liquidation.py
  function _ivy_stocks (line 30) | def _ivy_stocks():
  function _stocks_data (line 35) | def _stocks_data():
  function _options_data (line 41) | def _options_data():
  function _build_strategy (line 54) | def _build_strategy(schema, direction=Direction.BUY):
  function _run (line 63) | def _run(cost_model=None, direction=Direction.BUY, signal_selector=None):
  class TestTradePattern (line 86) | class TestTradePattern:
    method setup (line 90) | def setup(self):
    method test_trades_are_entries (line 93) | def test_trades_are_entries(self):
    method test_exit_filter_produces_exits (line 99) | def test_exit_filter_produces_exits(self):
    method test_first_trade_is_entry (line 105) | def test_first_trade_is_entry(self):
  class TestCashAccounting (line 116) | class TestCashAccounting:
    method setup (line 120) | def setup(self):
    method test_max_drawdown_under_100_pct (line 123) | def test_max_drawdown_under_100_pct(self):
    method test_total_capital_always_positive (line 130) | def test_total_capital_always_positive(self):
    method test_total_capital_equals_sum_of_parts (line 135) | def test_total_capital_equals_sum_of_parts(self):
    method test_initial_capital_preserved (line 143) | def test_initial_capital_preserved(self):
    method test_no_capital_inflation (line 148) | def test_no_capital_inflation(self):
  class TestDirectionVariants (line 162) | class TestDirectionVariants:
    method test_buy_put_cash_stays_positive (line 163) | def test_buy_put_cash_stays_positive(self):
    method test_sell_put_has_credit_entries (line 167) | def test_sell_put_has_credit_entries(self):
    method test_sell_put_max_dd_under_100 (line 176) | def test_sell_put_max_dd_under_100(self):
  class TestCommissionImpact (line 187) | class TestCommissionImpact:
    method test_commission_reduces_capital (line 188) | def test_commission_reduces_capital(self):
    method test_high_commission_still_positive (line 196) | def test_high_commission_still_positive(self):

FILE: tests/engine/test_max_notional.py
  function _ivy_stocks (line 18) | def _ivy_stocks():
  function _stocks_data (line 23) | def _stocks_data():
  function _options_data (line 29) | def _options_data():
  function _buy_strategy (line 42) | def _buy_strategy(schema):
  function _sell_strategy (line 52) | def _sell_strategy(schema):
  function _straddle_strategy (line 62) | def _straddle_strategy(schema):
  function _run_engine (line 75) | def _run_engine(max_notional_pct=None, strategy_fn=None):
  class TestMaxNotionalPct (line 94) | class TestMaxNotionalPct:
    method test_none_is_backward_compatible (line 97) | def test_none_is_backward_compatible(self):
    method test_long_only_unaffected (line 103) | def test_long_only_unaffected(self):
    method test_sell_strategy_capped (line 109) | def test_sell_strategy_capped(self):
    method test_zero_cap_blocks_all_short_trades (line 119) | def test_zero_cap_blocks_all_short_trades(self):
    method test_generous_cap_allows_trades (line 124) | def test_generous_cap_allows_trades(self):
    method test_straddle_both_legs_contribute_notional (line 131) | def test_straddle_both_legs_contribute_notional(self):
    method test_cap_monotonic (line 139) | def test_cap_monotonic(self):

FILE: tests/engine/test_multi_strategy.py
  function data_dir (line 21) | def data_dir():
  function _make_engine (line 25) | def _make_engine(data_dir):
  class TestStrategyAllocation (line 56) | class TestStrategyAllocation:
    method test_fields (line 57) | def test_fields(self):
  class TestMultiStrategyEngine (line 65) | class TestMultiStrategyEngine:
    method test_weight_normalization (line 66) | def test_weight_normalization(self):
    method test_equal_weights (line 79) | def test_equal_weights(self):
    method test_run_with_mocked_engines (line 89) | def test_run_with_mocked_engines(self):
    method test_run_engine_without_balance (line 124) | def test_run_engine_without_balance(self):
    method test_run_with_data (line 141) | def test_run_with_data(self, data_dir):

FILE: tests/engine/test_multi_strategy_engine.py
  function _ivy_stocks (line 28) | def _ivy_stocks():
  function _stocks_data (line 35) | def _stocks_data():
  function _options_data (line 41) | def _options_data():
  function _buy_put_strategy (line 54) | def _buy_put_strategy(schema):
  function _sell_call_strategy (line 64) | def _sell_call_strategy(schema):
  function _make_engine (line 74) | def _make_engine(**kwargs):
  class TestStrategySlot (line 90) | class TestStrategySlot:
    method test_dataclass_fields (line 91) | def test_dataclass_fields(self):
  class TestAddStrategy (line 111) | class TestAddStrategy:
    method test_adds_slot (line 112) | def test_adds_slot(self):
    method test_auto_names (line 120) | def test_auto_names(self):
    method test_custom_names (line 128) | def test_custom_names(self):
    method test_not_multi_strategy_by_default (line 140) | def test_not_multi_strategy_by_default(self):
  class TestValidation (line 149) | class TestValidation:
    method test_weights_must_sum_to_one (line 150) | def test_weights_must_sum_to_one(self):
  class TestSameFrequency (line 163) | class TestSameFrequency:
    method setup (line 165) | def setup(self):
    method test_balance_not_empty (line 176) | def test_balance_not_empty(self):
    method test_balance_has_required_columns (line 179) | def test_balance_has_required_columns(self):
    method test_has_run_metadata (line 186) | def test_has_run_metadata(self):
    method test_trade_log_type (line 189) | def test_trade_log_type(self):
  class TestDifferentFrequency (line 198) | class TestDifferentFrequency:
    method setup (line 200) | def setup(self):
    method test_balance_not_empty (line 213) | def test_balance_not_empty(self):
    method test_total_capital_computed (line 216) | def test_total_capital_computed(self):
  class TestBackwardCompat (line 226) | class TestBackwardCompat:
    method test_single_strategy_api_unchanged (line 227) | def test_single_strategy_api_unchanged(self):
  class TestSharedCash (line 240) | class TestSharedCash:
    method test_cash_flows_into_shared_pool (line 241) | def test_cash_flows_into_shared_pool(self):
  class TestPerStrategyExitThresholds (line 259) | class TestPerStrategyExitThresholds:
    method test_different_exit_thresholds (line 260) | def test_different_exit_thresholds(self):
  class TestPerStrategyDailyExits (line 281) | class TestPerStrategyDailyExits:
    method test_daily_exits_per_slot (line 282) | def test_daily_exits_per_slot(self):
    method test_global_check_exits_daily (line 296) | def test_global_check_exits_daily(self):
  class TestStopIfBroke (line 314) | class TestStopIfBroke:
    method test_stop_halts_multi_strategy (line 315) | def test_stop_halts_multi_strategy(self):
  class TestRustGate (line 333) | class TestRustGate:
    method test_multi_strategy_produces_metadata (line 334) | def test_multi_strategy_produces_metadata(self):
  class TestSingleSlotEquivalence (line 348) | class TestSingleSlotEquivalence:
    method test_single_slot_produces_balance (line 349) | def test_single_slot_produces_balance(self):
  class TestOptionsBudget (line 365) | class TestOptionsBudget:
    method test_options_budget_pct (line 366) | def test_options_budget_pct(self):

FILE: tests/engine/test_per_leg_overrides.py
  function _ivy_stocks (line 25) | def _ivy_stocks():
  function _stocks_data (line 30) | def _stocks_data():
  function _options_data (line 36) | def _options_data():
  class TestPerLegSignalSelector (line 49) | class TestPerLegSignalSelector:
    method test_leg_selector_overrides_engine (line 56) | def test_leg_selector_overrides_engine(self):
    method test_engine_selector_used_when_leg_has_none (line 88) | def test_engine_selector_used_when_leg_has_none(self):
  class TestPerLegFillModel (line 114) | class TestPerLegFillModel:
    method test_midprice_differs_from_market (line 117) | def test_midprice_differs_from_market(self):

FILE: tests/engine/test_pipeline.py
  function _prices (line 59) | def _prices() -> pd.DataFrame:
  function test_pipeline_rebalances_on_month_start_only (line 68) | def test_pipeline_rebalances_on_month_start_only():
  function test_run_monthly_reset_allows_rerun (line 86) | def test_run_monthly_reset_allows_rerun():
  function test_drawdown_guard_blocks_rebalance (line 99) | def test_drawdown_guard_blocks_rebalance():
  function test_drawdown_guard_reset (line 124) | def test_drawdown_guard_reset():
  class _StopAlgo (line 143) | class _StopAlgo:
    method __call__ (line 144) | def __call__(self, ctx: PipelineContext) -> StepDecision:
  function test_stop_algo_halts_pipeline_early (line 150) | def test_stop_algo_halts_pipeline_early():
  function test_select_these_filters_missing_symbols (line 169) | def test_select_these_filters_missing_symbols():
  function test_select_these_case_insensitive (line 184) | def test_select_these_case_insensitive():
  function test_weigh_specified_normalizes (line 193) | def test_weigh_specified_normalizes():
  function test_weigh_specified_skips_on_empty_selected (line 208) | def test_weigh_specified_skips_on_empty_selected():
  function test_rebalance_computes_floor_qty (line 226) | def test_rebalance_computes_floor_qty():
  function test_rebalance_skips_zero_price (line 241) | def test_rebalance_skips_zero_price():
  function test_balance_has_expected_columns (line 259) | def test_balance_has_expected_columns():
  function test_logs_dataframe_schema (line 270) | def test_logs_dataframe_schema():
  function test_empty_run_returns_empty_balance (line 282) | def test_empty_run_returns_empty_balance():
  function test_multi_symbol_rebalance (line 293) | def test_multi_symbol_rebalance():
  function _daily_prices (line 314) | def _daily_prices(symbols=("SPY", "TLT"), days=60, seed=42) -> pd.DataFr...
  function _weekly_prices (line 326) | def _weekly_prices() -> pd.DataFrame:
  function _ctx (line 335) | def _ctx(prices=None, total_capital=1000.0, cash=1000.0, positions=None,
  function test_run_weekly_triggers_once_per_week (line 364) | def test_run_weekly_triggers_once_per_week():
  function test_run_weekly_reset (line 378) | def test_run_weekly_reset():
  function test_run_weekly_skips_same_week (line 387) | def test_run_weekly_skips_same_week():
  function test_run_quarterly_triggers_once_per_quarter (line 399) | def test_run_quarterly_triggers_once_per_quarter():
  function test_run_quarterly_skips_same_quarter (line 417) | def test_run_quarterly_skips_same_quarter():
  function test_run_quarterly_reset (line 425) | def test_run_quarterly_reset():
  function test_run_yearly_triggers_once_per_year (line 436) | def test_run_yearly_triggers_once_per_year():
  function test_run_yearly_skips_same_year (line 450) | def test_run_yearly_skips_same_year():
  function test_run_yearly_reset (line 458) | def test_run_yearly_reset():
  function test_run_daily_always_continues (line 469) | def test_run_daily_always_continues():
  function test_run_daily_full_pipeline (line 475) | def test_run_daily_full_pipeline():
  function test_run_once_only_first_date (line 491) | def test_run_once_only_first_date():
  function test_run_once_reset (line 501) | def test_run_once_reset():
  function test_run_once_full_pipeline (line 511) | def test_run_once_full_pipeline():
  function test_run_on_date_specific_dates (line 527) | def test_run_on_date_specific_dates():
  function test_run_on_date_accepts_timestamps (line 537) | def test_run_on_date_accepts_timestamps():
  function test_run_on_date_full_pipeline (line 543) | def test_run_on_date_full_pipeline():
  function test_run_after_date_skips_before (line 562) | def test_run_after_date_skips_before():
  function test_run_after_date_full_pipeline (line 572) | def test_run_after_date_full_pipeline():
  function test_run_every_n_periods (line 592) | def test_run_every_n_periods():
  function test_run_every_n_periods_reset (line 606) | def test_run_every_n_periods_reset():
  function test_or_passes_if_any_child_passes (line 620) | def test_or_passes_if_any_child_passes():
  function test_or_skips_if_all_children_skip (line 627) | def test_or_skips_if_all_children_skip():
  function test_or_passes_when_one_passes (line 640) | def test_or_passes_when_one_passes():
  function test_or_reset (line 650) | def test_or_reset():
  function test_not_inverts_skip_to_continue (line 664) | def test_not_inverts_skip_to_continue():
  function test_not_inverts_continue_to_skip (line 672) | def test_not_inverts_continue_to_skip():
  function test_not_reset (line 681) | def test_not_reset():
  function test_select_all_picks_valid_prices (line 698) | def test_select_all_picks_valid_prices():
  function test_select_all_skips_zero_price (line 705) | def test_select_all_skips_zero_price():
  function test_select_all_skips_all_nan (line 711) | def test_select_all_skips_all_nan():
  function test_select_has_data_filters_by_history_length (line 721) | def test_select_has_data_filters_by_history_length():
  function test_select_has_data_removes_short_history (line 735) | def test_select_has_data_removes_short_history():
  function test_select_has_data_no_history (line 748) | def test_select_has_data_no_history():
  function test_select_has_data_uses_all_symbols_if_none_selected (line 755) | def test_select_has_data_uses_all_symbols_if_none_selected():
  function test_select_momentum_picks_top_n (line 773) | def test_select_momentum_picks_top_n():
  function test_select_momentum_ascending (line 793) | def test_select_momentum_ascending():
  function test_select_momentum_no_history (line 810) | def test_select_momentum_no_history():
  function test_select_n_truncates (line 820) | def test_select_n_truncates():
  function test_select_n_empty (line 827) | def test_select_n_empty():
  function test_select_n_fewer_than_n (line 833) | def test_select_n_fewer_than_n():
  function test_select_where_custom_filter (line 844) | def test_select_where_custom_filter():
  function test_select_where_all_filtered (line 856) | def test_select_where_all_filtered():
  function test_select_where_falls_back_to_prices_index (line 866) | def test_select_where_falls_back_to_prices_index():
  function test_weigh_equally_two_symbols (line 886) | def test_weigh_equally_two_symbols():
  function test_weigh_equally_single_symbol (line 894) | def test_weigh_equally_single_symbol():
  function test_weigh_equally_empty (line 900) | def test_weigh_equally_empty():
  function test_weigh_equally_three_symbols (line 906) | def test_weigh_equally_three_symbols():
  function test_weigh_inv_vol_basic (line 917) | def test_weigh_inv_vol_basic():
  function test_weigh_inv_vol_lower_vol_gets_higher_weight (line 931) | def test_weigh_inv_vol_lower_vol_gets_higher_weight():
  function test_weigh_inv_vol_no_history (line 949) | def test_weigh_inv_vol_no_history():
  function test_weigh_inv_vol_no_selected (line 955) | def test_weigh_inv_vol_no_selected():
  function test_weigh_mean_var_basic (line 965) | def test_weigh_mean_var_basic():
  function test_weigh_mean_var_single_asset (line 979) | def test_weigh_mean_var_single_asset():
  function test_weigh_mean_var_no_history (line 992) | def test_weigh_mean_var_no_history():
  function test_weigh_mean_var_insufficient_data (line 998) | def test_weigh_mean_var_insufficient_data():
  function test_weigh_erc_basic (line 1015) | def test_weigh_erc_basic():
  function test_weigh_erc_no_history (line 1029) | def test_weigh_erc_no_history():
  function test_weigh_erc_single_asset (line 1035) | def test_weigh_erc_single_asset():
  function test_weigh_erc_weights_sum_to_one (line 1048) | def test_weigh_erc_weights_sum_to_one():
  function test_target_vol_scales_weights (line 1064) | def test_target_vol_scales_weights():
  function test_target_vol_no_weights (line 1080) | def test_target_vol_no_weights():
  function test_target_vol_no_history (line 1086) | def test_target_vol_no_history():
  function test_target_vol_never_levers (line 1092) | def test_target_vol_never_levers():
  function test_limit_weights_caps (line 1115) | def test_limit_weights_caps():
  function test_limit_weights_no_change_under_limit (line 1123) | def test_limit_weights_no_change_under_limit():
  function test_limit_weights_empty (line 1130) | def test_limit_weights_empty():
  function test_limit_weights_redistributes (line 1136) | def test_limit_weights_redistributes():
  function test_capital_flow_dict (line 1150) | def test_capital_flow_dict():
  function test_capital_flow_no_flow_date (line 1158) | def test_capital_flow_no_flow_date():
  function test_capital_flow_withdrawal (line 1165) | def test_capital_flow_withdrawal():
  function test_capital_flow_callable (line 1173) | def test_capital_flow_callable():
  function test_capital_flow_in_pipeline (line 1188) | def test_capital_flow_in_pipeline():
  function test_rebalance_over_time_gradual (line 1211) | def test_rebalance_over_time_gradual():
  function test_rebalance_over_time_reset (line 1230) | def test_rebalance_over_time_reset():
  function test_rebalance_over_time_no_target (line 1239) | def test_rebalance_over_time_no_target():
  function test_pipeline_select_all_weigh_equally (line 1251) | def test_pipeline_select_all_weigh_equally():
  function test_pipeline_momentum_selection (line 1264) | def test_pipeline_momentum_selection():
  function test_pipeline_limit_weights_integration (line 1290) | def test_pipeline_limit_weights_integration():
  function test_pipeline_run_on_date_with_capital_flow (line 1309) | def test_pipeline_run_on_date_with_capital_flow():
  function test_pipeline_inv_vol_with_limit_weights (line 1329) | def test_pipeline_inv_vol_with_limit_weights():
  function test_run_after_days_skips_warmup (line 1363) | def test_run_after_days_skips_warmup():
  function test_run_after_days_reset (line 1372) | def test_run_after_days_reset():
  function test_run_after_days_in_pipeline (line 1382) | def test_run_after_days_in_pipeline():
  function test_run_if_out_of_bounds_skips_when_in_bounds (line 1400) | def test_run_if_out_of_bounds_skips_when_in_bounds():
  function test_run_if_out_of_bounds_triggers_when_drifted (line 1413) | def test_run_if_out_of_bounds_triggers_when_drifted():
  function test_run_if_out_of_bounds_no_prior_target (line 1426) | def test_run_if_out_of_bounds_no_prior_target():
  function test_run_if_out_of_bounds_reset (line 1432) | def test_run_if_out_of_bounds_reset():
  function test_limit_deltas_clips_large_change (line 1443) | def test_limit_deltas_clips_large_change():
  function test_limit_deltas_no_change_needed (line 1455) | def test_limit_deltas_no_change_needed():
  function test_limit_deltas_empty (line 1467) | def test_limit_deltas_empty():
  function test_scale_weights_half (line 1477) | def test_scale_weights_half():
  function test_scale_weights_double (line 1484) | def test_scale_weights_double():
  function test_scale_weights_empty (line 1491) | def test_scale_weights_empty():
  function test_select_randomly_picks_n (line 1501) | def test_select_randomly_picks_n():
  function test_select_randomly_deterministic (line 1513) | def test_select_randomly_deterministic():
  function test_select_randomly_n_exceeds_candidates (line 1529) | def test_select_randomly_n_exceeds_candidates():
  function test_select_randomly_no_candidates (line 1540) | def test_select_randomly_no_candidates():
  function test_select_active_filters_dead (line 1554) | def test_select_active_filters_dead():
  function test_select_active_all_dead (line 1564) | def test_select_active_all_dead():
  function test_weigh_randomly_sums_to_one (line 1577) | def test_weigh_randomly_sums_to_one():
  function test_weigh_randomly_deterministic (line 1584) | def test_weigh_randomly_deterministic():
  function test_weigh_randomly_empty (line 1592) | def test_weigh_randomly_empty():
  function test_weigh_target_basic (line 1602) | def test_weigh_target_basic():
  function test_weigh_target_uses_latest_row (line 1619) | def test_weigh_target_uses_latest_row():
  function test_weigh_target_no_data_before_date (line 1633) | def test_weigh_target_no_data_before_date():
  function test_weigh_target_empty_selected (line 1647) | def test_weigh_target_empty_selected():
  function test_close_dead_removes_zero_price (line 1661) | def test_close_dead_removes_zero_price():
  function test_close_dead_removes_nan_price (line 1671) | def test_close_dead_removes_nan_price():
  function test_close_dead_no_dead (line 1680) | def test_close_dead_no_dead():
  function test_close_dead_missing_price (line 1690) | def test_close_dead_missing_price():
  function test_close_positions_after_dates (line 1703) | def test_close_positions_after_dates():
  function test_close_positions_before_date (line 1715) | def test_close_positions_before_date():
  function test_close_positions_on_exact_date (line 1725) | def test_close_positions_on_exact_date():
  function test_require_passes_when_inner_passes (line 1739) | def test_require_passes_when_inner_passes():
  function test_require_blocks_when_inner_skips (line 1746) | def test_require_blocks_when_inner_skips():
  function test_require_reset (line 1754) | def test_require_reset():
  function test_benchmark_random_basic (line 1766) | def test_benchmark_random_basic():
  function test_benchmark_random_deterministic (line 1787) | def test_benchmark_random_deterministic():
  function test_benchmark_random_result_properties (line 1796) | def test_benchmark_random_result_properties():
  function test_pipeline_or_run_if_out_of_bounds (line 1811) | def test_pipeline_or_run_if_out_of_bounds():
  function test_pipeline_close_dead_then_rebalance (line 1829) | def test_pipeline_close_dead_then_rebalance():
  function test_pipeline_scale_weights_deleverage (line 1846) | def test_pipeline_scale_weights_deleverage():
  function test_pipeline_select_randomly_weigh_randomly (line 1863) | def test_pipeline_select_randomly_weigh_randomly():
  function test_pipeline_weigh_target_from_df (line 1878) | def test_pipeline_weigh_target_from_df():
  function test_select_regex_matches (line 1904) | def test_select_regex_matches():
  function test_select_regex_no_match_skips (line 1912) | def test_select_regex_no_match_skips():
  function test_select_regex_case_insensitive (line 1919) | def test_select_regex_case_insensitive():
  function test_select_regex_in_pipeline (line 1927) | def test_select_regex_in_pipeline():
  function test_hedge_risks_adjusts_weights (line 1949) | def test_hedge_risks_adjusts_weights():
  function test_hedge_risks_no_target_weights (line 1965) | def test_hedge_risks_no_target_weights():
  function test_hedge_risks_no_history (line 1971) | def test_hedge_risks_no_history():
  function test_hedge_risks_in_pipeline (line 1981) | def test_hedge_risks_in_pipeline():
  function test_margin_scales_weights (line 2002) | def test_margin_scales_weights():
  function test_margin_charges_interest (line 2014) | def test_margin_charges_interest():
  function test_margin_reset (line 2028) | def test_margin_reset():
  function test_margin_call_stops (line 2035) | def test_margin_call_stops():
  function test_coupon_pays_on_schedule (line 2055) | def test_coupon_pays_on_schedule():
  function test_coupon_semi_annual_spacing (line 2068) | def test_coupon_semi_annual_spacing():
  function test_coupon_stops_at_maturity (line 2085) | def test_coupon_stops_at_maturity():
  function test_coupon_before_start_date (line 2095) | def test_coupon_before_start_date():
  function test_coupon_invalid_frequency (line 2104) | def test_coupon_invalid_frequency():
  function test_coupon_reset (line 2110) | def test_coupon_reset():
  function test_replay_buys_on_matching_date (line 2122) | def test_replay_buys_on_matching_date():
  function test_replay_sells (line 2143) | def test_replay_sells():
  function test_replay_no_trades_on_date (line 2161) | def test_replay_no_trades_on_date():
  function test_replay_closes_position_to_zero (line 2175) | def test_replay_closes_position_to_zero():
  function test_replay_missing_columns_raises (line 2192) | def test_replay_missing_columns_raises():
  function test_replay_in_pipeline (line 2199) | def test_replay_in_pipeline():
  function test_set_date_range_returns_stats (line 2220) | def test_set_date_range_returns_stats():

FILE: tests/engine/test_portfolio_integration.py
  function _ivy_stocks (line 22) | def _ivy_stocks():
  function _stocks_data (line 27) | def _stocks_data():
  function _options_data (line 33) | def _options_data():
  function _buy_strategy (line 46) | def _buy_strategy(schema):
  function _run_engine (line 55) | def _run_engine():
  class TestPortfolioIntegration (line 73) | class TestPortfolioIntegration:
    method setup (line 77) | def setup(self):
    method test_portfolio_exists (line 80) | def test_portfolio_exists(self):
    method test_portfolio_position_count_matches_inventory (line 84) | def test_portfolio_position_count_matches_inventory(self):
    method test_positions_have_correct_legs (line 92) | def test_positions_have_correct_legs(self):
    method test_trade_log_not_empty (line 100) | def test_trade_log_not_empty(self):
    method test_portfolio_contracts_match_inventory (line 104) | def test_portfolio_contracts_match_inventory(self):

FILE: tests/engine/test_regression_snapshots.py
  function _ivy_stocks (line 32) | def _ivy_stocks():
  function _stocks_data (line 37) | def _stocks_data():
  function _options_data (line 43) | def _options_data():
  function _build_strategy (line 56) | def _build_strategy(schema, direction=Direction.BUY):
  function _run (line 65) | def _run(cost_model=None, direction=Direction.BUY, monthly=False):
  class TestSnapshotBuyPutNoCosts (line 88) | class TestSnapshotBuyPutNoCosts:
    method setup (line 92) | def setup(self):
    method test_final_capital (line 95) | def test_final_capital(self):
    method test_trade_count (line 99) | def test_trade_count(self):
    method test_balance_rows (line 103) | def test_balance_rows(self):
    method test_total_return (line 107) | def test_total_return(self):
    method test_max_drawdown (line 112) | def test_max_drawdown(self):
  class TestSnapshotBuyPutWithCommission (line 120) | class TestSnapshotBuyPutWithCommission:
    method setup (line 124) | def setup(self):
    method test_commission_reduces_capital (line 128) | def test_commission_reduces_capital(self):
    method test_final_capital (line 133) | def test_final_capital(self):
  class TestSnapshotSellPut (line 138) | class TestSnapshotSellPut:
    method setup (line 142) | def setup(self):
    method test_final_capital (line 145) | def test_final_capital(self):
    method test_trade_count (line 149) | def test_trade_count(self):
    method test_sell_vs_buy_differ (line 153) | def test_sell_vs_buy_differ(self):
  class TestSnapshotMonthlyRebalance (line 160) | class TestSnapshotMonthlyRebalance:
    method setup (line 164) | def setup(self):
    method test_fewer_balance_rows (line 168) | def test_fewer_balance_rows(self):
    method test_final_capital (line 173) | def test_final_capital(self):
    method test_balance_rows (line 177) | def test_balance_rows(self):

FILE: tests/engine/test_risk_wiring.py
  function _ivy_stocks (line 21) | def _ivy_stocks():
  function _stocks_data (line 26) | def _stocks_data():
  function _options_data (line 32) | def _options_data():
  function _buy_strategy (line 45) | def _buy_strategy(schema):
  function _run_engine (line 54) | def _run_engine(risk_manager=None):
  class TestRiskManagerWiring (line 73) | class TestRiskManagerWiring:
    method test_no_constraints_allows_all (line 76) | def test_no_constraints_allows_all(self):
    method test_max_delta_blocks_entries (line 80) | def test_max_delta_blocks_entries(self):
    method test_max_drawdown_blocks_during_crash (line 91) | def test_max_drawdown_blocks_during_crash(self):
    method test_risk_manager_preserves_capital (line 101) | def test_risk_manager_preserves_capital(self):

FILE: tests/engine/test_rust_parity.py
  function _ivy_stocks (line 32) | def _ivy_stocks():
  function _stocks_data (line 37) | def _stocks_data():
  function _options_data (line 43) | def _options_data():
  function _buy_strategy (line 56) | def _buy_strategy(schema):
  function _make_engine (line 65) | def _make_engine():
  function _run_python_path (line 78) | def _run_python_path():
  function _run_rust_path (line 93) | def _run_rust_path():
  class TestRustVsPythonParity (line 101) | class TestRustVsPythonParity:
    method setup (line 110) | def setup(self):
    method test_trade_log_shape (line 113) | def test_trade_log_shape(self):
    method test_regression_costs (line 116) | def test_regression_costs(self):
    method test_regression_qtys (line 122) | def test_regression_qtys(self):
    method test_final_capital (line 127) | def test_final_capital(self):
    method test_balance_row_count (line 131) | def test_balance_row_count(self):
    method test_balance_column_count (line 134) | def test_balance_column_count(self):
    method test_balance_has_all_columns (line 137) | def test_balance_has_all_columns(self):
    method test_initial_row_capital (line 149) | def test_initial_row_capital(self):
    method test_accumulated_return_starts_at_one (line 153) | def test_accumulated_return_starts_at_one(self):
  class TestRustDispatchGating (line 161) | class TestRustDispatchGating:
    method test_default_config_runs (line 164) | def test_default_config_runs(self):
    method test_custom_cost_model_runs (line 177) | def test_custom_cost_model_runs(self):
    method test_custom_selector_runs (line 190) | def test_custom_selector_runs(self):
    method test_per_leg_override_runs (line 204) | def test_per_leg_override_runs(self):
  class TestThresholdExits (line 230) | class TestThresholdExits:
    method test_profit_threshold_triggers_exit (line 233) | def test_profit_threshold_triggers_exit(self):
    method test_loss_threshold_triggers_exit (line 258) | def test_loss_threshold_triggers_exit(self):
    method test_both_thresholds (line 280) | def test_both_thresholds(self):
  class TestPerLegSellDirection (line 304) | class TestPerLegSellDirection:
    method test_sell_leg_with_midprice (line 307) | def test_sell_leg_with_midprice(self):
  class TestBalanceCompleteness (line 344) | class TestBalanceCompleteness:
    method test_balance_columns_present (line 347) | def test_balance_columns_present(self):
    method test_balance_no_negative_total_capital (line 376) | def test_balance_no_negative_total_capital(self):
  class TestEdgeCases (line 395) | class TestEdgeCases:
    method test_high_rebalance_freq (line 398) | def test_high_rebalance_freq(self):
    method test_stop_if_broke (line 413) | def test_stop_if_broke(self):

FILE: tests/engine/test_signal_selector_wiring.py
  function _ivy_stocks (line 28) | def _ivy_stocks():
  function _stocks_data (line 33) | def _stocks_data():
  function _options_data (line 39) | def _options_data():
  function _buy_strategy (line 52) | def _buy_strategy(schema):
  function _run_engine (line 61) | def _run_engine(signal_selector):
  class TestSignalSelectorWiring (line 80) | class TestSignalSelectorWiring:
    method test_first_match_still_works (line 83) | def test_first_match_still_works(self):
    method test_different_selectors_may_pick_different_contracts (line 87) | def test_different_selectors_may_pick_different_contracts(self):
    method test_nearest_delta_runs_without_error (line 95) | def test_nearest_delta_runs_without_error(self):
    method test_max_open_interest_runs (line 99) | def test_max_open_interest_runs(self):

FILE: tests/engine/test_strategy_tree.py
  function test_strategy_tree_allocates_capital_by_weights (line 10) | def test_strategy_tree_allocates_capital_by_weights():
  function test_nested_tree_weight_propagation (line 25) | def test_nested_tree_weight_propagation():
  function test_leaf_max_share_throttles_allocation (line 41) | def test_leaf_max_share_throttles_allocation():
  function test_node_rejects_engine_and_children (line 58) | def test_node_rejects_engine_and_children():
  function test_empty_branch_produces_no_leaves (line 68) | def test_empty_branch_produces_no_leaves():
  function test_engine_capital_restored_after_tree_run (line 81) | def test_engine_capital_restored_after_tree_run():
  function test_capital_uses_round_not_truncate (line 96) | def test_capital_uses_round_not_truncate():
  function test_balance_has_pct_change_and_accumulated_return (line 112) | def test_balance_has_pct_change_and_accumulated_return():
  function test_attribution_dict_structure (line 122) | def test_attribution_dict_structure():
  function test_to_dot_single_leaf (line 137) | def test_to_dot_single_leaf():
  function test_to_dot_nested_tree (line 146) | def test_to_dot_nested_tree():
  function test_to_dot_max_share_shown (line 159) | def test_to_dot_max_share_shown():
  function test_engine_to_dot_delegates_to_root (line 165) | def test_engine_to_dot_delegates_to_root():

FILE: tests/execution/test_cost_model.py
  class TestNoCosts (line 8) | class TestNoCosts:
    method test_option_cost_is_zero (line 9) | def test_option_cost_is_zero(self):
    method test_stock_cost_is_zero (line 13) | def test_stock_cost_is_zero(self):
  class TestPerContractCommission (line 18) | class TestPerContractCommission:
    method test_default_rate (line 19) | def test_default_rate(self):
    method test_custom_rate (line 23) | def test_custom_rate(self):
    method test_stock_rate (line 27) | def test_stock_rate(self):
    method test_negative_qty_uses_abs (line 31) | def test_negative_qty_uses_abs(self):
  class TestTieredCommission (line 36) | class TestTieredCommission:
    method test_default_tiers_small_qty (line 37) | def test_default_tiers_small_qty(self):
    method test_default_tiers_large_qty (line 42) | def test_default_tiers_large_qty(self):
  class TestSpreadSlippage (line 49) | class TestSpreadSlippage:
    method test_zero_pct (line 50) | def test_zero_pct(self):
    method test_half_spread (line 54) | def test_half_spread(self):
    method test_full_spread (line 59) | def test_full_spread(self):
    method test_option_cost_is_zero (line 63) | def test_option_cost_is_zero(self):
    method test_stock_cost_is_zero (line 68) | def test_stock_cost_is_zero(self):
  class TestTieredCommissionEdgeCases (line 73) | class TestTieredCommissionEdgeCases:
    method test_qty_exceeds_all_tiers (line 74) | def test_qty_exceeds_all_tiers(self):
    method test_stock_cost (line 81) | def test_stock_cost(self):
    method test_tier_boundary_exact (line 85) | def test_tier_boundary_exact(self):
  class TestRustConfigs (line 92) | class TestRustConfigs:
    method test_no_costs_rust_config (line 93) | def test_no_costs_rust_config(self):
    method test_per_contract_rust_config (line 97) | def test_per_contract_rust_config(self):
    method test_tiered_rust_config (line 103) | def test_tiered_rust_config(self):

FILE: tests/execution/test_execution_deep.py
  class TestNoCosts (line 40) | class TestNoCosts:
    method test_option_cost_always_zero (line 41) | def test_option_cost_always_zero(self):
    method test_stock_cost_always_zero (line 46) | def test_stock_cost_always_zero(self):
    method test_rust_config (line 50) | def test_rust_config(self):
  class TestPerContractCommission (line 54) | class TestPerContractCommission:
    method test_basic_cost (line 55) | def test_basic_cost(self):
    method test_negative_qty_uses_abs (line 59) | def test_negative_qty_uses_abs(self):
    method test_zero_qty (line 63) | def test_zero_qty(self):
    method test_stock_cost_per_share (line 67) | def test_stock_cost_per_share(self):
    method test_stock_cost_negative_qty (line 71) | def test_stock_cost_negative_qty(self):
    method test_rust_config_roundtrip (line 75) | def test_rust_config_roundtrip(self):
  class TestTieredCommission (line 83) | class TestTieredCommission:
    method test_default_tiers (line 84) | def test_default_tiers(self):
    method test_tier_boundary (line 89) | def test_tier_boundary(self):
    method test_crosses_first_tier (line 95) | def test_crosses_first_tier(self):
    method test_crosses_all_tiers (line 102) | def test_crosses_all_tiers(self):
    method test_custom_tiers (line 109) | def test_custom_tiers(self):
    method test_negative_qty (line 115) | def test_negative_qty(self):
    method test_zero_qty (line 120) | def test_zero_qty(self):
    method test_rust_config (line 124) | def test_rust_config(self):
  class TestSpreadSlippage (line 131) | class TestSpreadSlippage:
    method test_option_cost_is_zero (line 132) | def test_option_cost_is_zero(self):
    method test_slippage_computation (line 136) | def test_slippage_computation(self):
    method test_slippage_zero_spread (line 142) | def test_slippage_zero_spread(self):
    method test_slippage_full_pct (line 146) | def test_slippage_full_pct(self):
    method test_pct_bounds (line 151) | def test_pct_bounds(self):
  function _make_option_row (line 163) | def _make_option_row(bid=9.0, ask=10.0, volume=100):
  class TestMarketAtBidAsk (line 167) | class TestMarketAtBidAsk:
    method test_buy_fills_at_ask (line 168) | def test_buy_fills_at_ask(self):
    method test_sell_fills_at_bid (line 172) | def test_sell_fills_at_bid(self):
    method test_zero_spread (line 176) | def test_zero_spread(self):
  class TestMidPrice (line 182) | class TestMidPrice:
    method test_midpoint (line 183) | def test_midpoint(self):
    method test_same_bid_ask (line 187) | def test_same_bid_ask(self):
    method test_direction_doesnt_matter (line 191) | def test_direction_doesnt_matter(self):
    method test_wide_spread (line 196) | def test_wide_spread(self):
  class TestVolumeAwareFill (line 201) | class TestVolumeAwareFill:
    method test_high_volume_fills_at_target (line 202) | def test_high_volume_fills_at_target(self):
    method test_zero_volume_fills_at_mid (line 208) | def test_zero_volume_fills_at_mid(self):
    method test_half_volume_interpolates (line 214) | def test_half_volume_interpolates(self):
    method test_above_threshold_same_as_market (line 222) | def test_above_threshold_same_as_market(self):
    method test_rust_config (line 227) | def test_rust_config(self):
  function _make_candidates (line 239) | def _make_candidates(n=5, with_delta=True, with_oi=True):
  class TestFirstMatch (line 253) | class TestFirstMatch:
    method test_selects_first_row (line 254) | def test_selects_first_row(self):
    method test_single_row (line 260) | def test_single_row(self):
  class TestNearestDelta (line 267) | class TestNearestDelta:
    method test_selects_closest_delta (line 268) | def test_selects_closest_delta(self):
    method test_boundary_delta (line 275) | def test_boundary_delta(self):
    method test_missing_delta_column_fallback (line 282) | def test_missing_delta_column_fallback(self):
    method test_column_requirements (line 289) | def test_column_requirements(self):
    method test_rust_config (line 293) | def test_rust_config(self):
  class TestMaxOpenInterest (line 299) | class TestMaxOpenInterest:
    method test_selects_highest_oi (line 300) | def test_selects_highest_oi(self):
    method test_missing_oi_column_fallback (line 306) | def test_missing_oi_column_fallback(self):
    method test_custom_oi_column (line 312) | def test_custom_oi_column(self):
  class TestCapitalBasedSizer (line 325) | class TestCapitalBasedSizer:
    method test_basic_sizing (line 326) | def test_basic_sizing(self):
    method test_fractional_truncated (line 330) | def test_fractional_truncated(self):
    method test_zero_cost (line 334) | def test_zero_cost(self):
    method test_cost_exceeds_capital (line 338) | def test_cost_exceeds_capital(self):
    method test_negative_cost_uses_abs (line 342) | def test_negative_cost_uses_abs(self):
  class TestFixedQuantitySizer (line 347) | class TestFixedQuantitySizer:
    method test_within_budget (line 348) | def test_within_budget(self):
    method test_exceeds_budget_scales_down (line 352) | def test_exceeds_budget_scales_down(self):
    method test_zero_cost_returns_fixed_qty (line 356) | def test_zero_cost_returns_fixed_qty(self):
    method test_one_contract (line 361) | def test_one_contract(self):
  class TestFixedDollarSizer (line 366) | class TestFixedDollarSizer:
    method test_within_budget (line 367) | def test_within_budget(self):
    method test_amount_exceeds_available (line 371) | def test_amount_exceeds_available(self):
    method test_zero_cost (line 375) | def test_zero_cost(self):
  class TestPercentOfPortfolioSizer (line 380) | class TestPercentOfPortfolioSizer:
    method test_basic (line 381) | def test_basic(self):
    method test_pct_exceeds_available (line 386) | def test_pct_exceeds_available(self):
    method test_invalid_pct (line 391) | def test_invalid_pct(self):
    method test_zero_cost (line 397) | def test_zero_cost(self):

FILE: tests/execution/test_execution_pbt.py
  class TestNoCostsPBT (line 57) | class TestNoCostsPBT:
    method test_always_zero (line 60) | def test_always_zero(self, p, q, s):
  class TestPerContractPBT (line 66) | class TestPerContractPBT:
    method test_non_negative (line 69) | def test_non_negative(self, r, p, q, s):
    method test_symmetric_buy_sell (line 75) | def test_symmetric_buy_sell(self, r, p, q, s):
    method test_linear_in_quantity (line 82) | def test_linear_in_quantity(self, r, p, q, s):
    method test_independent_of_price_and_spc (line 92) | def test_independent_of_price_and_spc(self, r, p, q):
    method test_stock_cost_non_negative (line 102) | def test_stock_cost_non_negative(self, sr, p, q):
  class TestTieredCommissionPBT (line 107) | class TestTieredCommissionPBT:
    method test_non_negative (line 110) | def test_non_negative(self, p, q, s):
    method test_symmetric (line 116) | def test_symmetric(self, p, q, s):
    method test_monotone_in_quantity (line 122) | def test_monotone_in_quantity(self, q):
    method test_bounded_by_flat_rate (line 131) | def test_bounded_by_flat_rate(self, q):
    method test_bounded_below_by_lowest_rate (line 140) | def test_bounded_below_by_lowest_rate(self, q):
    method test_average_rate_decreasing (line 149) | def test_average_rate_decreasing(self, q):
  class TestSpreadSlippagePBT (line 160) | class TestSpreadSlippagePBT:
    method test_option_cost_always_zero (line 163) | def test_option_cost_always_zero(self, pct, p, q, s):
    method test_slippage_non_negative (line 171) | def test_slippage_non_negative(self, pct, bid, ask, q, s):
    method test_zero_spread_zero_slippage (line 178) | def test_zero_spread_zero_slippage(self, pct, p, q, s):
    method test_slippage_monotone_in_pct (line 185) | def test_slippage_monotone_in_pct(self, pct, p, q, s):
    method test_slippage_linear_in_quantity (line 197) | def test_slippage_linear_in_quantity(self, pct, p, s):
  class TestMarketAtBidAskPBT (line 211) | class TestMarketAtBidAskPBT:
    method test_buy_at_ask_sell_at_bid (line 215) | def test_buy_at_ask_sell_at_bid(self, bid, ask):
    method test_zero_spread_both_equal (line 224) | def test_zero_spread_both_equal(self, p):
    method test_buy_never_cheaper_than_sell (line 232) | def test_buy_never_cheaper_than_sell(self, bid, ask):
  class TestMidPricePBT (line 240) | class TestMidPricePBT:
    method test_between_bid_and_ask (line 245) | def test_between_bid_and_ask(self, bid, ask, d):
    method test_direction_independent (line 255) | def test_direction_independent(self, bid, ask):
    method test_midpoint_formula (line 264) | def test_midpoint_formula(self, bid, ask):
  class TestVolumeAwareFillPBT (line 272) | class TestVolumeAwareFillPBT:
    method test_fill_between_mid_and_edge (line 277) | def test_fill_between_mid_and_edge(self, bid, ask, vol, threshold, d):
    method test_zero_volume_fills_at_mid (line 293) | def test_zero_volume_fills_at_mid(self, bid, ask, threshold):
    method test_higher_volume_moves_toward_edge (line 305) | def test_higher_volume_moves_toward_edge(self, bid, ask, threshold, d):
    method test_above_threshold_equals_market (line 323) | def test_above_threshold_equals_market(self, bid, ask, threshold):
  function _make_candidates (line 336) | def _make_candidates(n, deltas=None, ois=None):
  class TestFirstMatchPBT (line 350) | class TestFirstMatchPBT:
    method test_always_selects_row_from_dataframe (line 353) | def test_always_selects_row_from_dataframe(self, n):
    method test_always_selects_first (line 361) | def test_always_selects_first(self, n):
  class TestNearestDeltaPBT (line 367) | class TestNearestDeltaPBT:
    method test_selected_is_closest_to_target (line 371) | def test_selected_is_closest_to_target(self, target, n):
    method test_result_is_valid_row (line 384) | def test_result_is_valid_row(self, target, n):
    method test_missing_delta_falls_back_to_first (line 393) | def test_missing_delta_falls_back_to_first(self, n):
  class TestMaxOpenInterestPBT (line 400) | class TestMaxOpenInterestPBT:
    method test_selects_max_oi (line 403) | def test_selects_max_oi(self, ois):
    method test_missing_oi_falls_back (line 411) | def test_missing_oi_falls_back(self, n):
    method test_uniform_oi_selects_some_row (line 419) | def test_uniform_oi_selects_some_row(self, n, oi):
  class TestCapitalBasedPBT (line 432) | class TestCapitalBasedPBT:
    method test_non_negative_integer (line 436) | def test_non_negative_integer(self, cost, avail, total):
    method test_total_cost_within_budget (line 445) | def test_total_cost_within_budget(self, cost, avail, total):
    method test_zero_cost_returns_zero (line 453) | def test_zero_cost_returns_zero(self, avail, total):
    method test_more_capital_more_contracts (line 461) | def test_more_capital_more_contracts(self, cost, avail, total):
    method test_negative_cost_same_as_positive (line 472) | def test_negative_cost_same_as_positive(self, cost, avail, total):
  class TestFixedQuantityPBT (line 477) | class TestFixedQuantityPBT:
    method test_never_exceeds_budget (line 482) | def test_never_exceeds_budget(self, qty, cost, avail, total):
    method test_large_budget_returns_fixed (line 492) | def test_large_budget_returns_fixed(self, qty, cost, avail, total):
    method test_bounded_by_capital_based (line 501) | def test_bounded_by_capital_based(self, qty, cost, avail, total):
  class TestFixedDollarPBT (line 509) | class TestFixedDollarPBT:
    method test_non_negative_integer (line 514) | def test_non_negative_integer(self, amount, cost, avail, total):
    method test_total_cost_within_min_amount_avail (line 524) | def test_total_cost_within_min_amount_avail(self, amount, cost, avail,...
    method test_zero_cost_returns_zero (line 533) | def test_zero_cost_returns_zero(self, amount, avail, total):
  class TestPercentOfPortfolioPBT (line 538) | class TestPercentOfPortfolioPBT:
    method test_non_negative_integer (line 544) | def test_non_negative_integer(self, pct, cost, avail, total):
    method test_cost_bounded_by_pct_of_total (line 555) | def test_cost_bounded_by_pct_of_total(self, pct, cost, avail, total):
    method test_zero_cost_returns_zero (line 565) | def test_zero_cost_returns_zero(self, pct, avail, total):
    method test_higher_pct_more_contracts (line 574) | def test_higher_pct_more_contracts(self, pct, cost, avail, total):

FILE: tests/execution/test_fill_model.py
  function _make_row (line 11) | def _make_row(bid: float = 1.00, ask: float = 1.10, volume: int = 100) -...
  class TestMarketAtBidAsk (line 15) | class TestMarketAtBidAsk:
    method test_buy_fills_at_ask (line 16) | def test_buy_fills_at_ask(self):
    method test_sell_fills_at_bid (line 20) | def test_sell_fills_at_bid(self):
  class TestMidPrice (line 25) | class TestMidPrice:
    method test_mid (line 26) | def test_mid(self):
    method test_mid_sell (line 30) | def test_mid_sell(self):
  class TestVolumeAwareFill (line 35) | class TestVolumeAwareFill:
    method test_high_volume_fills_at_target (line 36) | def test_high_volume_fills_at_target(self):
    method test_zero_volume_fills_at_mid (line 40) | def test_zero_volume_fills_at_mid(self):
    method test_half_volume_interpolates (line 44) | def test_half_volume_interpolates(self):

FILE: tests/execution/test_rust_parity_execution.py
  class TestCostModelEdgeCases (line 30) | class TestCostModelEdgeCases:
    method test_per_contract_basic (line 32) | def test_per_contract_basic(self):
    method test_per_contract_negative_quantity (line 35) | def test_per_contract_negative_quantity(self):
    method test_per_contract_zero_quantity (line 38) | def test_per_contract_zero_quantity(self):
    method test_per_contract_stock (line 41) | def test_per_contract_stock(self):
    method test_zero_rate (line 44) | def test_zero_rate(self):
    method test_very_small_quantity (line 48) | def test_very_small_quantity(self):
    method test_tiered_within_first_tier (line 51) | def test_tiered_within_first_tier(self):
    method test_tiered_spanning_tiers (line 55) | def test_tiered_spanning_tiers(self):
    method test_tiered_beyond_all (line 60) | def test_tiered_beyond_all(self):
    method test_tiered_exactly_at_boundary (line 65) | def test_tiered_exactly_at_boundary(self):
    method test_tiered_negative_quantity (line 69) | def test_tiered_negative_quantity(self):
    method test_very_large_quantity (line 74) | def test_very_large_quantity(self):
    method test_invalid_model_type_raises (line 78) | def test_invalid_model_type_raises(self):
  class TestFillModelEdgeCases (line 83) | class TestFillModelEdgeCases:
    method test_full_volume_buy (line 85) | def test_full_volume_buy(self):
    method test_full_volume_sell (line 88) | def test_full_volume_sell(self):
    method test_zero_volume (line 91) | def test_zero_volume(self):
    method test_half_volume (line 96) | def test_half_volume(self):
    method test_missing_volume (line 100) | def test_missing_volume(self):
    method test_zero_bid_ask (line 103) | def test_zero_bid_ask(self):
    method test_bid_equals_ask (line 106) | def test_bid_equals_ask(self):
    method test_invalid_fill_type_raises (line 109) | def test_invalid_fill_type_raises(self):
  class TestSignalSelectorEdgeCases (line 114) | class TestSignalSelectorEdgeCases:
    method test_nearest_delta_exact (line 116) | def test_nearest_delta_exact(self):
    method test_nearest_delta_between (line 119) | def test_nearest_delta_between(self):
    method test_empty_list (line 122) | def test_empty_list(self):
    method test_all_nan_nearest (line 126) | def test_all_nan_nearest(self):
    method test_all_nan_max (line 129) | def test_all_nan_max(self):
    method test_single_element (line 132) | def test_single_element(self):
    method test_max_value_basic (line 136) | def test_max_value_basic(self):
    method test_max_value_negative (line 139) | def test_max_value_negative(self):
    method test_max_value_ties_first_wins (line 142) | def test_max_value_ties_first_wins(self):
    method test_large_list (line 145) | def test_large_list(self):
  class TestRiskCheckEdgeCases (line 149) | class TestRiskCheckEdgeCases:
    method test_max_delta_allows (line 151) | def test_max_delta_allows(self):
    method test_max_delta_rejects (line 154) | def test_max_delta_rejects(self):
    method test_max_delta_exactly_at_limit (line 157) | def test_max_delta_exactly_at_limit(self):
    method test_max_delta_negative (line 160) | def test_max_delta_negative(self):
    method test_max_vega_allows (line 163) | def test_max_vega_allows(self):
    method test_max_vega_rejects (line 166) | def test_max_vega_rejects(self):
    method test_max_drawdown_allows (line 169) | def test_max_drawdown_allows(self):
    method test_max_drawdown_rejects (line 173) | def test_max_drawdown_rejects(self):
    method test_max_drawdown_zero_peak (line 177) | def test_max_drawdown_zero_peak(self):
    method test_zero_greeks (line 181) | def test_zero_greeks(self):
    method test_negative_peak_value (line 186) | def test_negative_peak_value(self):
    method test_invalid_constraint_raises (line 190) | def test_invalid_constraint_raises(self):
  class TestCostInvariants (line 205) | class TestCostInvariants:
    method test_cost_always_non_negative (line 209) | def test_cost_always_non_negative(self, rate, qty):
    method test_tiered_cost_non_negative (line 214) | def test_tiered_cost_non_negative(self, qty):
    method test_sign_symmetry (line 220) | def test_sign_symmetry(self, rate, qty):
  class TestFillInvariants (line 226) | class TestFillInvariants:
    method test_fill_between_bid_ask (line 235) | def test_fill_between_bid_ask(self, bid, spread, vol, is_buy):
  class TestSelectorInvariants (line 242) | class TestSelectorInvariants:
    method test_index_in_range (line 249) | def test_index_in_range(self, values, target):
    method test_max_index_in_range (line 260) | def test_max_index_in_range(self, values):
  class TestRiskInvariants (line 265) | class TestRiskInvariants:
    method test_higher_limit_more_permissive (line 273) | def test_higher_limit_more_permissive(self, limit_low, limit_high, del...
  class TestPythonClassDelegation (line 286) | class TestPythonClassDelegation:
    method test_per_contract_via_class (line 288) | def test_per_contract_via_class(self):
    method test_tiered_via_class (line 294) | def test_tiered_via_class(self):
    method test_volume_aware_via_class (line 300) | def test_volume_aware_via_class(self):
    method test_nearest_delta_via_class (line 307) | def test_nearest_delta_via_class(self):
    method test_max_oi_via_class (line 313) | def test_max_oi_via_class(self):
    method test_max_delta_via_class (line 318) | def test_max_delta_via_class(self):
    method test_max_vega_via_class (line 324) | def test_max_vega_via_class(self):
    method test_max_drawdown_via_class (line 330) | def test_max_drawdown_via_class(self):

FILE: tests/execution/test_signal_selector.py
  function _make_candidates (line 10) | def _make_candidates() -> pd.DataFrame:
  class TestFirstMatch (line 19) | class TestFirstMatch:
    method test_picks_first (line 20) | def test_picks_first(self):
  class TestNearestDelta (line 26) | class TestNearestDelta:
    method test_nearest_to_target (line 27) | def test_nearest_to_target(self):
    method test_nearest_to_different_target (line 32) | def test_nearest_to_different_target(self):
    method test_fallback_without_column (line 37) | def test_fallback_without_column(self):
  class TestMaxOpenInterest (line 44) | class TestMaxOpenInterest:
    method test_picks_max_oi (line 45) | def test_picks_max_oi(self):
    method test_fallback_without_column (line 50) | def test_fallback_without_column(self):

FILE: tests/execution/test_sizer.py
  class TestCapitalBased (line 8) | class TestCapitalBased:
    method test_basic_sizing (line 9) | def test_basic_sizing(self):
    method test_zero_cost (line 13) | def test_zero_cost(self):
    method test_insufficient_capital (line 17) | def test_insufficient_capital(self):
  class TestFixedQuantity (line 22) | class TestFixedQuantity:
    method test_fixed (line 23) | def test_fixed(self):
    method test_insufficient_capital_reduces (line 27) | def test_insufficient_capital_reduces(self):
  class TestFixedDollar (line 32) | class TestFixedDollar:
    method test_fixed_amount (line 33) | def test_fixed_amount(self):
    method test_amount_capped_by_available (line 37) | def test_amount_capped_by_available(self):
  class TestPercentOfPortfolio (line 42) | class TestPercentOfPortfolio:
    meth
Condensed preview — 181 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,588K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 542,
    "preview": "name: Test\n\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkou"
  },
  {
    "path": ".gitignore",
    "chars": 1662,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".python-version",
    "chars": 6,
    "preview": "3.12\n\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2299,
    "preview": "Contributing\n============\n\nContributions are welcome and very much appreciated. Credit will be appropriately given.\n\n## "
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "MIT License\n\nCopyright (c) 2019 Federico Carrone\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "Makefile",
    "chars": 2453,
    "preview": "NIX_CMD := XDG_CACHE_HOME=$(CURDIR)/.cache nix --extra-experimental-features 'nix-command flakes' develop --command\nRUNC"
  },
  {
    "path": "README.md",
    "chars": 8181,
    "preview": "Options Portfolio Backtester\n============================\n\nBacktest options strategies with realistic execution, Greeks-"
  },
  {
    "path": "benchmarks/benchmark_large_pipeline.py",
    "chars": 11396,
    "preview": "\"\"\"Large-scale performance benchmark: Rust vs Python on production data.\n\nRuns the same strategy through Rust full-loop "
  },
  {
    "path": "benchmarks/benchmark_matrix.py",
    "chars": 9837,
    "preview": "\"\"\"Standardized benchmark matrix for options_portfolio_backtester vs bt.\n\nRuns multiple scenarios over date ranges/rebal"
  },
  {
    "path": "benchmarks/benchmark_rust_vs_python.py",
    "chars": 14165,
    "preview": "\"\"\"Benchmark: Rust full-loop vs Python BacktestEngine vs legacy Backtest vs bt.\n\nRuns options backtest (with options dat"
  },
  {
    "path": "benchmarks/benchmark_sweep.py",
    "chars": 15858,
    "preview": "\"\"\"Benchmark: Rust parallel_sweep vs Python sequential grid search.\n\nThis is the PRIMARY benchmark for justifying the Ru"
  },
  {
    "path": "benchmarks/compare_with_bt.py",
    "chars": 8806,
    "preview": "\"\"\"Head-to-head comparison: options_portfolio_backtester stock-only mode vs bt.\n\nThis harness runs the same monthly stoc"
  },
  {
    "path": "data/README.md",
    "chars": 2193,
    "preview": "# Data Scripts\n\nScripts for fetching and converting market data into the formats expected by the backtester.\n\n## Quick S"
  },
  {
    "path": "data/convert_optionsdx.py",
    "chars": 3451,
    "preview": "#!/usr/bin/env python3\n\"\"\"Convert OptionsDX wide-format CSV to backtester long-format CSV.\n\nOptionsDX provides one row p"
  },
  {
    "path": "data/fetch_data.py",
    "chars": 15144,
    "preview": "#!/usr/bin/env python3\n\"\"\"Unified data fetch script for the options backtester.\n\nDownloads stock and options data, conve"
  },
  {
    "path": "data/fetch_signals.py",
    "chars": 3480,
    "preview": "#!/usr/bin/env python3\n\"\"\"Download macro signal data from FRED for use in backtest signal filters.\n\nDownloads:\n  - GDP ("
  },
  {
    "path": "flake.nix",
    "chars": 2831,
    "preview": "{\n  description = \"Options backtester dev environment\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-un"
  },
  {
    "path": "options_portfolio_backtester/__init__.py",
    "chars": 3030,
    "preview": "\"\"\"options_portfolio_backtester — the open-source options backtesting framework.\"\"\"\n\n# Core types\nfrom options_portfolio"
  },
  {
    "path": "options_portfolio_backtester/analytics/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "options_portfolio_backtester/analytics/charts.py",
    "chars": 4659,
    "preview": "\"\"\"Charts — Altair charts + matplotlib additions.\"\"\"\n\nfrom __future__ import annotations\n\nimport altair as alt\nimport pa"
  },
  {
    "path": "options_portfolio_backtester/analytics/optimization.py",
    "chars": 4504,
    "preview": "\"\"\"Walk-forward optimization and parameter grid sweep.\"\"\"\n\nfrom __future__ import annotations\n\nimport itertools\nfrom con"
  },
  {
    "path": "options_portfolio_backtester/analytics/stats.py",
    "chars": 10389,
    "preview": "\"\"\"BacktestStats — comprehensive analytics matching and exceeding bt/ffn.\n\nProvides:\n- Trade stats: profit factor, win r"
  },
  {
    "path": "options_portfolio_backtester/analytics/summary.py",
    "chars": 3198,
    "preview": "\"\"\"Summary statistics for trade logs.\"\"\"\n\nfrom __future__ import annotations\n\nimport numpy as np\nimport pandas as pd\n\nfr"
  },
  {
    "path": "options_portfolio_backtester/analytics/tearsheet.py",
    "chars": 4011,
    "preview": "\"\"\"Simple tearsheet-style report helpers.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom"
  },
  {
    "path": "options_portfolio_backtester/analytics/trade_log.py",
    "chars": 4729,
    "preview": "\"\"\"Structured trade log — replaces MultiIndex trade log with per-trade P&L.\"\"\"\n\nfrom __future__ import annotations\n\nfrom"
  },
  {
    "path": "options_portfolio_backtester/convexity/__init__.py",
    "chars": 798,
    "preview": "\"\"\"Convexity scanner: cross-asset tail protection scoring and allocation.\"\"\"\n\nfrom options_portfolio_backtester.convexit"
  },
  {
    "path": "options_portfolio_backtester/convexity/_utils.py",
    "chars": 321,
    "preview": "\"\"\"Shared utilities for the convexity module.\"\"\"\n\nfrom __future__ import annotations\n\nimport numpy as np\nimport pandas a"
  },
  {
    "path": "options_portfolio_backtester/convexity/allocator.py",
    "chars": 1171,
    "preview": "\"\"\"Allocation strategies: pick which instrument(s) to hedge.\"\"\"\n\nfrom __future__ import annotations\n\n\ndef pick_cheapest("
  },
  {
    "path": "options_portfolio_backtester/convexity/backtest.py",
    "chars": 4114,
    "preview": "\"\"\"Backtest: run the monthly rebalance loop via Rust backend.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfro"
  },
  {
    "path": "options_portfolio_backtester/convexity/config.py",
    "chars": 1136,
    "preview": "\"\"\"Configuration: instrument registry and backtest parameters.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses "
  },
  {
    "path": "options_portfolio_backtester/convexity/scoring.py",
    "chars": 2297,
    "preview": "\"\"\"Scoring: compute convexity ratios via Rust backend.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\n\nimport nu"
  },
  {
    "path": "options_portfolio_backtester/convexity/viz.py",
    "chars": 2209,
    "preview": "\"\"\"Visualization: Altair charts for scores, allocations, and P&L.\"\"\"\n\nfrom __future__ import annotations\n\nimport altair "
  },
  {
    "path": "options_portfolio_backtester/core/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "options_portfolio_backtester/core/types.py",
    "chars": 4210,
    "preview": "\"\"\"Core domain types for options backtesting.\n\nDirection is decoupled from column names — use Direction.price_column ins"
  },
  {
    "path": "options_portfolio_backtester/data/__init__.py",
    "chars": 42,
    "preview": "\"\"\"Data module — schema and providers.\"\"\"\n"
  },
  {
    "path": "options_portfolio_backtester/data/providers.py",
    "chars": 10451,
    "preview": "\"\"\"Data providers — ABCs, CSV implementations, and data loaders.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom "
  },
  {
    "path": "options_portfolio_backtester/data/schema.py",
    "chars": 6707,
    "preview": "\"\"\"Filter DSL — Schema, Field, and Filter for building query expressions.\"\"\"\n\nfrom __future__ import annotations\n\nfrom t"
  },
  {
    "path": "options_portfolio_backtester/engine/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "options_portfolio_backtester/engine/algo_adapters.py",
    "chars": 5452,
    "preview": "\"\"\"Algo adapter layer to drive BacktestEngine with bt-style pipeline blocks.\"\"\"\n\nfrom __future__ import annotations\n\nimp"
  },
  {
    "path": "options_portfolio_backtester/engine/clock.py",
    "chars": 2598,
    "preview": "\"\"\"Trading clock — date iteration and rebalance scheduling.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import G"
  },
  {
    "path": "options_portfolio_backtester/engine/engine.py",
    "chars": 36599,
    "preview": "\"\"\"BacktestEngine — thin orchestrator composing all framework components.\n\nReplaces the monolithic Backtest class with a"
  },
  {
    "path": "options_portfolio_backtester/engine/multi_strategy.py",
    "chars": 3134,
    "preview": "\"\"\"Multi-strategy engine — run N strategies with shared capital and risk budget.\"\"\"\n\nfrom __future__ import annotations\n"
  },
  {
    "path": "options_portfolio_backtester/engine/pipeline.py",
    "chars": 51856,
    "preview": "\"\"\"Composable algo pipeline for stock portfolio workflows.\n\nProvides bt-compatible scheduling, selection, weighting, and"
  },
  {
    "path": "options_portfolio_backtester/engine/strategy_tree.py",
    "chars": 4762,
    "preview": "\"\"\"Hierarchical strategy tree runner.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\n\n"
  },
  {
    "path": "options_portfolio_backtester/execution/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "options_portfolio_backtester/execution/_rust_bridge.py",
    "chars": 242,
    "preview": "\"\"\"Rust execution functions from _ob_rust.\"\"\"\n\nfrom options_portfolio_backtester._ob_rust import (\n    rust_option_cost,"
  },
  {
    "path": "options_portfolio_backtester/execution/cost_model.py",
    "chars": 3901,
    "preview": "\"\"\"Transaction cost models for options and stocks.\"\"\"\n\nfrom __future__ import annotations\n\nfrom abc import ABC, abstract"
  },
  {
    "path": "options_portfolio_backtester/execution/fill_model.py",
    "chars": 2153,
    "preview": "\"\"\"Fill models — determine the execution price for trades.\"\"\"\n\nfrom __future__ import annotations\n\nfrom abc import ABC, "
  },
  {
    "path": "options_portfolio_backtester/execution/signal_selector.py",
    "chars": 2858,
    "preview": "\"\"\"Signal selectors — choose which contract to trade from a set of candidates.\"\"\"\n\nfrom __future__ import annotations\n\nf"
  },
  {
    "path": "options_portfolio_backtester/execution/sizer.py",
    "chars": 2507,
    "preview": "\"\"\"Position sizing models — determine how many contracts to trade.\"\"\"\n\nfrom __future__ import annotations\n\nfrom abc impo"
  },
  {
    "path": "options_portfolio_backtester/portfolio/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "options_portfolio_backtester/portfolio/greeks.py",
    "chars": 784,
    "preview": "\"\"\"Portfolio-level Greeks aggregation.\"\"\"\n\nfrom __future__ import annotations\n\nfrom options_portfolio_backtester.core.ty"
  },
  {
    "path": "options_portfolio_backtester/portfolio/portfolio.py",
    "chars": 3495,
    "preview": "\"\"\"Portfolio — clean replacement for MultiIndex DataFrames.\n\nUses plain dicts and dataclasses instead of MultiIndex Data"
  },
  {
    "path": "options_portfolio_backtester/portfolio/position.py",
    "chars": 2598,
    "preview": "\"\"\"Option position and position leg — replaces MultiIndex inventory rows.\"\"\"\n\nfrom __future__ import annotations\n\nfrom d"
  },
  {
    "path": "options_portfolio_backtester/portfolio/risk.py",
    "chars": 3744,
    "preview": "\"\"\"Risk management — constraints checked before entering positions.\"\"\"\n\nfrom __future__ import annotations\n\nfrom abc imp"
  },
  {
    "path": "options_portfolio_backtester/strategy/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "options_portfolio_backtester/strategy/presets.py",
    "chars": 11120,
    "preview": "\"\"\"Pre-built strategy constructors for common options strategies.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing im"
  },
  {
    "path": "options_portfolio_backtester/strategy/strategy.py",
    "chars": 2624,
    "preview": "\"\"\"Strategy container — preserved interface with richer execution support.\"\"\"\n\nfrom __future__ import annotations\n\nimpor"
  },
  {
    "path": "options_portfolio_backtester/strategy/strategy_leg.py",
    "chars": 2611,
    "preview": "\"\"\"Strategy leg — re-exports the original StrategyLeg for now.\n\nThe new StrategyLeg is API-compatible with the original "
  },
  {
    "path": "pyproject.toml",
    "chars": 1665,
    "preview": "[build-system]\nrequires = [\"maturin>=1.7,<2.0\"]\nbuild-backend = \"maturin\"\n\n[project]\nname = \"options_portfolio_backteste"
  },
  {
    "path": "rust/.cargo/config.toml",
    "chars": 48,
    "preview": "[env]\nPYO3_USE_ABI3_FORWARD_COMPATIBILITY = \"1\"\n"
  },
  {
    "path": "rust/Cargo.toml",
    "chars": 62,
    "preview": "[workspace]\nmembers = [\"ob_core\", \"ob_python\"]\nresolver = \"2\"\n"
  },
  {
    "path": "rust/ob_core/Cargo.toml",
    "chars": 394,
    "preview": "[package]\nname = \"ob_core\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\npolars = { version = \"0.48\", features = [\""
  },
  {
    "path": "rust/ob_core/benches/hot_paths.rs",
    "chars": 5963,
    "preview": "use criterion::{black_box, criterion_group, criterion_main, Criterion};\nuse polars::prelude::*;\n\nuse ob_core::entries::{"
  },
  {
    "path": "rust/ob_core/src/backtest.rs",
    "chars": 75561,
    "preview": "//! Full backtest loop — mirrors BacktestEngine.run() for parity.\n//!\n//! Pre-partitions all data by date at startup for"
  },
  {
    "path": "rust/ob_core/src/balance.rs",
    "chars": 6642,
    "preview": "//! Full _update_balance orchestration in Rust.\n//!\n//! Mirrors Python's BacktestEngine._update_balance: for a date rang"
  },
  {
    "path": "rust/ob_core/src/convexity_backtest.rs",
    "chars": 11666,
    "preview": "/// Backtest engine: monthly rebalance loop for tail hedge overlay.\n///\n/// Model: 100% invested in equity (SPY). Each m"
  },
  {
    "path": "rust/ob_core/src/convexity_scoring.rs",
    "chars": 5720,
    "preview": "/// Convexity ratio scoring: find cheapest tail protection per day.\n\npub struct DailyScore {\n    pub date_ns: i64,\n    p"
  },
  {
    "path": "rust/ob_core/src/cost_model.rs",
    "chars": 4134,
    "preview": "//! Transaction cost models for options and stocks.\n//!\n//! Mirrors Python's `options_portfolio_backtester.execution.cos"
  },
  {
    "path": "rust/ob_core/src/entries.rs",
    "chars": 4102,
    "preview": "//! Entry signal computation in Rust.\n//!\n//! Mirrors Python's _execute_option_entries:\n//! 1. Anti-join to exclude held"
  },
  {
    "path": "rust/ob_core/src/exits.rs",
    "chars": 3424,
    "preview": "//! Exit mask computation in Rust.\n//!\n//! Mirrors Python's _execute_option_exits:\n//! 1. Compute current option quotes "
  },
  {
    "path": "rust/ob_core/src/fill_model.rs",
    "chars": 3616,
    "preview": "//! Fill models — determine the execution price for trades.\n//!\n//! Mirrors Python's `options_portfolio_backtester.execu"
  },
  {
    "path": "rust/ob_core/src/filter.rs",
    "chars": 28234,
    "preview": "//! Filter expression parser and evaluator.\n//!\n//! Parses the pandas-eval query strings generated by the Python Filter "
  },
  {
    "path": "rust/ob_core/src/inventory.rs",
    "chars": 7306,
    "preview": "//! Inventory join — THE hot path.\n//!\n//! Mirrors the inner loop of Python's `_update_balance`:\n//!   inv_info.merge(op"
  },
  {
    "path": "rust/ob_core/src/lib.rs",
    "chars": 266,
    "preview": "pub mod types;\npub mod inventory;\npub mod balance;\npub mod filter;\npub mod entries;\npub mod exits;\npub mod stats;\npub mo"
  },
  {
    "path": "rust/ob_core/src/risk.rs",
    "chars": 5088,
    "preview": "//! Risk management — constraints checked before entering positions.\n//!\n//! Mirrors Python's `options_portfolio_backtes"
  },
  {
    "path": "rust/ob_core/src/signal_selector.rs",
    "chars": 6398,
    "preview": "//! Signal selectors — choose which contract to trade from candidates.\n//!\n//! Mirrors Python's `options_portfolio_backt"
  },
  {
    "path": "rust/ob_core/src/stats.rs",
    "chars": 27763,
    "preview": "//! Performance statistics computation.\n//!\n//! Comprehensive stats matching Python's BacktestStats: return metrics,\n//!"
  },
  {
    "path": "rust/ob_core/src/types.rs",
    "chars": 3779,
    "preview": "/// Core domain types mirroring Python's options_portfolio_backtester.core.types.\n\n#[derive(Debug, Clone, Copy, PartialE"
  },
  {
    "path": "rust/ob_python/Cargo.toml",
    "chars": 326,
    "preview": "[package]\nname = \"ob_python\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nname = \"_ob_rust\"\ncrate-type = [\"cdylib\"]\n\n[depen"
  },
  {
    "path": "rust/ob_python/src/arrow_bridge.rs",
    "chars": 511,
    "preview": "//! Arrow C Data Interface bridge: pyarrow <-> Polars zero-copy.\n//!\n//! Uses pyo3-polars for direct DataFrame conversio"
  },
  {
    "path": "rust/ob_python/src/lib.rs",
    "chars": 1647,
    "preview": "use pyo3::prelude::*;\n\nmod arrow_bridge;\nmod py_balance;\nmod py_backtest;\nmod py_convexity;\nmod py_filter;\nmod py_entrie"
  },
  {
    "path": "rust/ob_python/src/py_backtest.rs",
    "chars": 18380,
    "preview": "//! PyO3 bindings for full backtest loop.\n\nuse pyo3::prelude::*;\nuse pyo3::types::{PyDict, PyList};\nuse pyo3_polars::PyD"
  },
  {
    "path": "rust/ob_python/src/py_balance.rs",
    "chars": 2184,
    "preview": "//! PyO3 bindings for balance update.\n\nuse pyo3::prelude::*;\nuse pyo3_polars::PyDataFrame;\n\nuse ob_core::balance::{compu"
  },
  {
    "path": "rust/ob_python/src/py_convexity.rs",
    "chars": 5210,
    "preview": "use numpy::PyReadonlyArray1;\nuse pyo3::prelude::*;\nuse pyo3::types::PyDict;\n\nuse ob_core::convexity_scoring;\nuse ob_core"
  },
  {
    "path": "rust/ob_python/src/py_entries.rs",
    "chars": 1292,
    "preview": "//! PyO3 bindings for entry signal computation.\n\nuse pyo3::prelude::*;\nuse pyo3_polars::PyDataFrame;\n\nuse ob_core::entri"
  },
  {
    "path": "rust/ob_python/src/py_execution.rs",
    "chars": 5978,
    "preview": "//! PyO3 bindings for execution models: cost, fill, signal selection, risk.\n//!\n//! Exposes flat functions that call int"
  },
  {
    "path": "rust/ob_python/src/py_exits.rs",
    "chars": 821,
    "preview": "//! PyO3 bindings for exit mask computation.\n\nuse pyo3::prelude::*;\nuse polars::prelude::{NamedFrom, Series};\n\nuse ob_co"
  },
  {
    "path": "rust/ob_python/src/py_filter.rs",
    "chars": 1627,
    "preview": "//! PyO3 bindings for filter compilation and evaluation.\n\nuse pyo3::prelude::*;\nuse pyo3_polars::PyDataFrame;\n\nuse ob_co"
  },
  {
    "path": "rust/ob_python/src/py_stats.rs",
    "chars": 6121,
    "preview": "//! PyO3 bindings for stats computation.\n\nuse pyo3::prelude::*;\n\nuse ob_core::stats;\n\n/// Compute backtest statistics fr"
  },
  {
    "path": "rust/ob_python/src/py_sweep.rs",
    "chars": 12120,
    "preview": "//! Parallel grid sweep using Rayon with real run_backtest() per config.\n//!\n//! Receives options+stocks data as DataFra"
  },
  {
    "path": "setup.cfg",
    "chars": 213,
    "preview": "[mypy]\npython_version = 3.12\nwarn_unused_configs = True\ndisallow_untyped_defs = False\nignore_missing_imports = True\n# Ex"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/analytics/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/analytics/test_analytics_pbt.py",
    "chars": 17261,
    "preview": "\"\"\"Property-based tests for BacktestStats via Rust compute_full_stats.\n\nFuzzes the analytics pipeline with random balanc"
  },
  {
    "path": "tests/analytics/test_charts.py",
    "chars": 4681,
    "preview": "\"\"\"Tests for chart functions (weights_chart + Altair charts).\"\"\"\n\nfrom __future__ import annotations\n\nimport pandas as p"
  },
  {
    "path": "tests/analytics/test_optimization.py",
    "chars": 4113,
    "preview": "\"\"\"Tests for analytics/optimization.py — grid_sweep and walk_forward.\"\"\"\n\nimport pandas as pd\nimport numpy as np\n\nfrom o"
  },
  {
    "path": "tests/analytics/test_stats.py",
    "chars": 12176,
    "preview": "\"\"\"Tests for BacktestStats — including the fixed profit_factor.\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport pytest"
  },
  {
    "path": "tests/analytics/test_stats_python_path.py",
    "chars": 11183,
    "preview": "\"\"\"Tests for BacktestStats.from_balance — covers the Rust compute_full_stats\npath including period stats, lookback, turn"
  },
  {
    "path": "tests/analytics/test_summary.py",
    "chars": 4945,
    "preview": "\"\"\"Tests for analytics/summary.py — the legacy summary statistics function.\"\"\"\n\nimport numpy as np\nimport pandas as pd\n\n"
  },
  {
    "path": "tests/analytics/test_tearsheet.py",
    "chars": 6056,
    "preview": "from __future__ import annotations\n\nfrom unittest.mock import patch\nfrom pathlib import Path\n\nimport numpy as np\nimport "
  },
  {
    "path": "tests/analytics/test_trade_log.py",
    "chars": 4695,
    "preview": "\"\"\"Tests for structured TradeLog.\"\"\"\n\nimport numpy as np\nimport pandas as pd\n\nfrom options_portfolio_backtester.analytic"
  },
  {
    "path": "tests/bench/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/bench/_test_helpers.py",
    "chars": 16733,
    "preview": "\"\"\"Shared helpers for bench regression tests.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\n\nimport numpy as np\nimpo"
  },
  {
    "path": "tests/bench/extract_prod_slices.py",
    "chars": 7603,
    "preview": "#!/usr/bin/env python3\n\"\"\"Extract diverse time-period slices from raw parquet data for parity testing.\n\nReads raw option"
  },
  {
    "path": "tests/bench/generate_test_data.py",
    "chars": 8358,
    "preview": "\"\"\"Generate large deterministic synthetic datasets for 3-way parity tests.\n\nProduces stock and options CSVs with the sam"
  },
  {
    "path": "tests/bench/test_edge_cases.py",
    "chars": 3402,
    "preview": "\"\"\"Edge-case regression tests.\n\nEach test runs the backtest ONCE and checks invariants.\n\"\"\"\n\nfrom __future__ import anno"
  },
  {
    "path": "tests/bench/test_execution_models.py",
    "chars": 3921,
    "preview": "\"\"\"Regression tests for execution models (cost, fill, signal, risk, exits).\"\"\"\n\nfrom __future__ import annotations\n\nimpo"
  },
  {
    "path": "tests/bench/test_invariants.py",
    "chars": 4706,
    "preview": "\"\"\"Balance sheet and trade log invariants.\n\nTests run each backtest ONCE and verify structural invariants.\nCovers small,"
  },
  {
    "path": "tests/bench/test_multi_leg.py",
    "chars": 2392,
    "preview": "\"\"\"Multi-leg strategy regression tests.\n\nEach test runs the backtest ONCE and checks invariants.\n\"\"\"\n\nfrom __future__ im"
  },
  {
    "path": "tests/bench/test_partial_exits.py",
    "chars": 2970,
    "preview": "\"\"\"Regression tests for partial exit (sell_some_options) scenarios.\n\nHigh options allocation (85%) forces sell_some_opti"
  },
  {
    "path": "tests/bench/test_sweep.py",
    "chars": 6315,
    "preview": "\"\"\"Regression tests for Rust parallel_sweep API.\"\"\"\n\nimport pandas as pd\nimport pytest\n\ntry:\n    import polars as pl\n   "
  },
  {
    "path": "tests/compat/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/compat/test_bt_overlap_gate.py",
    "chars": 1339,
    "preview": "from __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\n\nfrom scripts.compare_with_bt import normali"
  },
  {
    "path": "tests/conftest.py",
    "chars": 298,
    "preview": "from __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\n\n\ndef pytest_collection_modifyitems(config, "
  },
  {
    "path": "tests/convexity/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/convexity/conftest.py",
    "chars": 2671,
    "preview": "\"\"\"Shared fixtures for convexity tests.\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom options_portfolio"
  },
  {
    "path": "tests/convexity/test_allocator.py",
    "chars": 1563,
    "preview": "\"\"\"Tests for allocation strategies.\"\"\"\n\nimport pytest\n\nfrom options_portfolio_backtester.convexity.allocator import (\n  "
  },
  {
    "path": "tests/convexity/test_backtest.py",
    "chars": 1990,
    "preview": "\"\"\"Tests for convexity backtest module.\"\"\"\n\nimport pandas as pd\nimport pytest\n\nfrom options_portfolio_backtester.convexi"
  },
  {
    "path": "tests/convexity/test_config.py",
    "chars": 773,
    "preview": "\"\"\"Tests for convexity config.\"\"\"\n\nfrom options_portfolio_backtester.convexity.config import (\n    BacktestConfig,\n    I"
  },
  {
    "path": "tests/core/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/core/test_types.py",
    "chars": 6429,
    "preview": "\"\"\"Tests for core domain types.\"\"\"\n\nfrom options_portfolio_backtester.core.types import (\n    Direction, OptionType, Ord"
  },
  {
    "path": "tests/core/test_types_pbt.py",
    "chars": 9949,
    "preview": "\"\"\"Property-based tests for core domain types.\n\nFuzzes Greeks algebra, Fill notional, Direction/Order/OptionType enum in"
  },
  {
    "path": "tests/data/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/data/test_filter.py",
    "chars": 3278,
    "preview": "\"\"\"Tests for Schema DSL: Field filter operations.\"\"\"\n\nfrom options_portfolio_backtester.data.schema import Field\n\n\ndef t"
  },
  {
    "path": "tests/data/test_property_based.py",
    "chars": 4159,
    "preview": "\"\"\"Property-based tests for Schema, Field, and Filter DSL.\"\"\"\n\nimport numpy as np\nimport pandas as pd\nfrom hypothesis im"
  },
  {
    "path": "tests/data/test_providers.py",
    "chars": 2631,
    "preview": "\"\"\"Tests for data providers.\"\"\"\n\nimport os\nimport pytest\nimport pandas as pd\n\nfrom options_portfolio_backtester.data.pro"
  },
  {
    "path": "tests/data/test_providers_extended.py",
    "chars": 8984,
    "preview": "\"\"\"Extended tests for data providers — accessors, iteration, edge cases.\"\"\"\n\nimport os\nimport pandas as pd\nimport pytest"
  },
  {
    "path": "tests/data/test_schema.py",
    "chars": 4226,
    "preview": "\"\"\"Tests for Schema, Field, and Filter DSL.\"\"\"\n\nimport pandas as pd\nimport pytest\n\nfrom options_portfolio_backtester.dat"
  },
  {
    "path": "tests/engine/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/engine/test_algo_adapters.py",
    "chars": 9021,
    "preview": "from __future__ import annotations\n\nimport warnings\n\nimport pandas as pd\nimport pytest\n\nfrom options_portfolio_backteste"
  },
  {
    "path": "tests/engine/test_capital_conservation.py",
    "chars": 25658,
    "preview": "\"\"\"Capital conservation invariant: no money should be created or destroyed.\n\nAt every row in the balance sheet:\n    cash"
  },
  {
    "path": "tests/engine/test_chaos.py",
    "chars": 11760,
    "preview": "\"\"\"Chaos / fault-injection tests — corrupted and adversarial data.\n\nFeed corrupted data through the engine. Assert: eith"
  },
  {
    "path": "tests/engine/test_clock.py",
    "chars": 3598,
    "preview": "\"\"\"Tests for TradingClock — date iteration and rebalance scheduling.\"\"\"\n\nimport pandas as pd\nimport numpy as np\n\nfrom op"
  },
  {
    "path": "tests/engine/test_engine.py",
    "chars": 5419,
    "preview": "\"\"\"Tests for BacktestEngine — verifies regression values and engine behavior.\"\"\"\n\nimport os\nimport pytest\nimport numpy a"
  },
  {
    "path": "tests/engine/test_engine_deep.py",
    "chars": 31718,
    "preview": "\"\"\"Deep engine tests — multi-strategy, options_budget, SMA gating, monthly mode,\ncapital flow invariants, event logging,"
  },
  {
    "path": "tests/engine/test_engine_unit.py",
    "chars": 3413,
    "preview": "\"\"\"Unit tests for BacktestEngine internals — repr, metadata, static methods.\"\"\"\n\nimport json\n\nfrom options_portfolio_bac"
  },
  {
    "path": "tests/engine/test_full_liquidation.py",
    "chars": 7820,
    "preview": "\"\"\"Tests for option rebalance accounting.\n\nVerifies that at every rebalance:\n1. Exit filters run on held positions (posi"
  },
  {
    "path": "tests/engine/test_max_notional.py",
    "chars": 6261,
    "preview": "\"\"\"Tests for max_notional_pct engine parameter.\"\"\"\n\nimport os\n\nfrom options_portfolio_backtester.engine.engine import Ba"
  },
  {
    "path": "tests/engine/test_multi_strategy.py",
    "chars": 5542,
    "preview": "\"\"\"Tests for MultiStrategyEngine.\"\"\"\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom options_portf"
  },
  {
    "path": "tests/engine/test_multi_strategy_engine.py",
    "chars": 13817,
    "preview": "\"\"\"Tests for multi-strategy support within BacktestEngine.\n\nVerifies that add_strategy() + run() produces correct result"
  },
  {
    "path": "tests/engine/test_per_leg_overrides.py",
    "chars": 6978,
    "preview": "\"\"\"Tests for per-leg signal_selector and fill_model overrides.\"\"\"\n\nimport os\nimport numpy as np\nimport pandas as pd\nimpo"
  },
  {
    "path": "tests/engine/test_pipeline.py",
    "chars": 70759,
    "preview": "from __future__ import annotations\n\nimport pandas as pd\nimport numpy as np\n\nfrom options_portfolio_backtester.engine.pip"
  },
  {
    "path": "tests/engine/test_portfolio_integration.py",
    "chars": 4350,
    "preview": "\"\"\"Tests verifying Portfolio dataclass is kept in sync with legacy MultiIndex inventory.\"\"\"\n\nimport os\nimport pytest\nimp"
  },
  {
    "path": "tests/engine/test_regression_snapshots.py",
    "chars": 6468,
    "preview": "\"\"\"Regression snapshot tests — lock backtest outputs against golden values.\n\nRun a full backtest with fixed data + deter"
  },
  {
    "path": "tests/engine/test_risk_wiring.py",
    "chars": 3838,
    "preview": "\"\"\"Tests that RiskManager is actually wired into the engine.\"\"\"\n\nimport os\nimport pytest\nimport numpy as np\n\nfrom option"
  },
  {
    "path": "tests/engine/test_rust_parity.py",
    "chars": 16168,
    "preview": "\"\"\"Rust vs Python numerical parity: run same strategy both paths, compare values.\n\nWhen the Rust extension is available,"
  },
  {
    "path": "tests/engine/test_signal_selector_wiring.py",
    "chars": 3473,
    "preview": "\"\"\"Tests that SignalSelector is actually wired into the engine.\n\nAll execution goes through Rust, so we verify via stand"
  },
  {
    "path": "tests/engine/test_strategy_tree.py",
    "chars": 7071,
    "preview": "from __future__ import annotations\n\nimport pytest\n\nfrom options_portfolio_backtester.engine.strategy_tree import Strateg"
  },
  {
    "path": "tests/execution/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/execution/test_cost_model.py",
    "chars": 3497,
    "preview": "\"\"\"Tests for transaction cost models.\"\"\"\n\nfrom options_portfolio_backtester.execution.cost_model import (\n    NoCosts, P"
  },
  {
    "path": "tests/execution/test_execution_deep.py",
    "chars": 13329,
    "preview": "\"\"\"Deep execution model tests — fill models, cost models, signal selectors, sizers.\n\nTests edge cases, boundary conditio"
  },
  {
    "path": "tests/execution/test_execution_pbt.py",
    "chars": 24538,
    "preview": "\"\"\"Property-based tests for execution models — cost, fill, sizer, selector.\n\nUses Hypothesis to fuzz all execution compo"
  },
  {
    "path": "tests/execution/test_fill_model.py",
    "chars": 1606,
    "preview": "\"\"\"Tests for fill models.\"\"\"\n\nimport pandas as pd\n\nfrom options_portfolio_backtester.core.types import Direction\nfrom op"
  },
  {
    "path": "tests/execution/test_rust_parity_execution.py",
    "chars": 14189,
    "preview": "\"\"\"Rust execution model tests: edge cases, invariants, PBT, integration.\n\nCovers:\n- Edge-case fuzzing (NaN, Inf, empty, "
  },
  {
    "path": "tests/execution/test_signal_selector.py",
    "chars": 1582,
    "preview": "\"\"\"Tests for signal selectors.\"\"\"\n\nimport pandas as pd\n\nfrom options_portfolio_backtester.execution.signal_selector impo"
  },
  {
    "path": "tests/execution/test_sizer.py",
    "chars": 1510,
    "preview": "\"\"\"Tests for position sizing models.\"\"\"\n\nfrom options_portfolio_backtester.execution.sizer import (\n    CapitalBased, Fi"
  },
  {
    "path": "tests/portfolio/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/portfolio/test_greeks_aggregation.py",
    "chars": 2471,
    "preview": "\"\"\"Tests for portfolio-level Greeks aggregation.\"\"\"\n\nfrom options_portfolio_backtester.core.types import (\n    Direction"
  },
  {
    "path": "tests/portfolio/test_portfolio.py",
    "chars": 3750,
    "preview": "\"\"\"Tests for Portfolio class.\"\"\"\n\nfrom options_portfolio_backtester.core.types import Direction, OptionType, Order, Gree"
  },
  {
    "path": "tests/portfolio/test_position.py",
    "chars": 3896,
    "preview": "\"\"\"Tests for option position and position leg.\"\"\"\n\nfrom options_portfolio_backtester.core.types import Direction, Option"
  },
  {
    "path": "tests/portfolio/test_property_based.py",
    "chars": 5118,
    "preview": "\"\"\"Property-based tests for portfolio position and portfolio invariants.\"\"\"\n\nfrom hypothesis import given, settings, ass"
  },
  {
    "path": "tests/portfolio/test_risk.py",
    "chars": 2475,
    "preview": "\"\"\"Tests for risk management.\"\"\"\n\nfrom options_portfolio_backtester.core.types import Greeks\nfrom options_portfolio_back"
  },
  {
    "path": "tests/strategy/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/strategy/test_presets.py",
    "chars": 5616,
    "preview": "\"\"\"Tests for strategy preset constructors.\"\"\"\n\nimport math\n\nfrom options_portfolio_backtester.core.types import Directio"
  },
  {
    "path": "tests/strategy/test_strangle.py",
    "chars": 1713,
    "preview": "\"\"\"Tests for Strangle preset strategy.\"\"\"\n\nimport pytest\n\nfrom options_portfolio_backtester.strategy.presets import Stra"
  },
  {
    "path": "tests/strategy/test_strategy.py",
    "chars": 3544,
    "preview": "\"\"\"Tests for Strategy class: adding/removing legs, thresholds.\"\"\"\n\nimport math\n\nimport pytest\nimport numpy as np\nimport "
  },
  {
    "path": "tests/strategy/test_strategy_deep.py",
    "chars": 21376,
    "preview": "\"\"\"Deep strategy & risk tests — presets, multi-leg construction, portfolio, positions, Greeks.\n\nTests strategy construct"
  },
  {
    "path": "tests/strategy/test_strategy_leg.py",
    "chars": 3596,
    "preview": "\"\"\"Tests for StrategyLeg: entry/exit filters, custom filters.\"\"\"\n\nimport pandas as pd\n\nfrom options_portfolio_backtester"
  },
  {
    "path": "tests/strategy/test_strategy_pbt.py",
    "chars": 22189,
    "preview": "\"\"\"Property-based tests for strategies, risk constraints, and Greeks algebra.\n\nFuzzes strategy preset construction, risk"
  },
  {
    "path": "tests/test_cleanup.py",
    "chars": 3144,
    "preview": "\"\"\"Tests for post-refactor cleanup — verify dead code removed, imports correct.\"\"\"\n\nimport importlib\n\n\ndef test_top_leve"
  },
  {
    "path": "tests/test_data/ivy_5assets_data.csv",
    "chars": 39319,
    "preview": ",symbol,date,close,high,low,open,volume,adjClose,adjHigh,adjLow,adjOpen,adjVolume,divCash,splitFactor\n1246,VTI,2014-12-1"
  },
  {
    "path": "tests/test_data/ivy_portfolio.csv",
    "chars": 94247,
    "preview": "symbol,date,close,high,low,open,volume,adjClose,adjHigh,adjLow,adjOpen,adjVolume,divCash,splitFactor\nVTI,2017-01-03,116."
  },
  {
    "path": "tests/test_data/options_data.csv",
    "chars": 35103,
    "preview": ",underlying,underlying_last,optionroot,type,expiration,quotedate,strike,last,bid,ask,volume,openinterest,impliedvol,delt"
  },
  {
    "path": "tests/test_data/test_data_options.csv",
    "chars": 54396,
    "preview": "underlying,underlying_last, exchange,optionroot,optionext,type,expiration,quotedate,strike,last,bid,ask,volume,openinter"
  },
  {
    "path": "tests/test_data/test_data_stocks.csv",
    "chars": 128331,
    "preview": "symbol,date,close,high,low,open,volume,adjClose,adjHigh,adjLow,adjOpen,adjVolume,divCash,splitFactor\nVOO,2017-01-03,206."
  },
  {
    "path": "tests/test_deep_analytics_convexity.py",
    "chars": 22306,
    "preview": "\"\"\"Deep analytics, convexity, dispatch, and data provider tests.\n\nCovers:\n- BacktestStats edge cases (empty, single-row,"
  },
  {
    "path": "tests/test_intrinsic_sign.py",
    "chars": 4830,
    "preview": "\"\"\"Tests that intrinsic-value fallback produces correct sign in _current_options_capital.\n\nBUY legs are assets  → positi"
  },
  {
    "path": "tests/test_intrinsic_value.py",
    "chars": 1889,
    "preview": "\"\"\"Tests for intrinsic value fallback when options expire/go missing.\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport "
  },
  {
    "path": "tests/test_property_based.py",
    "chars": 13845,
    "preview": "\"\"\"Property-based and fuzz tests for core components.\n\nUses hypothesis to generate random inputs and verify invariants h"
  },
  {
    "path": "tests/test_smoke.py",
    "chars": 1704,
    "preview": "\"\"\"Smoke tests — verify all public imports work.\"\"\"\n\n\ndef test_top_level_imports():\n    \"\"\"All public symbols importable"
  }
]

About this extraction

This page contains the full source code of the lambdaclass/options_backtester GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 181 files (1.5 MB), approximately 507.2k tokens, and a symbol index with 2830 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!