# AGENTS.md ## Context - This project exposes a Flask API that uses Playwright to scrape Yahoo Finance options chains. - Entry point: `scraper_service.py` (launched via `runner.bat` or directly with Python). - The scraper loads the Yahoo options page (optionally with `?date=`) and validates expirations using the YYMMDD code embedded in contract symbols. - Option chains come from the embedded `optionChain` JSON when available, with an HTML table fallback. ## API - Route: `GET /scrape_sync` - Query params: - `stock`: symbol (default `MSFT`). - `expiration|expiry|date`: epoch seconds (Yahoo date param) or a date string matching `DATE_FORMATS`. - `strikeLimit`: number of nearest strikes to return per side (default `25`). - Behavior: - If `strikeLimit` is greater than available strikes, all available rows are returned. - `pruned_calls_count` and `pruned_puts_count` report how many rows were removed beyond the limit. - `selected_expiration` reports the resolved expiry (epoch + label), and mismatches return an error. - Route: `GET /profile` - Query params: - `stock`: symbol (default `MSFT`). - Behavior: - Loads `https://finance.yahoo.com/quote//` with Playwright. - Pulls the embedded SvelteKit payloads (quoteSummary, quote, quoteType, ratings, recommendations). - Returns a pruned JSON with valuation, profitability, growth, financial strength, cashflow, ownership, analyst, earnings, and performance summaries. ## Guard Rails - Run local 10-cycle validation (4 stocks x 4 expiries) before any deploy or push. - Run the same 10-cycle validation against the docker container before pushing the image. - Do not push if any response contains `error` or if contract symbols do not contain the expected YYMMDD code. - Keep Playwright version aligned with the docker base image (`mcr.microsoft.com/playwright/python:v1.57.0-jammy`). - Keep the API port open after a successful deploy so it can be tested immediately. ## Testing - Local server: - Start: `.\venv\Scripts\python.exe scraper_service.py` - Validate: `python scripts/test_cycles.py --base-url http://127.0.0.1:9777/scrape_sync` - Profile validation (local server): - Validate: `python scripts/test_profile_cycles.py --base-url http://127.0.0.1:9777/profile --runs 8` - Docker server: - Start: `docker run --rm -p 9777:9777 rushabhtechie/yahoo-scraper:latest` - Validate: `python scripts/test_cycles.py --base-url http://127.0.0.1:9777/scrape_sync` - Profile validation (docker server): - Validate: `python scripts/test_profile_cycles.py --base-url http://127.0.0.1:9777/profile --runs 8` ## Update Log (2025-12-28) - Added `/profile` endpoint backed by SvelteKit payload parsing (quoteSummary, quote, quoteType, ratings, recommendations). - `/profile` response trimmed to focus on valuation, profitability, growth, financial strength, cashflow, ownership, analyst, earnings, and performance summaries. - Validation ensures quote data matches the requested symbol, with issues reported in `validation`. - Issue encountered: existing server instance bound to port 9777 without `/profile`, resolved by restarting the service with the updated script. - Tests executed (local): - `.\venv\Scripts\python.exe scripts/test_profile_cycles.py --runs 8 --timeout 180` - `.\venv\Scripts\python.exe scripts\test_cycles.py --base-url http://127.0.0.1:9777/scrape_sync` - Tests executed (docker): - `docker build -t rushabhtechie/yahoo-scraper:latest .` - `.\venv\Scripts\python.exe scripts\test_cycles.py --base-url http://127.0.0.1:9777/scrape_sync` - `.\venv\Scripts\python.exe scripts\test_profile_cycles.py --base-url http://127.0.0.1:9777/profile --runs 8 --timeout 180` - The test harness verifies: - Requested expiration matches `selected_expiration.value`. - Contract symbols include the expected YYMMDD code. - `total_calls`/`total_puts` match `min(strikeLimit, available)`. - `pruned_*_count` equals the number of rows removed. ## Docker - Build: `docker build -t rushabhtechie/yahoo-scraper:latest .` - Run (CPU): `docker run --rm -p 9777:9777 rushabhtechie/yahoo-scraper:latest` - The container uses the Playwright base image with bundled browsers. ## GPU Acceleration - GPU is auto-detected via `NVIDIA_VISIBLE_DEVICES`, `/dev/nvidia0`, or `/dev/dri`. - Override detection: - Force on: `ENABLE_GPU=1` - Force off: `ENABLE_GPU=0` - Docker (NVIDIA): `docker run --rm --gpus all -e ENABLE_GPU=1 -p 9777:9777 rushabhtechie/yahoo-scraper:latest` - Docker (AMD/Intel): `docker run --rm --device=/dev/dri --group-add video -e ENABLE_GPU=1 -p 9777:9777 rushabhtechie/yahoo-scraper:latest` ## Line-by-line explanation of scraper_service.py - Line 1: Import symbols or modules. Code: `from flask import Flask, jsonify, request` - Line 2: Import symbols or modules. Code: `from playwright.sync_api import sync_playwright` - Line 3: Import symbols or modules. Code: `from bs4 import BeautifulSoup` - Line 4: Import symbols or modules. Code: `from datetime import datetime, timezone` - Line 5: Import symbols or modules. Code: `import urllib.parse` - Line 6: Import symbols or modules. Code: `import logging` - Line 7: Import symbols or modules. Code: `import json` - Line 8: Import symbols or modules. Code: `import re` - Line 9: Import symbols or modules. Code: `import time` - Line 10: Import symbols or modules. Code: `import os` - Line 11: Blank line for readability. Code: `` - Line 12: Execute the statement as written. Code: `app = Flask(__name__)` - Line 13: Blank line for readability. Code: `` - Line 14: Comment describing the next block. Code: `# Logging` - Line 15: Execute the statement as written. Code: `logging.basicConfig(` - Line 16: Execute the statement as written. Code: ` level=logging.INFO,` - Line 17: Execute the statement as written. Code: ` format="%(asctime)s [%(levelname)s] %(message)s"` - Line 18: Execute the statement as written. Code: `)` - Line 19: Execute the statement as written. Code: `app.logger.setLevel(logging.INFO)` - Line 20: Blank line for readability. Code: `` - Line 21: Execute the statement as written. Code: `DATE_FORMATS = (` - Line 22: Execute the statement as written. Code: ` "%Y-%m-%d",` - Line 23: Execute the statement as written. Code: ` "%Y/%m/%d",` - Line 24: Execute the statement as written. Code: ` "%Y%m%d",` - Line 25: Execute the statement as written. Code: ` "%b %d, %Y",` - Line 26: Execute the statement as written. Code: ` "%B %d, %Y",` - Line 27: Execute the statement as written. Code: `)` - Line 28: Blank line for readability. Code: `` - Line 29: Execute the statement as written. Code: `GPU_ACCEL_ENV = "ENABLE_GPU"` - Line 30: Blank line for readability. Code: `` - Line 31: Blank line for readability. Code: `` - Line 32: Define the parse_env_flag function. Code: `def parse_env_flag(value, default=False):` - Line 33: Execute the statement as written. Code: ` if value is None:` - Line 34: Execute the statement as written. Code: ` return default` - Line 35: Execute the statement as written. Code: ` return str(value).strip().lower() in ("1", "true", "yes", "on")` - Line 36: Blank line for readability. Code: `` - Line 37: Blank line for readability. Code: `` - Line 38: Define the detect_gpu_available function. Code: `def detect_gpu_available():` - Line 39: Execute the statement as written. Code: ` env_value = os.getenv(GPU_ACCEL_ENV)` - Line 40: Execute the statement as written. Code: ` if env_value is not None:` - Line 41: Execute the statement as written. Code: ` return parse_env_flag(env_value, default=False)` - Line 42: Blank line for readability. Code: `` - Line 43: Execute the statement as written. Code: ` nvidia_visible = os.getenv("NVIDIA_VISIBLE_DEVICES")` - Line 44: Execute the statement as written. Code: ` if nvidia_visible and nvidia_visible.lower() not in ("none", "void", "off"):` - Line 45: Execute the statement as written. Code: ` return True` - Line 46: Blank line for readability. Code: `` - Line 47: Execute the statement as written. Code: ` if os.path.exists("/dev/nvidia0"):` - Line 48: Execute the statement as written. Code: ` return True` - Line 49: Blank line for readability. Code: `` - Line 50: Execute the statement as written. Code: ` if os.path.exists("/dev/dri/renderD128") or os.path.exists("/dev/dri/card0"):` - Line 51: Execute the statement as written. Code: ` return True` - Line 52: Blank line for readability. Code: `` - Line 53: Execute the statement as written. Code: ` return False` - Line 54: Blank line for readability. Code: `` - Line 55: Blank line for readability. Code: `` - Line 56: Define the chromium_launch_args function. Code: `def chromium_launch_args():` - Line 57: Execute the statement as written. Code: ` if not detect_gpu_available():` - Line 58: Execute the statement as written. Code: ` return []` - Line 59: Blank line for readability. Code: `` - Line 60: Execute the statement as written. Code: ` if os.name == "nt":` - Line 61: Execute the statement as written. Code: ` return ["--enable-gpu"]` - Line 62: Blank line for readability. Code: `` - Line 63: Execute the statement as written. Code: ` return [` - Line 64: Execute the statement as written. Code: ` "--enable-gpu",` - Line 65: Execute the statement as written. Code: ` "--ignore-gpu-blocklist",` - Line 66: Execute the statement as written. Code: ` "--disable-software-rasterizer",` - Line 67: Execute the statement as written. Code: ` "--use-gl=egl",` - Line 68: Execute the statement as written. Code: ` "--enable-zero-copy",` - Line 69: Execute the statement as written. Code: ` "--enable-gpu-rasterization",` - Line 70: Execute the statement as written. Code: ` ]` - Line 71: Blank line for readability. Code: `` - Line 72: Blank line for readability. Code: `` - Line 73: Define the parse_date function. Code: `def parse_date(value):` - Line 74: Execute the statement as written. Code: ` for fmt in DATE_FORMATS:` - Line 75: Execute the statement as written. Code: ` try:` - Line 76: Execute the statement as written. Code: ` return datetime.strptime(value, fmt).date()` - Line 77: Execute the statement as written. Code: ` except ValueError:` - Line 78: Execute the statement as written. Code: ` continue` - Line 79: Execute the statement as written. Code: ` return None` - Line 80: Blank line for readability. Code: `` - Line 81: Blank line for readability. Code: `` - Line 82: Define the normalize_label function. Code: `def normalize_label(value):` - Line 83: Execute the statement as written. Code: ` return " ".join(value.strip().split()).lower()` - Line 84: Blank line for readability. Code: `` - Line 85: Blank line for readability. Code: `` - Line 86: Define the format_expiration_label function. Code: `def format_expiration_label(timestamp):` - Line 87: Execute the statement as written. Code: ` try:` - Line 88: Execute the statement as written. Code: ` return datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d")` - Line 89: Execute the statement as written. Code: ` except Exception:` - Line 90: Execute the statement as written. Code: ` return str(timestamp)` - Line 91: Blank line for readability. Code: `` - Line 92: Blank line for readability. Code: `` - Line 93: Define the format_percent function. Code: `def format_percent(value):` - Line 94: Execute the statement as written. Code: ` if value is None:` - Line 95: Execute the statement as written. Code: ` return None` - Line 96: Execute the statement as written. Code: ` try:` - Line 97: Execute the statement as written. Code: ` return f"{value * 100:.2f}%"` - Line 98: Execute the statement as written. Code: ` except Exception:` - Line 99: Execute the statement as written. Code: ` return None` - Line 100: Blank line for readability. Code: `` - Line 101: Blank line for readability. Code: `` - Line 102: Define the extract_raw_value function. Code: `def extract_raw_value(value):` - Line 103: Execute the statement as written. Code: ` if isinstance(value, dict):` - Line 104: Execute the statement as written. Code: ` return value.get("raw")` - Line 105: Execute the statement as written. Code: ` return value` - Line 106: Blank line for readability. Code: `` - Line 107: Blank line for readability. Code: `` - Line 108: Define the extract_fmt_value function. Code: `def extract_fmt_value(value):` - Line 109: Execute the statement as written. Code: ` if isinstance(value, dict):` - Line 110: Execute the statement as written. Code: ` return value.get("fmt")` - Line 111: Execute the statement as written. Code: ` return None` - Line 112: Blank line for readability. Code: `` - Line 113: Blank line for readability. Code: `` - Line 114: Define the format_percent_value function. Code: `def format_percent_value(value):` - Line 115: Execute the statement as written. Code: ` fmt = extract_fmt_value(value)` - Line 116: Execute the statement as written. Code: ` if fmt is not None:` - Line 117: Execute the statement as written. Code: ` return fmt` - Line 118: Execute the statement as written. Code: ` return format_percent(extract_raw_value(value))` - Line 119: Blank line for readability. Code: `` - Line 120: Blank line for readability. Code: `` - Line 121: Define the format_last_trade_date function. Code: `def format_last_trade_date(timestamp):` - Line 122: Execute the statement as written. Code: ` timestamp = extract_raw_value(timestamp)` - Line 123: Execute the statement as written. Code: ` if not timestamp:` - Line 124: Execute the statement as written. Code: ` return None` - Line 125: Execute the statement as written. Code: ` try:` - Line 126: Execute the statement as written. Code: ` return datetime.fromtimestamp(timestamp).strftime("%m/%d/%Y %I:%M %p") + " EST"` - Line 127: Execute the statement as written. Code: ` except Exception:` - Line 128: Execute the statement as written. Code: ` return None` - Line 129: Blank line for readability. Code: `` - Line 130: Blank line for readability. Code: `` - Line 131: Define the extract_option_chain_from_html function. Code: `def extract_option_chain_from_html(html):` - Line 132: Execute the statement as written. Code: ` if not html:` - Line 133: Execute the statement as written. Code: ` return None` - Line 134: Blank line for readability. Code: `` - Line 135: Execute the statement as written. Code: ` token = "\"body\":\""` - Line 136: Execute the statement as written. Code: ` start = 0` - Line 137: Execute the statement as written. Code: ` while True:` - Line 138: Execute the statement as written. Code: ` idx = html.find(token, start)` - Line 139: Execute the statement as written. Code: ` if idx == -1:` - Line 140: Execute the statement as written. Code: ` break` - Line 141: Execute the statement as written. Code: ` i = idx + len(token)` - Line 142: Execute the statement as written. Code: ` escaped = False` - Line 143: Execute the statement as written. Code: ` raw_chars = []` - Line 144: Execute the statement as written. Code: ` while i < len(html):` - Line 145: Execute the statement as written. Code: ` ch = html[i]` - Line 146: Execute the statement as written. Code: ` if escaped:` - Line 147: Execute the statement as written. Code: ` raw_chars.append(ch)` - Line 148: Execute the statement as written. Code: ` escaped = False` - Line 149: Execute the statement as written. Code: ` else:` - Line 150: Execute the statement as written. Code: ` if ch == "\\":` - Line 151: Execute the statement as written. Code: ` raw_chars.append(ch)` - Line 152: Execute the statement as written. Code: ` escaped = True` - Line 153: Execute the statement as written. Code: ` elif ch == "\"":` - Line 154: Execute the statement as written. Code: ` break` - Line 155: Execute the statement as written. Code: ` else:` - Line 156: Execute the statement as written. Code: ` raw_chars.append(ch)` - Line 157: Execute the statement as written. Code: ` i += 1` - Line 158: Execute the statement as written. Code: ` raw = "".join(raw_chars)` - Line 159: Execute the statement as written. Code: ` try:` - Line 160: Execute the statement as written. Code: ` body_text = json.loads(f"\"{raw}\"")` - Line 161: Execute the statement as written. Code: ` except json.JSONDecodeError:` - Line 162: Execute the statement as written. Code: ` start = idx + len(token)` - Line 163: Execute the statement as written. Code: ` continue` - Line 164: Execute the statement as written. Code: ` if "optionChain" not in body_text:` - Line 165: Execute the statement as written. Code: ` start = idx + len(token)` - Line 166: Execute the statement as written. Code: ` continue` - Line 167: Execute the statement as written. Code: ` try:` - Line 168: Execute the statement as written. Code: ` payload = json.loads(body_text)` - Line 169: Execute the statement as written. Code: ` except json.JSONDecodeError:` - Line 170: Execute the statement as written. Code: ` start = idx + len(token)` - Line 171: Execute the statement as written. Code: ` continue` - Line 172: Execute the statement as written. Code: ` option_chain = payload.get("optionChain")` - Line 173: Execute the statement as written. Code: ` if option_chain and option_chain.get("result"):` - Line 174: Execute the statement as written. Code: ` return option_chain` - Line 175: Blank line for readability. Code: `` - Line 176: Execute the statement as written. Code: ` start = idx + len(token)` - Line 177: Blank line for readability. Code: `` - Line 178: Execute the statement as written. Code: ` return None` - Line 179: Blank line for readability. Code: `` - Line 180: Blank line for readability. Code: `` - Line 181: Define the extract_expiration_dates_from_chain function. Code: `def extract_expiration_dates_from_chain(chain):` - Line 182: Execute the statement as written. Code: ` if not chain:` - Line 183: Execute the statement as written. Code: ` return []` - Line 184: Blank line for readability. Code: `` - Line 185: Execute the statement as written. Code: ` result = chain.get("result", [])` - Line 186: Execute the statement as written. Code: ` if not result:` - Line 187: Execute the statement as written. Code: ` return []` - Line 188: Execute the statement as written. Code: ` return result[0].get("expirationDates", []) or []` - Line 189: Blank line for readability. Code: `` - Line 190: Blank line for readability. Code: `` - Line 191: Define the normalize_chain_rows function. Code: `def normalize_chain_rows(rows):` - Line 192: Execute the statement as written. Code: ` normalized = []` - Line 193: Execute the statement as written. Code: ` for row in rows or []:` - Line 194: Execute the statement as written. Code: ` normalized.append(` - Line 195: Execute the statement as written. Code: ` {` - Line 196: Execute the statement as written. Code: ` "Contract Name": row.get("contractSymbol"),` - Line 197: Execute the statement as written. Code: ` "Last Trade Date (EST)": format_last_trade_date(` - Line 198: Execute the statement as written. Code: ` row.get("lastTradeDate")` - Line 199: Execute the statement as written. Code: ` ),` - Line 200: Execute the statement as written. Code: ` "Strike": extract_raw_value(row.get("strike")),` - Line 201: Execute the statement as written. Code: ` "Last Price": extract_raw_value(row.get("lastPrice")),` - Line 202: Execute the statement as written. Code: ` "Bid": extract_raw_value(row.get("bid")),` - Line 203: Execute the statement as written. Code: ` "Ask": extract_raw_value(row.get("ask")),` - Line 204: Execute the statement as written. Code: ` "Change": extract_raw_value(row.get("change")),` - Line 205: Execute the statement as written. Code: ` "% Change": format_percent_value(row.get("percentChange")),` - Line 206: Execute the statement as written. Code: ` "Volume": extract_raw_value(row.get("volume")),` - Line 207: Execute the statement as written. Code: ` "Open Interest": extract_raw_value(row.get("openInterest")),` - Line 208: Execute the statement as written. Code: ` "Implied Volatility": format_percent_value(` - Line 209: Execute the statement as written. Code: ` row.get("impliedVolatility")` - Line 210: Execute the statement as written. Code: ` ),` - Line 211: Execute the statement as written. Code: ` }` - Line 212: Execute the statement as written. Code: ` )` - Line 213: Execute the statement as written. Code: ` return normalized` - Line 214: Blank line for readability. Code: `` - Line 215: Blank line for readability. Code: `` - Line 216: Define the build_rows_from_chain function. Code: `def build_rows_from_chain(chain):` - Line 217: Execute the statement as written. Code: ` result = chain.get("result", []) if chain else []` - Line 218: Execute the statement as written. Code: ` if not result:` - Line 219: Execute the statement as written. Code: ` return [], []` - Line 220: Execute the statement as written. Code: ` options = result[0].get("options", [])` - Line 221: Execute the statement as written. Code: ` if not options:` - Line 222: Execute the statement as written. Code: ` return [], []` - Line 223: Execute the statement as written. Code: ` option = options[0]` - Line 224: Execute the statement as written. Code: ` return (` - Line 225: Execute the statement as written. Code: ` normalize_chain_rows(option.get("calls")),` - Line 226: Execute the statement as written. Code: ` normalize_chain_rows(option.get("puts")),` - Line 227: Execute the statement as written. Code: ` )` - Line 228: Blank line for readability. Code: `` - Line 229: Blank line for readability. Code: `` - Line 230: Define the extract_contract_expiry_code function. Code: `def extract_contract_expiry_code(contract_name):` - Line 231: Execute the statement as written. Code: ` if not contract_name:` - Line 232: Execute the statement as written. Code: ` return None` - Line 233: Execute the statement as written. Code: ` match = re.search(r"(\d{6})", contract_name)` - Line 234: Execute the statement as written. Code: ` return match.group(1) if match else None` - Line 235: Blank line for readability. Code: `` - Line 236: Blank line for readability. Code: `` - Line 237: Define the expected_expiry_code function. Code: `def expected_expiry_code(timestamp):` - Line 238: Execute the statement as written. Code: ` if not timestamp:` - Line 239: Execute the statement as written. Code: ` return None` - Line 240: Execute the statement as written. Code: ` try:` - Line 241: Execute the statement as written. Code: ` return datetime.utcfromtimestamp(timestamp).strftime("%y%m%d")` - Line 242: Execute the statement as written. Code: ` except Exception:` - Line 243: Execute the statement as written. Code: ` return None` - Line 244: Blank line for readability. Code: `` - Line 245: Blank line for readability. Code: `` - Line 246: Define the extract_expiration_dates_from_html function. Code: `def extract_expiration_dates_from_html(html):` - Line 247: Execute the statement as written. Code: ` if not html:` - Line 248: Execute the statement as written. Code: ` return []` - Line 249: Blank line for readability. Code: `` - Line 250: Execute the statement as written. Code: ` patterns = (` - Line 251: Execute the statement as written. Code: ` r'\\"expirationDates\\":\[(.*?)\]',` - Line 252: Execute the statement as written. Code: ` r'"expirationDates":\[(.*?)\]',` - Line 253: Execute the statement as written. Code: ` )` - Line 254: Execute the statement as written. Code: ` match = None` - Line 255: Execute the statement as written. Code: ` for pattern in patterns:` - Line 256: Execute the statement as written. Code: ` match = re.search(pattern, html, re.DOTALL)` - Line 257: Execute the statement as written. Code: ` if match:` - Line 258: Execute the statement as written. Code: ` break` - Line 259: Execute the statement as written. Code: ` if not match:` - Line 260: Execute the statement as written. Code: ` return []` - Line 261: Blank line for readability. Code: `` - Line 262: Execute the statement as written. Code: ` raw = match.group(1)` - Line 263: Execute the statement as written. Code: ` values = []` - Line 264: Execute the statement as written. Code: ` for part in raw.split(","):` - Line 265: Execute the statement as written. Code: ` part = part.strip()` - Line 266: Execute the statement as written. Code: ` if part.isdigit():` - Line 267: Execute the statement as written. Code: ` try:` - Line 268: Execute the statement as written. Code: ` values.append(int(part))` - Line 269: Execute the statement as written. Code: ` except Exception:` - Line 270: Execute the statement as written. Code: ` continue` - Line 271: Execute the statement as written. Code: ` return values` - Line 272: Blank line for readability. Code: `` - Line 273: Blank line for readability. Code: `` - Line 274: Define the build_expiration_options function. Code: `def build_expiration_options(expiration_dates):` - Line 275: Execute the statement as written. Code: ` options = []` - Line 276: Execute the statement as written. Code: ` for value in expiration_dates or []:` - Line 277: Execute the statement as written. Code: ` try:` - Line 278: Execute the statement as written. Code: ` value_int = int(value)` - Line 279: Execute the statement as written. Code: ` except Exception:` - Line 280: Execute the statement as written. Code: ` continue` - Line 281: Blank line for readability. Code: `` - Line 282: Execute the statement as written. Code: ` label = format_expiration_label(value_int)` - Line 283: Execute the statement as written. Code: ` try:` - Line 284: Execute the statement as written. Code: ` date_value = datetime.utcfromtimestamp(value_int).date()` - Line 285: Execute the statement as written. Code: ` except Exception:` - Line 286: Execute the statement as written. Code: ` date_value = None` - Line 287: Blank line for readability. Code: `` - Line 288: Execute the statement as written. Code: ` options.append({"value": value_int, "label": label, "date": date_value})` - Line 289: Execute the statement as written. Code: ` return sorted(options, key=lambda x: x["value"])` - Line 290: Blank line for readability. Code: `` - Line 291: Blank line for readability. Code: `` - Line 292: Define the resolve_expiration function. Code: `def resolve_expiration(expiration, options):` - Line 293: Execute the statement as written. Code: ` if not expiration:` - Line 294: Execute the statement as written. Code: ` return None, None` - Line 295: Blank line for readability. Code: `` - Line 296: Execute the statement as written. Code: ` raw = expiration.strip()` - Line 297: Execute the statement as written. Code: ` if not raw:` - Line 298: Execute the statement as written. Code: ` return None, None` - Line 299: Blank line for readability. Code: `` - Line 300: Execute the statement as written. Code: ` if raw.isdigit():` - Line 301: Execute the statement as written. Code: ` value = int(raw)` - Line 302: Execute the statement as written. Code: ` if options:` - Line 303: Execute the statement as written. Code: ` for opt in options:` - Line 304: Execute the statement as written. Code: ` if opt.get("value") == value:` - Line 305: Execute the statement as written. Code: ` return value, opt.get("label")` - Line 306: Execute the statement as written. Code: ` return None, None` - Line 307: Execute the statement as written. Code: ` return value, format_expiration_label(value)` - Line 308: Blank line for readability. Code: `` - Line 309: Execute the statement as written. Code: ` requested_date = parse_date(raw)` - Line 310: Execute the statement as written. Code: ` if requested_date:` - Line 311: Execute the statement as written. Code: ` for opt in options:` - Line 312: Execute the statement as written. Code: ` if opt.get("date") == requested_date:` - Line 313: Execute the statement as written. Code: ` return opt.get("value"), opt.get("label")` - Line 314: Execute the statement as written. Code: ` return None, None` - Line 315: Blank line for readability. Code: `` - Line 316: Execute the statement as written. Code: ` normalized = normalize_label(raw)` - Line 317: Execute the statement as written. Code: ` for opt in options:` - Line 318: Execute the statement as written. Code: ` if normalize_label(opt.get("label", "")) == normalized:` - Line 319: Execute the statement as written. Code: ` return opt.get("value"), opt.get("label")` - Line 320: Blank line for readability. Code: `` - Line 321: Execute the statement as written. Code: ` return None, None` - Line 322: Blank line for readability. Code: `` - Line 323: Blank line for readability. Code: `` - Line 324: Define the wait_for_tables function. Code: `def wait_for_tables(page):` - Line 325: Execute the statement as written. Code: ` try:` - Line 326: Execute the statement as written. Code: ` page.wait_for_selector(` - Line 327: Execute the statement as written. Code: ` "section[data-testid='options-list-table'] table",` - Line 328: Execute the statement as written. Code: ` timeout=30000,` - Line 329: Execute the statement as written. Code: ` )` - Line 330: Execute the statement as written. Code: ` except Exception:` - Line 331: Execute the statement as written. Code: ` page.wait_for_selector("table", timeout=30000)` - Line 332: Blank line for readability. Code: `` - Line 333: Execute the statement as written. Code: ` for _ in range(30): # 30 * 1s = 30 seconds` - Line 334: Execute the statement as written. Code: ` tables = page.query_selector_all(` - Line 335: Execute the statement as written. Code: ` "section[data-testid='options-list-table'] table"` - Line 336: Execute the statement as written. Code: ` )` - Line 337: Execute the statement as written. Code: ` if len(tables) >= 2:` - Line 338: Execute the statement as written. Code: ` return tables` - Line 339: Execute the statement as written. Code: ` tables = page.query_selector_all("table")` - Line 340: Execute the statement as written. Code: ` if len(tables) >= 2:` - Line 341: Execute the statement as written. Code: ` return tables` - Line 342: Execute the statement as written. Code: ` time.sleep(1)` - Line 343: Execute the statement as written. Code: ` return []` - Line 344: Blank line for readability. Code: `` - Line 345: Blank line for readability. Code: `` - Line 346: Define the parse_strike_limit function. Code: `def parse_strike_limit(value, default=25):` - Line 347: Execute the statement as written. Code: ` if value is None:` - Line 348: Execute the statement as written. Code: ` return default` - Line 349: Execute the statement as written. Code: ` try:` - Line 350: Execute the statement as written. Code: ` limit = int(value)` - Line 351: Execute the statement as written. Code: ` except (TypeError, ValueError):` - Line 352: Execute the statement as written. Code: ` return default` - Line 353: Execute the statement as written. Code: ` return limit if limit > 0 else default` - Line 354: Blank line for readability. Code: `` - Line 355: Blank line for readability. Code: `` - Line 356: Define the scrape_yahoo_options function. Code: `def scrape_yahoo_options(symbol, expiration=None, strike_limit=25):` - Line 357: Define the parse_table function. Code: ` def parse_table(table_html, side):` - Line 358: Execute the statement as written. Code: ` if not table_html:` - Line 359: Execute the statement as written. Code: ` app.logger.warning("No %s table HTML for %s", side, symbol)` - Line 360: Execute the statement as written. Code: ` return []` - Line 361: Blank line for readability. Code: `` - Line 362: Execute the statement as written. Code: ` soup = BeautifulSoup(table_html, "html.parser")` - Line 363: Blank line for readability. Code: `` - Line 364: Execute the statement as written. Code: ` headers = [th.get_text(strip=True) for th in soup.select("thead th")]` - Line 365: Execute the statement as written. Code: ` rows = soup.select("tbody tr")` - Line 366: Blank line for readability. Code: `` - Line 367: Execute the statement as written. Code: ` parsed = []` - Line 368: Execute the statement as written. Code: ` for r in rows:` - Line 369: Execute the statement as written. Code: ` tds = r.find_all("td")` - Line 370: Execute the statement as written. Code: ` if len(tds) != len(headers):` - Line 371: Execute the statement as written. Code: ` continue` - Line 372: Blank line for readability. Code: `` - Line 373: Execute the statement as written. Code: ` item = {}` - Line 374: Execute the statement as written. Code: ` for i, c in enumerate(tds):` - Line 375: Execute the statement as written. Code: ` key = headers[i]` - Line 376: Execute the statement as written. Code: ` val = c.get_text(" ", strip=True)` - Line 377: Blank line for readability. Code: `` - Line 378: Comment describing the next block. Code: ` # Convert numeric fields` - Line 379: Execute the statement as written. Code: ` if key in ["Strike", "Last Price", "Bid", "Ask", "Change"]:` - Line 380: Execute the statement as written. Code: ` try:` - Line 381: Execute the statement as written. Code: ` val = float(val.replace(",", ""))` - Line 382: Execute the statement as written. Code: ` except Exception:` - Line 383: Execute the statement as written. Code: ` val = None` - Line 384: Execute the statement as written. Code: ` elif key in ["Volume", "Open Interest"]:` - Line 385: Execute the statement as written. Code: ` try:` - Line 386: Execute the statement as written. Code: ` val = int(val.replace(",", ""))` - Line 387: Execute the statement as written. Code: ` except Exception:` - Line 388: Execute the statement as written. Code: ` val = None` - Line 389: Execute the statement as written. Code: ` elif val in ["-", ""]:` - Line 390: Execute the statement as written. Code: ` val = None` - Line 391: Blank line for readability. Code: `` - Line 392: Execute the statement as written. Code: ` item[key] = val` - Line 393: Blank line for readability. Code: `` - Line 394: Execute the statement as written. Code: ` parsed.append(item)` - Line 395: Blank line for readability. Code: `` - Line 396: Execute the statement as written. Code: ` app.logger.info("Parsed %d %s rows", len(parsed), side)` - Line 397: Execute the statement as written. Code: ` return parsed` - Line 398: Blank line for readability. Code: `` - Line 399: Define the read_option_chain function. Code: ` def read_option_chain(page):` - Line 400: Execute the statement as written. Code: ` html = page.content()` - Line 401: Execute the statement as written. Code: ` option_chain = extract_option_chain_from_html(html)` - Line 402: Execute the statement as written. Code: ` if option_chain:` - Line 403: Execute the statement as written. Code: ` expiration_dates = extract_expiration_dates_from_chain(option_chain)` - Line 404: Execute the statement as written. Code: ` else:` - Line 405: Execute the statement as written. Code: ` expiration_dates = extract_expiration_dates_from_html(html)` - Line 406: Execute the statement as written. Code: ` return option_chain, expiration_dates` - Line 407: Blank line for readability. Code: `` - Line 408: Define the has_expected_expiry function. Code: ` def has_expected_expiry(options, expected_code):` - Line 409: Execute the statement as written. Code: ` if not expected_code:` - Line 410: Execute the statement as written. Code: ` return False` - Line 411: Execute the statement as written. Code: ` for row in options or []:` - Line 412: Execute the statement as written. Code: ` name = row.get("Contract Name")` - Line 413: Execute the statement as written. Code: ` if extract_contract_expiry_code(name) == expected_code:` - Line 414: Execute the statement as written. Code: ` return True` - Line 415: Execute the statement as written. Code: ` return False` - Line 416: Blank line for readability. Code: `` - Line 417: Execute the statement as written. Code: ` encoded = urllib.parse.quote(symbol, safe="")` - Line 418: Execute the statement as written. Code: ` base_url = f"https://finance.yahoo.com/quote/{encoded}/options/"` - Line 419: Execute the statement as written. Code: ` requested_expiration = expiration.strip() if expiration else None` - Line 420: Execute the statement as written. Code: ` if not requested_expiration:` - Line 421: Execute the statement as written. Code: ` requested_expiration = None` - Line 422: Execute the statement as written. Code: ` url = base_url` - Line 423: Blank line for readability. Code: `` - Line 424: Execute the statement as written. Code: ` app.logger.info(` - Line 425: Execute the statement as written. Code: ` "Starting scrape for symbol=%s expiration=%s url=%s",` - Line 426: Execute the statement as written. Code: ` symbol,` - Line 427: Execute the statement as written. Code: ` requested_expiration,` - Line 428: Execute the statement as written. Code: ` base_url,` - Line 429: Execute the statement as written. Code: ` )` - Line 430: Blank line for readability. Code: `` - Line 431: Execute the statement as written. Code: ` calls_html = None` - Line 432: Execute the statement as written. Code: ` puts_html = None` - Line 433: Execute the statement as written. Code: ` calls_full = []` - Line 434: Execute the statement as written. Code: ` puts_full = []` - Line 435: Execute the statement as written. Code: ` price = None` - Line 436: Execute the statement as written. Code: ` selected_expiration_value = None` - Line 437: Execute the statement as written. Code: ` selected_expiration_label = None` - Line 438: Execute the statement as written. Code: ` expiration_options = []` - Line 439: Execute the statement as written. Code: ` target_date = None` - Line 440: Execute the statement as written. Code: ` fallback_to_base = False` - Line 441: Blank line for readability. Code: `` - Line 442: Execute the statement as written. Code: ` with sync_playwright() as p:` - Line 443: Execute the statement as written. Code: ` launch_args = chromium_launch_args()` - Line 444: Execute the statement as written. Code: ` if launch_args:` - Line 445: Execute the statement as written. Code: ` app.logger.info("GPU acceleration enabled")` - Line 446: Execute the statement as written. Code: ` else:` - Line 447: Execute the statement as written. Code: ` app.logger.info("GPU acceleration disabled")` - Line 448: Execute the statement as written. Code: ` browser = p.chromium.launch(headless=True, args=launch_args)` - Line 449: Execute the statement as written. Code: ` page = browser.new_page()` - Line 450: Execute the statement as written. Code: ` page.set_extra_http_headers(` - Line 451: Execute the statement as written. Code: ` {` - Line 452: Execute the statement as written. Code: ` "User-Agent": (` - Line 453: Execute the statement as written. Code: ` "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "` - Line 454: Execute the statement as written. Code: ` "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36"` - Line 455: Execute the statement as written. Code: ` )` - Line 456: Execute the statement as written. Code: ` }` - Line 457: Execute the statement as written. Code: ` )` - Line 458: Execute the statement as written. Code: ` page.set_default_timeout(60000)` - Line 459: Blank line for readability. Code: `` - Line 460: Execute the statement as written. Code: ` try:` - Line 461: Execute the statement as written. Code: ` if requested_expiration:` - Line 462: Execute the statement as written. Code: ` if requested_expiration.isdigit():` - Line 463: Execute the statement as written. Code: ` target_date = int(requested_expiration)` - Line 464: Execute the statement as written. Code: ` selected_expiration_value = target_date` - Line 465: Execute the statement as written. Code: ` selected_expiration_label = format_expiration_label(target_date)` - Line 466: Execute the statement as written. Code: ` else:` - Line 467: Execute the statement as written. Code: ` parsed_date = parse_date(requested_expiration)` - Line 468: Execute the statement as written. Code: ` if parsed_date:` - Line 469: Execute the statement as written. Code: ` target_date = int(` - Line 470: Execute the statement as written. Code: ` datetime(` - Line 471: Execute the statement as written. Code: ` parsed_date.year,` - Line 472: Execute the statement as written. Code: ` parsed_date.month,` - Line 473: Execute the statement as written. Code: ` parsed_date.day,` - Line 474: Execute the statement as written. Code: ` tzinfo=timezone.utc,` - Line 475: Execute the statement as written. Code: ` ).timestamp()` - Line 476: Execute the statement as written. Code: ` )` - Line 477: Execute the statement as written. Code: ` selected_expiration_value = target_date` - Line 478: Execute the statement as written. Code: ` selected_expiration_label = format_expiration_label(target_date)` - Line 479: Execute the statement as written. Code: ` else:` - Line 480: Execute the statement as written. Code: ` fallback_to_base = True` - Line 481: Blank line for readability. Code: `` - Line 482: Execute the statement as written. Code: ` if target_date:` - Line 483: Execute the statement as written. Code: ` url = f"{base_url}?date={target_date}"` - Line 484: Blank line for readability. Code: `` - Line 485: Execute the statement as written. Code: ` page.goto(url, wait_until="domcontentloaded", timeout=60000)` - Line 486: Execute the statement as written. Code: ` app.logger.info("Page loaded (domcontentloaded) for %s", symbol)` - Line 487: Blank line for readability. Code: `` - Line 488: Execute the statement as written. Code: ` option_chain, expiration_dates = read_option_chain(page)` - Line 489: Execute the statement as written. Code: ` app.logger.info("Option chain found: %s", bool(option_chain))` - Line 490: Execute the statement as written. Code: ` expiration_options = build_expiration_options(expiration_dates)` - Line 491: Blank line for readability. Code: `` - Line 492: Execute the statement as written. Code: ` if fallback_to_base:` - Line 493: Execute the statement as written. Code: ` resolved_value, resolved_label = resolve_expiration(` - Line 494: Execute the statement as written. Code: ` requested_expiration, expiration_options` - Line 495: Execute the statement as written. Code: ` )` - Line 496: Execute the statement as written. Code: ` if resolved_value is None:` - Line 497: Execute the statement as written. Code: ` return {` - Line 498: Execute the statement as written. Code: ` "error": "Requested expiration not available",` - Line 499: Execute the statement as written. Code: ` "stock": symbol,` - Line 500: Execute the statement as written. Code: ` "requested_expiration": requested_expiration,` - Line 501: Execute the statement as written. Code: ` "available_expirations": [` - Line 502: Execute the statement as written. Code: ` {"label": opt.get("label"), "value": opt.get("value")}` - Line 503: Execute the statement as written. Code: ` for opt in expiration_options` - Line 504: Execute the statement as written. Code: ` ],` - Line 505: Execute the statement as written. Code: ` }` - Line 506: Blank line for readability. Code: `` - Line 507: Execute the statement as written. Code: ` target_date = resolved_value` - Line 508: Execute the statement as written. Code: ` selected_expiration_value = resolved_value` - Line 509: Execute the statement as written. Code: ` selected_expiration_label = resolved_label or format_expiration_label(` - Line 510: Execute the statement as written. Code: ` resolved_value` - Line 511: Execute the statement as written. Code: ` )` - Line 512: Execute the statement as written. Code: ` url = f"{base_url}?date={resolved_value}"` - Line 513: Execute the statement as written. Code: ` page.goto(url, wait_until="domcontentloaded", timeout=60000)` - Line 514: Execute the statement as written. Code: ` app.logger.info("Page loaded (domcontentloaded) for %s", symbol)` - Line 515: Blank line for readability. Code: `` - Line 516: Execute the statement as written. Code: ` option_chain, expiration_dates = read_option_chain(page)` - Line 517: Execute the statement as written. Code: ` expiration_options = build_expiration_options(expiration_dates)` - Line 518: Blank line for readability. Code: `` - Line 519: Execute the statement as written. Code: ` if target_date and expiration_options:` - Line 520: Execute the statement as written. Code: ` matched = None` - Line 521: Execute the statement as written. Code: ` for opt in expiration_options:` - Line 522: Execute the statement as written. Code: ` if opt.get("value") == target_date:` - Line 523: Execute the statement as written. Code: ` matched = opt` - Line 524: Execute the statement as written. Code: ` break` - Line 525: Execute the statement as written. Code: ` if not matched:` - Line 526: Execute the statement as written. Code: ` return {` - Line 527: Execute the statement as written. Code: ` "error": "Requested expiration not available",` - Line 528: Execute the statement as written. Code: ` "stock": symbol,` - Line 529: Execute the statement as written. Code: ` "requested_expiration": requested_expiration,` - Line 530: Execute the statement as written. Code: ` "available_expirations": [` - Line 531: Execute the statement as written. Code: ` {"label": opt.get("label"), "value": opt.get("value")}` - Line 532: Execute the statement as written. Code: ` for opt in expiration_options` - Line 533: Execute the statement as written. Code: ` ],` - Line 534: Execute the statement as written. Code: ` }` - Line 535: Execute the statement as written. Code: ` selected_expiration_value = matched.get("value")` - Line 536: Execute the statement as written. Code: ` selected_expiration_label = matched.get("label")` - Line 537: Execute the statement as written. Code: ` elif expiration_options and not target_date:` - Line 538: Execute the statement as written. Code: ` selected_expiration_value = expiration_options[0].get("value")` - Line 539: Execute the statement as written. Code: ` selected_expiration_label = expiration_options[0].get("label")` - Line 540: Blank line for readability. Code: `` - Line 541: Execute the statement as written. Code: ` calls_full, puts_full = build_rows_from_chain(option_chain)` - Line 542: Execute the statement as written. Code: ` app.logger.info(` - Line 543: Execute the statement as written. Code: ` "Option chain rows: calls=%d puts=%d",` - Line 544: Execute the statement as written. Code: ` len(calls_full),` - Line 545: Execute the statement as written. Code: ` len(puts_full),` - Line 546: Execute the statement as written. Code: ` )` - Line 547: Blank line for readability. Code: `` - Line 548: Execute the statement as written. Code: ` if not calls_full and not puts_full:` - Line 549: Execute the statement as written. Code: ` app.logger.info("Waiting for options tables...")` - Line 550: Blank line for readability. Code: `` - Line 551: Execute the statement as written. Code: ` tables = wait_for_tables(page)` - Line 552: Execute the statement as written. Code: ` if len(tables) < 2:` - Line 553: Execute the statement as written. Code: ` app.logger.error(` - Line 554: Execute the statement as written. Code: ` "Only %d tables found; expected 2. HTML may have changed.",` - Line 555: Execute the statement as written. Code: ` len(tables),` - Line 556: Execute the statement as written. Code: ` )` - Line 557: Execute the statement as written. Code: ` return {"error": "Could not locate options tables", "stock": symbol}` - Line 558: Blank line for readability. Code: `` - Line 559: Execute the statement as written. Code: ` app.logger.info("Found %d tables. Extracting Calls & Puts.", len(tables))` - Line 560: Blank line for readability. Code: `` - Line 561: Execute the statement as written. Code: ` calls_html = tables[0].evaluate("el => el.outerHTML")` - Line 562: Execute the statement as written. Code: ` puts_html = tables[1].evaluate("el => el.outerHTML")` - Line 563: Blank line for readability. Code: `` - Line 564: Comment describing the next block. Code: ` # --- Extract current price ---` - Line 565: Execute the statement as written. Code: ` try:` - Line 566: Comment describing the next block. Code: ` # Primary selector` - Line 567: Execute the statement as written. Code: ` price_text = page.locator(` - Line 568: Execute the statement as written. Code: ` "fin-streamer[data-field='regularMarketPrice']"` - Line 569: Execute the statement as written. Code: ` ).inner_text()` - Line 570: Execute the statement as written. Code: ` price = float(price_text.replace(",", ""))` - Line 571: Execute the statement as written. Code: ` except Exception:` - Line 572: Execute the statement as written. Code: ` try:` - Line 573: Comment describing the next block. Code: ` # Fallback` - Line 574: Execute the statement as written. Code: ` price_text = page.locator("span[data-testid='qsp-price']").inner_text()` - Line 575: Execute the statement as written. Code: ` price = float(price_text.replace(",", ""))` - Line 576: Execute the statement as written. Code: ` except Exception as e:` - Line 577: Execute the statement as written. Code: ` app.logger.warning("Failed to extract price for %s: %s", symbol, e)` - Line 578: Blank line for readability. Code: `` - Line 579: Execute the statement as written. Code: ` app.logger.info("Current price for %s = %s", symbol, price)` - Line 580: Execute the statement as written. Code: ` finally:` - Line 581: Execute the statement as written. Code: ` browser.close()` - Line 582: Blank line for readability. Code: `` - Line 583: Execute the statement as written. Code: ` if not calls_full and not puts_full and calls_html and puts_html:` - Line 584: Execute the statement as written. Code: ` calls_full = parse_table(calls_html, "calls")` - Line 585: Execute the statement as written. Code: ` puts_full = parse_table(puts_html, "puts")` - Line 586: Blank line for readability. Code: `` - Line 587: Execute the statement as written. Code: ` expected_code = expected_expiry_code(target_date)` - Line 588: Execute the statement as written. Code: ` if expected_code:` - Line 589: Execute the statement as written. Code: ` if not has_expected_expiry(calls_full, expected_code) and not has_expected_expiry(` - Line 590: Execute the statement as written. Code: ` puts_full, expected_code` - Line 591: Execute the statement as written. Code: ` ):` - Line 592: Execute the statement as written. Code: ` return {` - Line 593: Execute the statement as written. Code: ` "error": "Options chain does not match requested expiration",` - Line 594: Execute the statement as written. Code: ` "stock": symbol,` - Line 595: Execute the statement as written. Code: ` "requested_expiration": requested_expiration,` - Line 596: Execute the statement as written. Code: ` "expected_expiration_code": expected_code,` - Line 597: Execute the statement as written. Code: ` "selected_expiration": {` - Line 598: Execute the statement as written. Code: ` "value": selected_expiration_value,` - Line 599: Execute the statement as written. Code: ` "label": selected_expiration_label,` - Line 600: Execute the statement as written. Code: ` },` - Line 601: Execute the statement as written. Code: ` }` - Line 602: Blank line for readability. Code: `` - Line 603: Comment describing the next block. Code: ` # ----------------------------------------------------------------------` - Line 604: Comment describing the next block. Code: ` # Pruning logic` - Line 605: Comment describing the next block. Code: ` # ----------------------------------------------------------------------` - Line 606: Define the prune_nearest function. Code: ` def prune_nearest(options, price_value, limit=25, side=""):` - Line 607: Execute the statement as written. Code: ` if price_value is None:` - Line 608: Execute the statement as written. Code: ` return options, 0` - Line 609: Blank line for readability. Code: `` - Line 610: Execute the statement as written. Code: ` numeric = [o for o in options if isinstance(o.get("Strike"), (int, float))]` - Line 611: Blank line for readability. Code: `` - Line 612: Execute the statement as written. Code: ` if len(numeric) <= limit:` - Line 613: Execute the statement as written. Code: ` return numeric, 0` - Line 614: Blank line for readability. Code: `` - Line 615: Execute the statement as written. Code: ` sorted_opts = sorted(numeric, key=lambda x: abs(x["Strike"] - price_value))` - Line 616: Execute the statement as written. Code: ` pruned = sorted_opts[:limit]` - Line 617: Execute the statement as written. Code: ` pruned_count = len(options) - len(pruned)` - Line 618: Execute the statement as written. Code: ` return pruned, pruned_count` - Line 619: Blank line for readability. Code: `` - Line 620: Execute the statement as written. Code: ` calls, pruned_calls = prune_nearest(` - Line 621: Execute the statement as written. Code: ` calls_full,` - Line 622: Execute the statement as written. Code: ` price,` - Line 623: Execute the statement as written. Code: ` limit=strike_limit,` - Line 624: Execute the statement as written. Code: ` side="calls",` - Line 625: Execute the statement as written. Code: ` )` - Line 626: Execute the statement as written. Code: ` puts, pruned_puts = prune_nearest(` - Line 627: Execute the statement as written. Code: ` puts_full,` - Line 628: Execute the statement as written. Code: ` price,` - Line 629: Execute the statement as written. Code: ` limit=strike_limit,` - Line 630: Execute the statement as written. Code: ` side="puts",` - Line 631: Execute the statement as written. Code: ` )` - Line 632: Blank line for readability. Code: `` - Line 633: Define the strike_range function. Code: ` def strike_range(opts):` - Line 634: Execute the statement as written. Code: ` strikes = [o["Strike"] for o in opts if isinstance(o.get("Strike"), (int, float))]` - Line 635: Execute the statement as written. Code: ` return [min(strikes), max(strikes)] if strikes else [None, None]` - Line 636: Blank line for readability. Code: `` - Line 637: Execute the statement as written. Code: ` return {` - Line 638: Execute the statement as written. Code: ` "stock": symbol,` - Line 639: Execute the statement as written. Code: ` "url": url,` - Line 640: Execute the statement as written. Code: ` "requested_expiration": requested_expiration,` - Line 641: Execute the statement as written. Code: ` "selected_expiration": {` - Line 642: Execute the statement as written. Code: ` "value": selected_expiration_value,` - Line 643: Execute the statement as written. Code: ` "label": selected_expiration_label,` - Line 644: Execute the statement as written. Code: ` },` - Line 645: Execute the statement as written. Code: ` "current_price": price,` - Line 646: Execute the statement as written. Code: ` "calls": calls,` - Line 647: Execute the statement as written. Code: ` "puts": puts,` - Line 648: Execute the statement as written. Code: ` "calls_strike_range": strike_range(calls),` - Line 649: Execute the statement as written. Code: ` "puts_strike_range": strike_range(puts),` - Line 650: Execute the statement as written. Code: ` "total_calls": len(calls),` - Line 651: Execute the statement as written. Code: ` "total_puts": len(puts),` - Line 652: Execute the statement as written. Code: ` "pruned_calls_count": pruned_calls,` - Line 653: Execute the statement as written. Code: ` "pruned_puts_count": pruned_puts,` - Line 654: Execute the statement as written. Code: ` }` - Line 655: Blank line for readability. Code: `` - Line 656: Blank line for readability. Code: `` - Line 657: Attach a decorator to the next function. Code: `@app.route("/scrape_sync")` - Line 658: Define the scrape_sync function. Code: `def scrape_sync():` - Line 659: Execute the statement as written. Code: ` symbol = request.args.get("stock", "MSFT")` - Line 660: Execute the statement as written. Code: ` expiration = (` - Line 661: Execute the statement as written. Code: ` request.args.get("expiration")` - Line 662: Execute the statement as written. Code: ` or request.args.get("expiry")` - Line 663: Execute the statement as written. Code: ` or request.args.get("date")` - Line 664: Execute the statement as written. Code: ` )` - Line 665: Execute the statement as written. Code: ` strike_limit = parse_strike_limit(request.args.get("strikeLimit"), default=25)` - Line 666: Execute the statement as written. Code: ` app.logger.info(` - Line 667: Execute the statement as written. Code: ` "Received /scrape_sync request for symbol=%s expiration=%s strike_limit=%s",` - Line 668: Execute the statement as written. Code: ` symbol,` - Line 669: Execute the statement as written. Code: ` expiration,` - Line 670: Execute the statement as written. Code: ` strike_limit,` - Line 671: Execute the statement as written. Code: ` )` - Line 672: Execute the statement as written. Code: ` return jsonify(scrape_yahoo_options(symbol, expiration, strike_limit))` - Line 673: Blank line for readability. Code: `` - Line 674: Blank line for readability. Code: `` - Line 675: Run the Flask development server when executed as a script. Code: `if __name__ == "__main__":` - Line 676: Execute the statement as written. Code: ` app.run(host="0.0.0.0", port=9777)`