"""Dataclass config, colors, defaults.""" from dataclasses import dataclass, field from typing import Dict, List @dataclass class GexConfig: # --- data source --- cboe_url_a: str = "https://cdn.cboe.com/api/global/delayed_quotes/options/{symbol}.json" cboe_url_b: str = "https://cdn.cboe.com/api/global/delayed_quotes/options/_{symbol}.json" user_agent: str = "Mozilla/5.0 (compatible; gex-snapshot)" retries: int = 3 backoff_base: float = 2.0 # seconds; delays = base * 2^attempt # --- tickers (FIX 34) --- # Adding a ticker requires appending ONE string here — nothing else. # FIX 96: added NVDA, GOOGL, AAPL (single-name equities; plain Cboe endpoint, # instrument_class equity_etf, 16:00 ET settlement — all auto-derived). tickers: List[str] = field(default_factory=lambda: ["NDX", "SPY", "SMH", "NVDA", "GOOGL", "AAPL"]) default_ticker: str = "NDX" # Endpoint cache: data/endpoint_map.json records which URL variant worked per symbol. endpoint_map_path: str = "data/endpoint_map.json" # Contract-specification overrides ONLY (multiplier, settlement style). # Never tuning parameters. Ship empty. contract_spec_overrides: Dict[str, Dict] = field(default_factory=dict) # Display labels — purely presentational, never affects computation. display_labels: Dict[str, str] = field(default_factory=lambda: { "NDX": "NDX (Nasdaq-100)", "SPY": "SPY (S&P 500 ETF)", "SMH": "SMH (Semiconductor ETF)", "NVDA": "NVDA (Nvidia)", "GOOGL": "GOOGL (Alphabet)", "AAPL": "AAPL (Apple)", }) # --- filtering --- dte_max: int = 365 # FIX 80: expiries inside this many minutes of settlement are excluded from the # reconciliation numerator and denominator (still plotted from reported gamma). # Near settlement, T -> 0 and gamma ∝ 1/sqrt(T) makes the Black-Scholes # recompute unstable against Cboe's ~15-min-delayed quote feed, so the # reported-vs-recomputed gap for the front expiry is dominated by the feed lag, # not by a real model error. Scoring reconciliation on those expiries produces # false failures. 30 min is the starting threshold. min_minutes_to_settlement: int = 30 # FIX 85: when the empirical gamma-rounding floor (Monte Carlo, p95) brackets # the unsigned gate (0.10), the gate is dominated by data-precision noise and a # per-symbol unsigned threshold is derived as floor_p95 × this multiplier rather # than hand-tuned. Documented in the audit page; the multiplier is a headroom # factor above the 95th percentile of pure rounding noise, NOT a green-light knob. unsigned_floor_multiplier: float = 1.5 unsigned_gate: float = 0.10 # FIX 88: a derived unsigned gate is CAPPED at this multiple of the base gate. # An uncapped gate (e.g. 0.35 against a realistic worst case of ~0.10) leaves # the unsigned check unable to fire. When the corrected floor still brackets the # base gate the result is "indeterminate" (precision-limited), never a pass. unsigned_gate_cap_multiplier: float = 2.0 # FIX 35: bands are now derived from the chain's own ATM IV (two-pass). # These are the DEFAULTS used when band_basis == "fallback" (IV unavailable). strike_band: float = 0.12 # keep strikes within spot*(1±band) for BARS plot_band: float = 0.08 # minimum visible window = spot*(1±band) profile_band: float = 0.25 # GEX profile evaluated over spot*(1±band) profile_band_dex: float = 0.40 # DEX profile wider (FIX 26) so its V-minimum is # interior, not a grid-boundary artifact at ±25% # FIX 35: band derivation constants. sigma_30d = atm_iv * sqrt(30/365). band_strike_mult: float = 0.80 # strike_band = clip(0.80 * sigma_30d, ...) band_plot_mult: float = 0.50 # plot_band = clip(0.50 * sigma_30d, ...) band_profile_mult: float = 1.50 # profile_band = clip(1.50 * sigma_30d, ...) band_dex_mult: float = 2.50 # dex_band = clip(2.50 * sigma_30d, ...) atm_iv_fallback: float = 0.30 # used when IV picker fails # Guardrails against a garbage IV — NOT tuning knobs. band_limits: Dict[str, tuple] = field(default_factory=lambda: { "strike_band": (0.04, 0.20), "plot_band": (0.03, 0.15), "profile_band": (0.06, 0.35), "dex_band": (0.10, 0.50), }) # --- profile grid (FIX 36) --- profile_grid_points: int = 400 # fixed count; scale-free across symbols # --- timestamp --- source_timestamp_tz: str = "UTC" # Cboe's `timestamp` field is UTC; converted to ET for display # --- profile axes (FIX 11, reworked FIX 50) --- # "data": per-chart limits = ±1.10 * max|profile| over the VISIBLE window only. # Simple, always correct scale — the curve fills the axis regardless of OI. # "rolling": cross-snapshot-comparable = ±1.2 * median of this ticker's last # `rolling_window` profile maxima (read from the FIX 40 history CSV), falling # back to "data" when fewer than 5 snapshots exist. OI-aware by construction. # The old spot_relative formula (k*spot^2*1e-2*axis_ref_oi) is DELETED — it # scaled with a FIXED reference OI while real exposure scales with actual chain # OI, so it wildly over-scaled low-OI chains like NDX (87k OI vs SMH's 2M+). profile_axis_mode: str = "data" rolling_window: int = 20 # --- greeks recompute --- risk_free_rate: float = 0.04 dividend_yield: float = 0.0 contract_multiplier: int = 100 # --- HVL (FIX 29) --- hvl_rule: str = "zero_cross" profile_band_hvl: float = 0.40 # HVL search grid (wider than display ±25%) # --- GEX Transition (FIX 30) --- gex_transition_color: str = "#7FA6C9" gex_transition_persistence: int = 3 # consecutive strikes each side # --- Spread detection (FIX 31, revised FIX 37) --- # OI floor is now relative: max(2000, 0.5% of chain OI). spread_oi_min_abs: int = 2_000 spread_oi_min_frac: float = 0.005 # 0.5% of chain OI spread_ratio_min: float = 0.80 spread_max_width: int = 8 # |strike_a - strike_b| <= this * increment spread_flag_threshold: float = 0.20 # --- ATM IV cross-check (FIX 32) --- realised_vol_days: int = 20 iv_hv_outlier_hi: float = 2.5 iv_hv_outlier_lo: float = 0.4 # --- dealer positioning (FIX 18) --- dealer_proxy: str = "gross_oi" # --- schedule (FIX 86b: explicit asymmetric windows) --- # Slot label describes INTENT, not precision. The recorded capture time is # authoritative; the window only gates whether a capture is accepted for a # given slot filename. Late-but-same-day captures with correct timestamps are # usable data — losing them entirely is the worse failure. # AM window: 09:30–12:00 ET (morning session, broad to catch late opens) # PM window: 14:00–16:15 ET (afternoon through settlement) slot_am_start: str = "09:30" slot_am_end: str = "12:00" slot_pm_start: str = "14:00" slot_pm_end: str = "16:15" # --- chart --- fig_width: float = 10.0 fig_height: float = 9.0 dpi: int = 110 bg_color: str = "#0B0B0B" axes_bg: str = "#000000" bar_pos_color: str = "#2E9E4F" bar_neg_color: str = "#A32B20" dex_color: str = "#E08A3C" gex_profile_color: str = "#E8D44D" call_res_color: str = "#E03B3B" put_sup_color: str = "#3CB371" hvl_color: str = "#8C8C3B" spot_color: str = "#6E3B34" grid_color: str = "#333333" text_color: str = "#E8C89A" title_color: str = "#F2C185" footer_color: str = "#8A7A68" brand_text: str = "WWW.ALLOFTHESEWORDS.COM" watermark_text: str = "" # --- output --- outdir: str = "out" cache_dir: str = "data/raw" history_dir: str = "data/history" # FIX 40