Seven explained Jupyter notebooks and a command-line research tool for US stock and diversified ETF options. Capital is configurable; the default maximum portfolio loss budget is 20%. All amounts are USD. This version analyzes a new cash allocation, not an existing broker portfolio.
Strategies: cash-secured puts, covered calls including the share purchase, protective puts and same-expiration collars. No margin borrowing, uncovered short options, order submission or automatic rolling. A 20% loss budget constrains modeled expiration losses; it does not guarantee a 20% maximum running drawdown.
Run from this project directory. The core CLI uses only the Python standard library (Python 3.11+):
# Fictional prices: learn the output without any network access.
python3 scripts/options.py demo --capital 100000 --max-portfolio-loss 0.20
# Test the credentials already in .env without printing them.
python3 scripts/options.py audit
# Install Jupyter and the optional IBKR quote adapter using uv.
uv sync --extra notebooks --extra brokers
# Open the seven notebooks. Every code cell has an explanatory Markdown cell.
uv run --extra notebooks --extra brokers jupyter lab notebooksreports/demo.md, .csv and .json contain the example analysis. Synthetic data always returns DO_NOT_INVEST, while still displaying candidate economics. Capital of $100,000 is an illustration, not a suggested allocation. The CLI requires --capital.
If uv is unavailable, use a virtual environment and python -m pip install -e '.[notebooks,brokers]'.
# Massive options plus Alpaca underlying quotes and exchange clock.
python3 scripts/options.py scan --capital 100000 --provider massive \
--symbols SPY IWM --max-portfolio-loss 0.20 --output reports/daily
# Alpaca options plus Alpaca underlying quotes and clock.
python3 scripts/options.py scan --capital 100000 --provider alpaca \
--option-feed opra --stock-feed sip --output reports/alpaca
# IBKR stock/option quotes; still uses Alpaca's clock to check market hours.
.venv/bin/python scripts/options.py scan --capital 100000 --provider ibkr \
--symbols SPY IWM --output reports/ibkr
# A custom risk/cost scenario; percentages use fractions.
python3 scripts/options.py scan --capital 75000 --max-portfolio-loss 0.10 \
--max-symbol-allocation 0.20 --max-group-allocation 0.40 \
--cash-reserve 0.20 --min-dte 30 --max-dte 45 \
--expected-return 0.00 --volatility 0.30 --cash-rate 0.03 \
--fee-per-contract 0.65 --strategies collar cash_secured_put
# Inspect a previously captured snapshot. Current freshness gates still apply.
python3 scripts/options.py analyze --capital 100000 \
--snapshot data/snapshots/YOUR_SNAPSHOT.json --output reports/recheckProvider failures, closed markets, stale data, unavailable event review, insufficient capital or insufficient modeled edge can all produce DO_NOT_INVEST. Nothing switches silently to invented, delayed or indicative prices to make a recommendation. Even one collection error blocks portfolio selection, rather than implying that an incomplete universe was fully evaluated.
Read config/universe.json before a live decision. The starting entries contain unknown event information, intentionally blocking candidate eligibility. Review the underlying mandate, earnings (stocks), distributions/ex-dividend date, and corporate actions using current issuer/broker information. Set:
eligible: true only for a suitable unleveraged stock or diversified unleveraged ETF.risk_group: a shared group for correlated exposures; SPY and IWM both useus_equity.events_reviewed_through: the last date covered by your event review, at least the option expiration, inYYYY-MM-DDformat.earnings_date: next known earnings date, or null when none occurs in the reviewed period.ex_dividend_date: next known ex-dividend date, or null when none occurs in the reviewed period.corporate_action_pending: false only after checking that no relevant action is pending.
Unknown is not equivalent to no event. In this initial method, any earnings or ex-dividend event in the holding period excludes the contract. No paid event-calendar entitlement is assumed. This manual input is part of the documented method; it is not automatically populated by the script. Do not extend the review date just to make a trade pass.
Each scan writes a timestamped normalized snapshot to data/snapshots/, a frozen paper report to data/paper_reports/, and three review files under the output prefix:
| File | Contents |
|---|---|
.md |
Decision and reasons, proposed lots/shares and option legs, funding, assignment cash, maximum loss, candidate comparison and assumptions |
.json |
Every candidate, prices/timestamps/feed, direction, expiry/strike, modeled breakeven, maximum profit/loss, scenario probability and expected P/L, tail estimate, stress P/L, selected portfolio and rejected contracts |
.csv |
One row per candidate for spreadsheet comparison |
DO_NOT_INVEST means do not open a position from this report. REVIEW_CANDIDATES means the specified inputs passed the model's gates and there are positions to review manually. It does not mean profitability has been established. Unselected candidates are alternatives; summing them can break funding and loss limits.
The model uses buy-at-ask/sell-at-bid with extra slippage and cost reserves. Prices are analytical assumptions, not broker-ready guaranteed limit prices. Verify contract deliverables, price increments, commissions, assignment/exercise deadlines and current quotes at the broker. Preserve stock coverage and protective legs during entry and exit. Protection expires; this program does not manage that process for you.
All apply to scan, analyze and demo. Rates/allocations are decimals, not percentage points.
| Parameter | Default | Meaning |
|---|---|---|
--capital |
Required | New USD cash allocation; excludes other holdings |
--max-portfolio-loss |
0.20 | Maximum sum of selected positions' modeled expiration losses / capital |
--max-symbol-allocation |
0.25 | Maximum fully reserved capital for one symbol / total capital |
--max-group-allocation |
0.50 | Maximum funding for correlated symbols in the same risk group |
--cash-reserve |
0.10 | Fraction of capital that cannot be committed |
--min-dte / --max-dte |
21 / 60 | Calendar days to expiration; allowed range 1–730 |
--max-quote-age |
120 | Maximum quote and clock age in seconds; future quotes over 5s are rejected |
--max-quote-skew |
30 | Maximum separation between underlying and option quote timestamps, seconds |
--max-spread-pct |
0.15 | Maximum bid/ask spread divided by midpoint, including underlying quotes |
--min-open-interest |
100 | Minimum open interest; missing values rejected even at zero threshold |
--fee-per-contract |
0.65 | USD per option contract per side; reserve entry and closing fees |
--stock-slippage-bps |
2 | Extra share purchase cost in basis points above ask |
--option-slippage |
0.01 | Additional adverse fill allowance, USD per option share per entry leg |
--exit-cost-per-share |
0.02 | Dollar cost reserve per share, multiplied by 100 for every strategy lot |
--expected-return |
0.00 | Assumed annual continuously modeled stock drift; not estimated alpha |
--volatility |
0.25 | Assumed annual lognormal volatility for every underlying; sensitivity input |
--cash-rate |
0.00 | Assumed simple annual cash comparison yield; enter an appropriate available net yield |
--min-edge |
0.001 | Minimum model expected P/L above cash benchmark, divided by committed capital, per holding period |
--strategies |
All four | Space-separated collar cash_secured_put covered_call protective_put |
--output |
reports/<command> |
File prefix, without extension; repeated runs replace these review files |
--capital 100000 --max-portfolio-loss 0.20 sets a $20,000 loss budget. It is not a target to lose or spend $20,000. Standard 100-share lots may prevent investment at small capital sizes. Size is also capped at the displayed quote size; displayed liquidity can disappear.
Trading dates, DTE and event windows use America/New_York, including daylight saving time. Timestamps remain timezone-aware; archives use UTC filenames.
Scan-only arguments: --provider massive|alpaca|ibkr (default massive), --symbols SPY IWM, --universe config/universe.json, --option-feed opra|indicative, --stock-feed sip|iex|delayed_sip (otherwise .env's ALPACA_DATA_FEED, default sip), and optional --snapshot-output PATH. The indicative/IEX/delayed choices support inspecting restricted feeds but cannot pass the consolidated real-time gate. IBKR ignores Alpaca feed arguments for quotes. Global --env PATH must precede the command, e.g. python3 scripts/options.py --env .env audit.
Default return/volatility/rate settings are scenarios, not forecasts. The observed FRED rate is reported by audit but never silently replaces your cash yield. Strategy P/L does not credit collateral interest or dividends. This is conservative relative to a positive cash benchmark, but not a complete broker cash-flow model. A 0% benchmark can be inappropriate; set it deliberately.
| Notebook | Method explained |
|---|---|
| 00 data access | Access, normalized quotes, feed status, data rejection |
| 01 portfolio constraints | Capital, 20% default, cash reserve, funding and capital sensitivity |
| 02 screening | Universe, liquidity, events, Greeks as descriptive data |
| 03 strategy comparison | Costed payoffs, plots, breakevens, return assumptions |
| 04 stress and allocation | Crashes, assumption sensitivity and shared portfolio constraints |
| 05 historical validation | Observed history, frozen reports, non-overlapping paper expiration evaluation |
| 06 daily report | Same engine as CLI, decision interpretation and exports |
Every computation has a preceding explanation of its purpose, assumptions and interpretation. Notebooks are standalone and default to demo mode. Edit the initial parameter cell or set environment variables before Jupyter:
OPTIONS_MODE=live OPTIONS_CAPITAL=75000 OPTIONS_MAX_LOSS=0.20 \
OPTIONS_PROVIDER=ibkr OPTIONS_SYMBOLS=SPY,IWM \
uv run --extra notebooks --extra brokers jupyter lab notebooksOther Policy fields can be edited in that initial cell. The option Greeks supplied by providers are displayed; no delta-as-win-probability rule is used. Descriptive underlying volatility is not automatically fitted into the ranking model.
Every chart has an explanation immediately before its code cell. Running a notebook displays the figures and exports PNG and SVG copies under reports/charts/<notebook>/<UTC run timestamp>/. Separate run directories preserve the distinction between older and current decisions.
| Notebook | Charts and what to look for |
|---|---|
| 00 | Feed coverage and quote-age ranges: whether the inputs are current enough to use |
| 01 | Per-position funding and worst-loss bars against your dollar limits: whether one lot fits |
| 02 | Strike/DTE/open-interest map and spread-versus-open-interest scatter: coverage and liquid parts of the chain |
| 03 | Individual position payoffs with green/red profit/loss regions, strikes, breakeven, maximum loss/profit and funding; model P/L histogram; 50%/90% model price intervals through expiry |
| 04 | Underlying-shock P/L heatmap, drift/volatility sensitivity heatmap and aggregate budget use |
| 05 | Real adjusted-close history with 20/50-session averages, drawdown and 20-session realized volatility; paper cohort comparison only when settled data exists |
| 06 | Actual proposed allocation and lot-scaled payoff charts for every selected position |
In notebook 03, edit CHART_CANDIDATE_IDS to inspect exact alternatives; the empty default shows up to eight representative symbol/strategy combinations. DISTRIBUTION_POSITION_INDEX chooses the position for the model distribution and price-interval charts. In notebook 04, SENSITIVITY_POSITION_INDEX selects the assumption heatmap position. These controls change displayed examples, not strategy selection.
The trend dashboard needs real history; the saved example contains 255 Tiingo SPY observations. Set HISTORY_SYMBOL to match a custom saved file. Missing historical or settled paper data produces an explanation instead of a fabricated performance chart. Model intervals are not guaranteed trading ranges, and all payoff charts describe expiration rather than an early exit.
# Download underlying history for descriptive analysis.
python3 scripts/options.py history --symbol SPY --start 2025-09-01 \
--end 2026-09-04 --output data/underlying_history.json
# Copy the CSV header from examples/settlements.csv to data/settlements.csv,
# then supply independently verified RAW underlying closes at expiration.
python3 scripts/options.py evaluate --reports data/paper_reports/*.json \
--settlements data/settlements.csv --output reports/expiry_evaluationThe settlement CSV columns are symbol,expiry,underlying_close. Supply the contractual expiration date and the corresponding raw underlying settlement/closing price. Do not substitute adjusted close or a different date without verifying the settlement convention. Nonstandard expirations or corporate actions need a fuller model.
The evaluator skips incomplete or overlapping cohorts so the same capital is not reused. It calculates expiration P/L, return on all allocated capital and cash/underlying comparisons. It does not manufacture a historical options backtest from stock prices. With no settled, frozen reports it returns INSUFFICIENT_DATA.
Current limitations: no early-assignment path simulation, actual fill ledger, intra-period drawdown, taxes, dividends, cash-interest credit, automatic stock inventory management or wheel backtest. Expiration liquidation is assumed. Results use each cohort's stated starting capital and are not compounded. Establishing a historical edge requires point-in-time option quotes and an execution/assignment cash-flow model beyond this first evaluator. The notebooks state these limits alongside the relevant steps.
For repeatable daily collection, run the scan command from your scheduler during regular US trading hours. The broker clock catches closures and holidays. Scans archive separate timestamped inputs/results; the human-readable prefix is overwritten for convenience. No scheduled job is installed automatically.
.env is loaded without shell evaluation and never printed. Existing environment variables take precedence. .env, collected data, reports and the virtual environment are ignored by git. Copy .env.example only for a new installation; do not overwrite your existing .env.
- Alpaca: option snapshot pagination plus contract metadata and open interest. Explicit OPRA/SIP entitlement required for recommendations.
ALPACA_PAPER=true(default) selects the paper endpoint for clock/contract metadata; set false only if your API key is for the live endpoint. Both paths make GET requests only. - Massive: paginated option chains. Quotes and real-time status depend on subscription; missing fields fail screening. Uses Alpaca for underlying quotes and market clock.
- IBKR: optional
ib_asyncadapter; uses configured host, port and nonzero client ID. Always connects read-only with account synchronization disabled. The gateway must already be running and permit API connections. Samples the two nearest eligible expirations and up to four put/call strikes around 95%/105% of spot, rather than requesting an unbounded chain. Requests streaming quotes and open interest; releases subscriptions on completion. Quote age is based on receipt of both bid and ask updates, because this API does not provide exchange timestamps for these ticks. Delayed/frozen quotes and missing bid/ask timestamps fail screening. Requires the relevant TWS subscriptions and modern share-size reporting.IBKR_MODEis descriptive; the gateway/port selects the actual session.IBKR_READ_ONLYcannot enable trading in this project. - FRED: recent DGS3MO observation for benchmark context, not a guaranteed broker cash rate.
- Tiingo: underlying daily history, not options history.
The provider audit checks IBKR TCP connectivity only. A scan --provider ibkr actually checks the quote path. Endpoint accessibility alone never establishes real-time entitlement.
python3 -m unittest discover -s tests -v
.venv/bin/python scripts/check_notebooks.pyThe notebook checker starts real kernels, executes all seven notebooks in demo mode, and saves executed copies under reports/executed_notebooks/. It also checks that every code cell is preceded by Markdown. Notebook sources are generated by scripts/build_notebooks.py; update that source before regenerating them.
See methodology and primary sources for payoff equations, allocation logic and data documentation.