197 lines
6.8 KiB
Python
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
|