chart/tests/test_es_options.py

197 lines
6.8 KiB
Python

from datetime import date, datetime
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api.routes import router as api_router
from app.config import Settings
from app.runtime import Runtime
from app.market.es_options import (
api_symbol,
attach_deltas,
black76_delta,
daily_root,
filter_contracts,
from_tos_symbol,
monthly_root,
nearby_expirations,
parse_option_quote,
search_from_quotes,
tos_symbol,
)
def test_weekday_roots_match_the_verified_august_week():
assert daily_root(date(2026, 8, 14)) == "EW2Q26"
assert daily_root(date(2026, 8, 17)) == "E3AQ26"
assert daily_root(date(2026, 8, 18)) == "E3BQ26"
assert daily_root(date(2026, 8, 19)) == "E3CQ26"
assert daily_root(date(2026, 8, 20)) == "E3DQ26"
assert daily_root(date(2026, 8, 21)) == "EW3Q26"
def test_monthly_uses_es_root_only_on_quarterlies():
assert monthly_root(date(2026, 8, 21)) == "EW3Q26"
assert monthly_root(date(2026, 9, 18)) == "ESU26"
def test_tos_mapping_strips_and_restores_exchange_suffix():
assert from_tos_symbol("./E3AQ26P7780:XCME") == "./E3AQ26P7780"
assert tos_symbol("./E3AQ26P7780") == "./E3AQ26P7780:XCME"
assert api_symbol("E3AQ26", "P", 7780) == "./E3AQ26P7780"
def test_dropdown_for_friday_august_14():
rows = nearby_expirations(date(2026, 8, 14))
assert [(row.kind, row.date, row.root) for row in rows] == [
("daily", date(2026, 8, 14), "EW2Q26"),
("daily", date(2026, 8, 17), "E3AQ26"),
("daily", date(2026, 8, 18), "E3BQ26"),
("weekly", date(2026, 8, 14), "EW2Q26"),
("monthly", date(2026, 8, 21), "EW3Q26"),
]
def test_dropdown_skips_the_weekend():
rows = nearby_expirations(date(2026, 8, 15))
assert [row.date for row in rows if row.kind == "daily"] == [
date(2026, 8, 17),
date(2026, 8, 18),
date(2026, 8, 19),
]
assert rows[-1].root == "EW3Q26"
assert rows[-1].date == date(2026, 8, 21)
def test_monthly_rolls_to_the_september_es_root_after_the_august_third_friday():
rows = nearby_expirations(date(2026, 8, 22))
monthly = rows[-1]
assert monthly.kind == "monthly"
assert monthly.date == date(2026, 9, 18)
assert monthly.root == "ESU26"
def test_abs_delta_filter_keeps_the_015_to_025_band():
rows = [
{"strike": 7700, "mark": 3.0, "abs_delta": 0.05},
{"strike": 7770, "mark": 6.8, "abs_delta": 0.16},
{"strike": 7780, "mark": 8.3, "abs_delta": 0.206},
{"strike": 7850, "mark": 22.0, "abs_delta": 0.40},
]
kept = filter_contracts(rows, "delta", 0.15, 0.25)
assert [row["strike"] for row in kept] == [7770, 7780]
def test_price_filter_uses_mark():
rows = [
{"strike": 7765, "mark": 6.2, "abs_delta": 0.14},
{"strike": 7780, "mark": 8.3, "abs_delta": 0.21},
{"strike": 7790, "mark": 10.1, "abs_delta": 0.26},
]
kept = filter_contracts(rows, "price", 6.0, 8.0)
assert [row["strike"] for row in kept] == [7765]
def test_black76_put_delta_is_negative_and_increases_toward_atm():
otm = black76_delta(7827, 7780, 3 / 365.25, 0.0855, "P")
nearer = black76_delta(7827, 7800, 3 / 365.25, 0.0855, "P")
assert otm is not None and nearer is not None
assert otm < 0 and nearer < 0
assert abs(nearer) > abs(otm)
def test_search_filters_parsed_quotes_by_mark():
quotes = {
"./E3AQ26P7765": {
"assetMainType": "FUTURE_OPTION",
"quote": {"mark": 6.2, "bidPrice": 6.0, "askPrice": 6.2, "totalVolume": 43, "openInterest": 437},
"reference": {"strikePrice": 7765, "contractType": "P", "description": "./E3AQ26P7765:XCME"},
},
"./E3AQ26P7825": {
"assetMainType": "FUTURE_OPTION",
"quote": {"mark": 22.0, "bidPrice": 21.8, "askPrice": 22.0, "totalVolume": 10, "openInterest": 20},
"reference": {"strikePrice": 7825, "contractType": "P", "description": "./E3AQ26P7825:XCME"},
},
"errors": {"invalidSymbols": ["./E3AQ26P9999"]},
}
result = search_from_quotes(
day=date(2026, 8, 17),
side="P",
mode="price",
low=6,
high=8,
forward=7827,
quotes=quotes,
now=datetime(2026, 8, 14, 10, 0),
)
assert [row["tos"] for row in result["contracts"]] == ["./E3AQ26P7765:XCME"]
assert result["contracts"][0]["mark"] == 6.2
def test_equity_payload_is_not_treated_as_a_futures_option():
assert parse_option_quote("ES", {"assetMainType": "EQUITY", "quote": {"mark": 72}, "reference": {}}) is None
def test_attach_deltas_labels_the_015_band_from_live_shaped_marks():
contracts = [
{"strike": 7770.0, "mark": 6.8},
{"strike": 7780.0, "mark": 8.3},
{"strike": 7825.0, "mark": 22.0},
]
ranked = attach_deltas(contracts, 7827.0, 3 / 365.25, "P", 0.0855)
band = filter_contracts(ranked, "delta", 0.15, 0.25)
assert [row["strike"] for row in band] == [7770.0, 7780.0]
def _client(tmp_path):
app = FastAPI()
app.include_router(api_router)
app.state.runtime = Runtime(Settings(manual_lines_path=tmp_path / "lines.json"))
return TestClient(app)
def test_expirations_endpoint_needs_no_schwab(tmp_path):
response = _client(tmp_path).get("/api/es-options/expirations")
assert response.status_code == 200
rows = response.json()["expirations"]
assert len(rows) == 5
assert [row["kind"] for row in rows] == ["daily", "daily", "daily", "weekly", "monthly"]
assert all(row["root"] and row["date"] and row["label"] for row in rows)
def test_search_endpoint_uses_the_injected_snapshot(tmp_path, monkeypatch):
def fake_search(settings, **kwargs):
assert kwargs["root"] == "E3AQ26"
assert kwargs["side"] == "P"
assert kwargs["mode"] == "price"
return {
"underlying": "/ESU26",
"underlying_price": 7827.0,
"iv": 0.085,
"delta_approx": True,
"contracts": [
{
"symbol": "./E3AQ26P7765",
"tos": "./E3AQ26P7765:XCME",
"strike": 7765,
"mark": 6.2,
"abs_delta": 0.139,
}
],
}
monkeypatch.setattr("app.api.routes.run_search", fake_search)
response = _client(tmp_path).get(
"/api/es-options/search",
params={"date": "2026-08-17", "root": "E3AQ26", "side": "P", "mode": "price", "min": 6, "max": 8},
)
assert response.status_code == 200
assert response.json()["contracts"][0]["tos"] == "./E3AQ26P7765:XCME"
def test_search_rejects_a_bad_date(tmp_path):
response = _client(tmp_path).get(
"/api/es-options/search",
params={"date": "17-08-2026", "root": "E3AQ26", "min": 0.15, "max": 0.25},
)
assert response.status_code == 400