diff --git a/AGENTS.md b/AGENTS.md index a446182..6f7b5bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,8 +3,9 @@ ## 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). -- API route: `GET /scrape_sync` with `stock` and optional `expiration|expiry|date` parameters. +- API route: `GET /scrape_sync` with `stock`, optional `expiration|expiry|date`, and `strikeLimit` parameters. - Expiration inputs: epoch seconds (Yahoo date param) or date strings supported by `DATE_FORMATS`. +- strikeLimit defaults to 25 and controls the number of nearest strikes returned per side. ## Docker - Build: `docker build -t :latest .` @@ -314,307 +315,329 @@ - Line 299: Return a value to the caller. Code: `return []` - Line 300: Blank line for readability. Code: `` - Line 301: Blank line for readability. Code: `` -- Line 302: Define the scrape_yahoo_options function. Code: `def scrape_yahoo_options(symbol, expiration=None):` -- Line 303: Define the parse_table function. Code: `def parse_table(table_html, side):` -- Line 304: Conditional branch. Code: `if not table_html:` -- Line 305: Emit or configure a log message. Code: `app.logger.warning("No %s table HTML for %s", side, symbol)` -- Line 306: Return a value to the caller. Code: `return []` -- Line 307: Blank line for readability. Code: `` -- Line 308: Execute the statement as written. Code: `soup = BeautifulSoup(table_html, "html.parser")` -- Line 309: Blank line for readability. Code: `` -- Line 310: Extract header labels from the table. Code: `headers = [th.get_text(strip=True) for th in soup.select("thead th")]` -- Line 311: Collect table rows for parsing. Code: `rows = soup.select("tbody tr")` -- Line 312: Blank line for readability. Code: `` -- Line 313: Initialize the parsed rows list. Code: `parsed = []` -- Line 314: Loop over items. Code: `for r in rows:` -- Line 315: Collect table cells for the current row. Code: `tds = r.find_all("td")` -- Line 316: Conditional branch. Code: `if len(tds) != len(headers):` -- Line 317: Execute the statement as written. Code: `continue` -- Line 318: Blank line for readability. Code: `` -- Line 319: Initialize a row dictionary. Code: `item = {}` -- Line 320: Loop over items. Code: `for i, c in enumerate(tds):` -- Line 321: Read the header name for the current column. Code: `key = headers[i]` -- Line 322: Read or convert the cell value. Code: `val = c.get_text(" ", strip=True)` -- Line 323: Blank line for readability. Code: `` -- Line 324: Comment describing the next block. Code: `# Convert numeric fields` -- Line 325: Conditional branch. Code: `if key in ["Strike", "Last Price", "Bid", "Ask", "Change"]:` -- Line 326: Start a try block for error handling. Code: `try:` -- Line 327: Read or convert the cell value. Code: `val = float(val.replace(",", ""))` -- Line 328: Handle exceptions for the preceding try block. Code: `except Exception:` -- Line 329: Read or convert the cell value. Code: `val = None` -- Line 330: Alternative conditional branch. Code: `elif key in ["Volume", "Open Interest"]:` -- Line 331: Start a try block for error handling. Code: `try:` -- Line 332: Read or convert the cell value. Code: `val = int(val.replace(",", ""))` -- Line 333: Handle exceptions for the preceding try block. Code: `except Exception:` -- Line 334: Read or convert the cell value. Code: `val = None` -- Line 335: Alternative conditional branch. Code: `elif val in ["-", ""]:` -- Line 336: Read or convert the cell value. Code: `val = None` -- Line 337: Blank line for readability. Code: `` -- Line 338: Execute the statement as written. Code: `item[key] = val` -- Line 339: Blank line for readability. Code: `` -- Line 340: Execute the statement as written. Code: `parsed.append(item)` -- Line 341: Blank line for readability. Code: `` -- Line 342: Emit or configure a log message. Code: `app.logger.info("Parsed %d %s rows", len(parsed), side)` -- Line 343: Return a value to the caller. Code: `return parsed` -- Line 344: Blank line for readability. Code: `` -- Line 345: Define the read_option_chain function. Code: `def read_option_chain(page):` -- Line 346: Capture the page HTML content. Code: `html = page.content()` -- Line 347: Execute the statement as written. Code: `option_chain = extract_option_chain_from_html(html)` -- Line 348: Conditional branch. Code: `if option_chain:` -- Line 349: Extract expiration date timestamps from the HTML. Code: `expiration_dates = extract_expiration_dates_from_chain(option_chain)` -- Line 350: Fallback branch. Code: `else:` -- Line 351: Extract expiration date timestamps from the HTML. Code: `expiration_dates = extract_expiration_dates_from_html(html)` -- Line 352: Return a value to the caller. Code: `return option_chain, expiration_dates` -- Line 353: Blank line for readability. Code: `` -- Line 354: Define the has_expected_expiry function. Code: `def has_expected_expiry(options, expected_code):` -- Line 355: Conditional branch. Code: `if not expected_code:` -- Line 356: Return a value to the caller. Code: `return False` -- Line 357: Loop over items. Code: `for row in options or []:` -- Line 358: Execute the statement as written. Code: `name = row.get("Contract Name")` -- Line 359: Conditional branch. Code: `if extract_contract_expiry_code(name) == expected_code:` -- Line 360: Return a value to the caller. Code: `return True` -- Line 361: Return a value to the caller. Code: `return False` -- Line 362: Blank line for readability. Code: `` -- Line 363: URL-encode the stock symbol. Code: `encoded = urllib.parse.quote(symbol, safe="")` -- Line 364: Build the base Yahoo Finance options URL. Code: `base_url = f"https://finance.yahoo.com/quote/{encoded}/options/"` -- Line 365: Normalize the expiration input string. Code: `requested_expiration = expiration.strip() if expiration else None` -- Line 366: Conditional branch. Code: `if not requested_expiration:` -- Line 367: Normalize the expiration input string. Code: `requested_expiration = None` -- Line 368: Set the URL to load. Code: `url = base_url` -- Line 369: Blank line for readability. Code: `` -- Line 370: Emit or configure a log message. Code: `app.logger.info(` -- Line 371: Execute the statement as written. Code: `"Starting scrape for symbol=%s expiration=%s url=%s",` -- Line 372: Execute the statement as written. Code: `symbol,` -- Line 373: Execute the statement as written. Code: `requested_expiration,` -- Line 374: Execute the statement as written. Code: `base_url,` -- Line 375: Close the current block or container. Code: `)` -- Line 376: Blank line for readability. Code: `` -- Line 377: Reserve storage for options table HTML. Code: `calls_html = None` -- Line 378: Reserve storage for options table HTML. Code: `puts_html = None` -- Line 379: Parse the full calls and puts tables. Code: `calls_full = []` -- Line 380: Parse the full calls and puts tables. Code: `puts_full = []` -- Line 381: Initialize or assign the current price. Code: `price = None` -- Line 382: Track the resolved expiration metadata. Code: `selected_expiration_value = None` -- Line 383: Track the resolved expiration metadata. Code: `selected_expiration_label = None` -- Line 384: Prepare or update the list of available expirations. Code: `expiration_options = []` -- Line 385: Track the resolved expiration epoch timestamp. Code: `target_date = None` -- Line 386: Track whether a base-page lookup is needed. Code: `fallback_to_base = False` -- Line 387: Blank line for readability. Code: `` -- Line 388: Enter a context manager block. Code: `with sync_playwright() as p:` -- Line 389: Launch a Playwright browser instance. Code: `browser = p.chromium.launch(headless=True)` -- Line 390: Create a new Playwright page. Code: `page = browser.new_page()` -- Line 391: Interact with the Playwright page. Code: `page.set_extra_http_headers(` -- Line 392: Execute the statement as written. Code: `{` -- Line 393: Execute the statement as written. Code: `"User-Agent": (` -- Line 394: Execute the statement as written. Code: `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "` -- Line 395: Execute the statement as written. Code: `"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36"` -- Line 396: Close the current block or container. Code: `)` -- Line 397: Close the current block or container. Code: `}` -- Line 398: Close the current block or container. Code: `)` -- Line 399: Interact with the Playwright page. Code: `page.set_default_timeout(60000)` -- Line 400: Blank line for readability. Code: `` -- Line 401: Start a try block for error handling. Code: `try:` -- Line 402: Conditional branch. Code: `if requested_expiration:` -- Line 403: Conditional branch. Code: `if requested_expiration.isdigit():` -- Line 404: Track the resolved expiration epoch timestamp. Code: `target_date = int(requested_expiration)` -- Line 405: Track the resolved expiration metadata. Code: `selected_expiration_value = target_date` -- Line 406: Track the resolved expiration metadata. Code: `selected_expiration_label = format_expiration_label(target_date)` -- Line 407: Fallback branch. Code: `else:` -- Line 408: Execute the statement as written. Code: `parsed_date = parse_date(requested_expiration)` -- Line 409: Conditional branch. Code: `if parsed_date:` -- Line 410: Track the resolved expiration epoch timestamp. Code: `target_date = int(` -- Line 411: Execute the statement as written. Code: `datetime(` -- Line 412: Execute the statement as written. Code: `parsed_date.year,` -- Line 413: Execute the statement as written. Code: `parsed_date.month,` -- Line 414: Execute the statement as written. Code: `parsed_date.day,` -- Line 415: Execute the statement as written. Code: `tzinfo=timezone.utc,` -- Line 416: Execute the statement as written. Code: `).timestamp()` -- Line 417: Close the current block or container. Code: `)` -- Line 418: Track the resolved expiration metadata. Code: `selected_expiration_value = target_date` -- Line 419: Track the resolved expiration metadata. Code: `selected_expiration_label = format_expiration_label(target_date)` -- Line 420: Fallback branch. Code: `else:` -- Line 421: Track whether a base-page lookup is needed. Code: `fallback_to_base = True` -- Line 422: Blank line for readability. Code: `` -- Line 423: Conditional branch. Code: `if target_date:` -- Line 424: Set the URL to load. Code: `url = f"{base_url}?date={target_date}"` -- Line 425: Blank line for readability. Code: `` -- Line 426: Navigate the Playwright page to the target URL. Code: `page.goto(url, wait_until="domcontentloaded", timeout=60000)` -- Line 427: Emit or configure a log message. Code: `app.logger.info("Page loaded (domcontentloaded) for %s", symbol)` -- Line 428: Blank line for readability. Code: `` -- Line 429: Execute the statement as written. Code: `option_chain, expiration_dates = read_option_chain(page)` -- Line 430: Emit or configure a log message. Code: `app.logger.info("Option chain found: %s", bool(option_chain))` -- Line 431: Prepare or update the list of available expirations. Code: `expiration_options = build_expiration_options(expiration_dates)` +- Line 302: Define the parse_strike_limit function. Code: `def parse_strike_limit(value, default=25):` +- Line 303: Conditional branch. Code: `if value is None:` +- Line 304: Return a value to the caller. Code: `return default` +- Line 305: Start a try block for error handling. Code: `try:` +- Line 306: Execute the statement as written. Code: `limit = int(value)` +- Line 307: Handle exceptions for the preceding try block. Code: `except (TypeError, ValueError):` +- Line 308: Return a value to the caller. Code: `return default` +- Line 309: Return a value to the caller. Code: `return limit if limit > 0 else default` +- Line 310: Blank line for readability. Code: `` +- Line 311: Blank line for readability. Code: `` +- Line 312: Define the scrape_yahoo_options function. Code: `def scrape_yahoo_options(symbol, expiration=None, strike_limit=25):` +- Line 313: Define the parse_table function. Code: `def parse_table(table_html, side):` +- Line 314: Conditional branch. Code: `if not table_html:` +- Line 315: Emit or configure a log message. Code: `app.logger.warning("No %s table HTML for %s", side, symbol)` +- Line 316: Return a value to the caller. Code: `return []` +- Line 317: Blank line for readability. Code: `` +- Line 318: Execute the statement as written. Code: `soup = BeautifulSoup(table_html, "html.parser")` +- Line 319: Blank line for readability. Code: `` +- Line 320: Extract header labels from the table. Code: `headers = [th.get_text(strip=True) for th in soup.select("thead th")]` +- Line 321: Collect table rows for parsing. Code: `rows = soup.select("tbody tr")` +- Line 322: Blank line for readability. Code: `` +- Line 323: Initialize the parsed rows list. Code: `parsed = []` +- Line 324: Loop over items. Code: `for r in rows:` +- Line 325: Collect table cells for the current row. Code: `tds = r.find_all("td")` +- Line 326: Conditional branch. Code: `if len(tds) != len(headers):` +- Line 327: Execute the statement as written. Code: `continue` +- Line 328: Blank line for readability. Code: `` +- Line 329: Initialize a row dictionary. Code: `item = {}` +- Line 330: Loop over items. Code: `for i, c in enumerate(tds):` +- Line 331: Read the header name for the current column. Code: `key = headers[i]` +- Line 332: Read or convert the cell value. Code: `val = c.get_text(" ", strip=True)` +- Line 333: Blank line for readability. Code: `` +- Line 334: Comment describing the next block. Code: `# Convert numeric fields` +- Line 335: Conditional branch. Code: `if key in ["Strike", "Last Price", "Bid", "Ask", "Change"]:` +- Line 336: Start a try block for error handling. Code: `try:` +- Line 337: Read or convert the cell value. Code: `val = float(val.replace(",", ""))` +- Line 338: Handle exceptions for the preceding try block. Code: `except Exception:` +- Line 339: Read or convert the cell value. Code: `val = None` +- Line 340: Alternative conditional branch. Code: `elif key in ["Volume", "Open Interest"]:` +- Line 341: Start a try block for error handling. Code: `try:` +- Line 342: Read or convert the cell value. Code: `val = int(val.replace(",", ""))` +- Line 343: Handle exceptions for the preceding try block. Code: `except Exception:` +- Line 344: Read or convert the cell value. Code: `val = None` +- Line 345: Alternative conditional branch. Code: `elif val in ["-", ""]:` +- Line 346: Read or convert the cell value. Code: `val = None` +- Line 347: Blank line for readability. Code: `` +- Line 348: Execute the statement as written. Code: `item[key] = val` +- Line 349: Blank line for readability. Code: `` +- Line 350: Execute the statement as written. Code: `parsed.append(item)` +- Line 351: Blank line for readability. Code: `` +- Line 352: Emit or configure a log message. Code: `app.logger.info("Parsed %d %s rows", len(parsed), side)` +- Line 353: Return a value to the caller. Code: `return parsed` +- Line 354: Blank line for readability. Code: `` +- Line 355: Define the read_option_chain function. Code: `def read_option_chain(page):` +- Line 356: Capture the page HTML content. Code: `html = page.content()` +- Line 357: Execute the statement as written. Code: `option_chain = extract_option_chain_from_html(html)` +- Line 358: Conditional branch. Code: `if option_chain:` +- Line 359: Extract expiration date timestamps from the HTML. Code: `expiration_dates = extract_expiration_dates_from_chain(option_chain)` +- Line 360: Fallback branch. Code: `else:` +- Line 361: Extract expiration date timestamps from the HTML. Code: `expiration_dates = extract_expiration_dates_from_html(html)` +- Line 362: Return a value to the caller. Code: `return option_chain, expiration_dates` +- Line 363: Blank line for readability. Code: `` +- Line 364: Define the has_expected_expiry function. Code: `def has_expected_expiry(options, expected_code):` +- Line 365: Conditional branch. Code: `if not expected_code:` +- Line 366: Return a value to the caller. Code: `return False` +- Line 367: Loop over items. Code: `for row in options or []:` +- Line 368: Execute the statement as written. Code: `name = row.get("Contract Name")` +- Line 369: Conditional branch. Code: `if extract_contract_expiry_code(name) == expected_code:` +- Line 370: Return a value to the caller. Code: `return True` +- Line 371: Return a value to the caller. Code: `return False` +- Line 372: Blank line for readability. Code: `` +- Line 373: URL-encode the stock symbol. Code: `encoded = urllib.parse.quote(symbol, safe="")` +- Line 374: Build the base Yahoo Finance options URL. Code: `base_url = f"https://finance.yahoo.com/quote/{encoded}/options/"` +- Line 375: Normalize the expiration input string. Code: `requested_expiration = expiration.strip() if expiration else None` +- Line 376: Conditional branch. Code: `if not requested_expiration:` +- Line 377: Normalize the expiration input string. Code: `requested_expiration = None` +- Line 378: Set the URL to load. Code: `url = base_url` +- Line 379: Blank line for readability. Code: `` +- Line 380: Emit or configure a log message. Code: `app.logger.info(` +- Line 381: Execute the statement as written. Code: `"Starting scrape for symbol=%s expiration=%s url=%s",` +- Line 382: Execute the statement as written. Code: `symbol,` +- Line 383: Execute the statement as written. Code: `requested_expiration,` +- Line 384: Execute the statement as written. Code: `base_url,` +- Line 385: Close the current block or container. Code: `)` +- Line 386: Blank line for readability. Code: `` +- Line 387: Reserve storage for options table HTML. Code: `calls_html = None` +- Line 388: Reserve storage for options table HTML. Code: `puts_html = None` +- Line 389: Parse the full calls and puts tables. Code: `calls_full = []` +- Line 390: Parse the full calls and puts tables. Code: `puts_full = []` +- Line 391: Initialize or assign the current price. Code: `price = None` +- Line 392: Track the resolved expiration metadata. Code: `selected_expiration_value = None` +- Line 393: Track the resolved expiration metadata. Code: `selected_expiration_label = None` +- Line 394: Prepare or update the list of available expirations. Code: `expiration_options = []` +- Line 395: Track the resolved expiration epoch timestamp. Code: `target_date = None` +- Line 396: Track whether a base-page lookup is needed. Code: `fallback_to_base = False` +- Line 397: Blank line for readability. Code: `` +- Line 398: Enter a context manager block. Code: `with sync_playwright() as p:` +- Line 399: Launch a Playwright browser instance. Code: `browser = p.chromium.launch(headless=True)` +- Line 400: Create a new Playwright page. Code: `page = browser.new_page()` +- Line 401: Interact with the Playwright page. Code: `page.set_extra_http_headers(` +- Line 402: Execute the statement as written. Code: `{` +- Line 403: Execute the statement as written. Code: `"User-Agent": (` +- Line 404: Execute the statement as written. Code: `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "` +- Line 405: Execute the statement as written. Code: `"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36"` +- Line 406: Close the current block or container. Code: `)` +- Line 407: Close the current block or container. Code: `}` +- Line 408: Close the current block or container. Code: `)` +- Line 409: Interact with the Playwright page. Code: `page.set_default_timeout(60000)` +- Line 410: Blank line for readability. Code: `` +- Line 411: Start a try block for error handling. Code: `try:` +- Line 412: Conditional branch. Code: `if requested_expiration:` +- Line 413: Conditional branch. Code: `if requested_expiration.isdigit():` +- Line 414: Track the resolved expiration epoch timestamp. Code: `target_date = int(requested_expiration)` +- Line 415: Track the resolved expiration metadata. Code: `selected_expiration_value = target_date` +- Line 416: Track the resolved expiration metadata. Code: `selected_expiration_label = format_expiration_label(target_date)` +- Line 417: Fallback branch. Code: `else:` +- Line 418: Execute the statement as written. Code: `parsed_date = parse_date(requested_expiration)` +- Line 419: Conditional branch. Code: `if parsed_date:` +- Line 420: Track the resolved expiration epoch timestamp. Code: `target_date = int(` +- Line 421: Execute the statement as written. Code: `datetime(` +- Line 422: Execute the statement as written. Code: `parsed_date.year,` +- Line 423: Execute the statement as written. Code: `parsed_date.month,` +- Line 424: Execute the statement as written. Code: `parsed_date.day,` +- Line 425: Execute the statement as written. Code: `tzinfo=timezone.utc,` +- Line 426: Execute the statement as written. Code: `).timestamp()` +- Line 427: Close the current block or container. Code: `)` +- Line 428: Track the resolved expiration metadata. Code: `selected_expiration_value = target_date` +- Line 429: Track the resolved expiration metadata. Code: `selected_expiration_label = format_expiration_label(target_date)` +- Line 430: Fallback branch. Code: `else:` +- Line 431: Track whether a base-page lookup is needed. Code: `fallback_to_base = True` - Line 432: Blank line for readability. Code: `` -- Line 433: Conditional branch. Code: `if fallback_to_base:` -- Line 434: Execute the statement as written. Code: `resolved_value, resolved_label = resolve_expiration(` -- Line 435: Execute the statement as written. Code: `requested_expiration, expiration_options` -- Line 436: Close the current block or container. Code: `)` -- Line 437: Conditional branch. Code: `if resolved_value is None:` -- Line 438: Return a value to the caller. Code: `return {` -- Line 439: Execute the statement as written. Code: `"error": "Requested expiration not available",` -- Line 440: Execute the statement as written. Code: `"stock": symbol,` -- Line 441: Execute the statement as written. Code: `"requested_expiration": requested_expiration,` -- Line 442: Execute the statement as written. Code: `"available_expirations": [` -- Line 443: Execute the statement as written. Code: `{"label": opt.get("label"), "value": opt.get("value")}` -- Line 444: Loop over items. Code: `for opt in expiration_options` -- Line 445: Close the current block or container. Code: `],` -- Line 446: Close the current block or container. Code: `}` -- Line 447: Blank line for readability. Code: `` -- Line 448: Track the resolved expiration epoch timestamp. Code: `target_date = resolved_value` -- Line 449: Track the resolved expiration metadata. Code: `selected_expiration_value = resolved_value` -- Line 450: Track the resolved expiration metadata. Code: `selected_expiration_label = resolved_label or format_expiration_label(` -- Line 451: Execute the statement as written. Code: `resolved_value` -- Line 452: Close the current block or container. Code: `)` -- Line 453: Set the URL to load. Code: `url = f"{base_url}?date={resolved_value}"` -- Line 454: Navigate the Playwright page to the target URL. Code: `page.goto(url, wait_until="domcontentloaded", timeout=60000)` -- Line 455: Emit or configure a log message. Code: `app.logger.info("Page loaded (domcontentloaded) for %s", symbol)` -- Line 456: Blank line for readability. Code: `` -- Line 457: Execute the statement as written. Code: `option_chain, expiration_dates = read_option_chain(page)` -- Line 458: Prepare or update the list of available expirations. Code: `expiration_options = build_expiration_options(expiration_dates)` -- Line 459: Blank line for readability. Code: `` -- Line 460: Conditional branch. Code: `if target_date and expiration_options:` -- Line 461: Execute the statement as written. Code: `matched = None` -- Line 462: Loop over items. Code: `for opt in expiration_options:` -- Line 463: Conditional branch. Code: `if opt.get("value") == target_date:` -- Line 464: Execute the statement as written. Code: `matched = opt` -- Line 465: Execute the statement as written. Code: `break` -- Line 466: Conditional branch. Code: `if not matched:` -- Line 467: Return a value to the caller. Code: `return {` -- Line 468: Execute the statement as written. Code: `"error": "Requested expiration not available",` -- Line 469: Execute the statement as written. Code: `"stock": symbol,` -- Line 470: Execute the statement as written. Code: `"requested_expiration": requested_expiration,` -- Line 471: Execute the statement as written. Code: `"available_expirations": [` -- Line 472: Execute the statement as written. Code: `{"label": opt.get("label"), "value": opt.get("value")}` -- Line 473: Loop over items. Code: `for opt in expiration_options` -- Line 474: Close the current block or container. Code: `],` -- Line 475: Close the current block or container. Code: `}` -- Line 476: Track the resolved expiration metadata. Code: `selected_expiration_value = matched.get("value")` -- Line 477: Track the resolved expiration metadata. Code: `selected_expiration_label = matched.get("label")` -- Line 478: Alternative conditional branch. Code: `elif expiration_options and not target_date:` -- Line 479: Track the resolved expiration metadata. Code: `selected_expiration_value = expiration_options[0].get("value")` -- Line 480: Track the resolved expiration metadata. Code: `selected_expiration_label = expiration_options[0].get("label")` -- Line 481: Blank line for readability. Code: `` -- Line 482: Execute the statement as written. Code: `calls_full, puts_full = build_rows_from_chain(option_chain)` -- Line 483: Emit or configure a log message. Code: `app.logger.info(` -- Line 484: Execute the statement as written. Code: `"Option chain rows: calls=%d puts=%d",` -- Line 485: Execute the statement as written. Code: `len(calls_full),` -- Line 486: Execute the statement as written. Code: `len(puts_full),` -- Line 487: Close the current block or container. Code: `)` -- Line 488: Blank line for readability. Code: `` -- Line 489: Conditional branch. Code: `if not calls_full and not puts_full:` -- Line 490: Emit or configure a log message. Code: `app.logger.info("Waiting for options tables...")` +- Line 433: Conditional branch. Code: `if target_date:` +- Line 434: Set the URL to load. Code: `url = f"{base_url}?date={target_date}"` +- Line 435: Blank line for readability. Code: `` +- Line 436: Navigate the Playwright page to the target URL. Code: `page.goto(url, wait_until="domcontentloaded", timeout=60000)` +- Line 437: Emit or configure a log message. Code: `app.logger.info("Page loaded (domcontentloaded) for %s", symbol)` +- Line 438: Blank line for readability. Code: `` +- Line 439: Execute the statement as written. Code: `option_chain, expiration_dates = read_option_chain(page)` +- Line 440: Emit or configure a log message. Code: `app.logger.info("Option chain found: %s", bool(option_chain))` +- Line 441: Prepare or update the list of available expirations. Code: `expiration_options = build_expiration_options(expiration_dates)` +- Line 442: Blank line for readability. Code: `` +- Line 443: Conditional branch. Code: `if fallback_to_base:` +- Line 444: Execute the statement as written. Code: `resolved_value, resolved_label = resolve_expiration(` +- Line 445: Execute the statement as written. Code: `requested_expiration, expiration_options` +- Line 446: Close the current block or container. Code: `)` +- Line 447: Conditional branch. Code: `if resolved_value is None:` +- Line 448: Return a value to the caller. Code: `return {` +- Line 449: Execute the statement as written. Code: `"error": "Requested expiration not available",` +- Line 450: Execute the statement as written. Code: `"stock": symbol,` +- Line 451: Execute the statement as written. Code: `"requested_expiration": requested_expiration,` +- Line 452: Execute the statement as written. Code: `"available_expirations": [` +- Line 453: Execute the statement as written. Code: `{"label": opt.get("label"), "value": opt.get("value")}` +- Line 454: Loop over items. Code: `for opt in expiration_options` +- Line 455: Close the current block or container. Code: `],` +- Line 456: Close the current block or container. Code: `}` +- Line 457: Blank line for readability. Code: `` +- Line 458: Track the resolved expiration epoch timestamp. Code: `target_date = resolved_value` +- Line 459: Track the resolved expiration metadata. Code: `selected_expiration_value = resolved_value` +- Line 460: Track the resolved expiration metadata. Code: `selected_expiration_label = resolved_label or format_expiration_label(` +- Line 461: Execute the statement as written. Code: `resolved_value` +- Line 462: Close the current block or container. Code: `)` +- Line 463: Set the URL to load. Code: `url = f"{base_url}?date={resolved_value}"` +- Line 464: Navigate the Playwright page to the target URL. Code: `page.goto(url, wait_until="domcontentloaded", timeout=60000)` +- Line 465: Emit or configure a log message. Code: `app.logger.info("Page loaded (domcontentloaded) for %s", symbol)` +- Line 466: Blank line for readability. Code: `` +- Line 467: Execute the statement as written. Code: `option_chain, expiration_dates = read_option_chain(page)` +- Line 468: Prepare or update the list of available expirations. Code: `expiration_options = build_expiration_options(expiration_dates)` +- Line 469: Blank line for readability. Code: `` +- Line 470: Conditional branch. Code: `if target_date and expiration_options:` +- Line 471: Execute the statement as written. Code: `matched = None` +- Line 472: Loop over items. Code: `for opt in expiration_options:` +- Line 473: Conditional branch. Code: `if opt.get("value") == target_date:` +- Line 474: Execute the statement as written. Code: `matched = opt` +- Line 475: Execute the statement as written. Code: `break` +- Line 476: Conditional branch. Code: `if not matched:` +- Line 477: Return a value to the caller. Code: `return {` +- Line 478: Execute the statement as written. Code: `"error": "Requested expiration not available",` +- Line 479: Execute the statement as written. Code: `"stock": symbol,` +- Line 480: Execute the statement as written. Code: `"requested_expiration": requested_expiration,` +- Line 481: Execute the statement as written. Code: `"available_expirations": [` +- Line 482: Execute the statement as written. Code: `{"label": opt.get("label"), "value": opt.get("value")}` +- Line 483: Loop over items. Code: `for opt in expiration_options` +- Line 484: Close the current block or container. Code: `],` +- Line 485: Close the current block or container. Code: `}` +- Line 486: Track the resolved expiration metadata. Code: `selected_expiration_value = matched.get("value")` +- Line 487: Track the resolved expiration metadata. Code: `selected_expiration_label = matched.get("label")` +- Line 488: Alternative conditional branch. Code: `elif expiration_options and not target_date:` +- Line 489: Track the resolved expiration metadata. Code: `selected_expiration_value = expiration_options[0].get("value")` +- Line 490: Track the resolved expiration metadata. Code: `selected_expiration_label = expiration_options[0].get("label")` - Line 491: Blank line for readability. Code: `` -- Line 492: Collect option tables from the page. Code: `tables = wait_for_tables(page)` -- Line 493: Conditional branch. Code: `if len(tables) < 2:` -- Line 494: Emit or configure a log message. Code: `app.logger.error(` -- Line 495: Execute the statement as written. Code: `"Only %d tables found; expected 2. HTML may have changed.",` -- Line 496: Execute the statement as written. Code: `len(tables),` +- Line 492: Execute the statement as written. Code: `calls_full, puts_full = build_rows_from_chain(option_chain)` +- Line 493: Emit or configure a log message. Code: `app.logger.info(` +- Line 494: Execute the statement as written. Code: `"Option chain rows: calls=%d puts=%d",` +- Line 495: Execute the statement as written. Code: `len(calls_full),` +- Line 496: Execute the statement as written. Code: `len(puts_full),` - Line 497: Close the current block or container. Code: `)` -- Line 498: Return a value to the caller. Code: `return {"error": "Could not locate options tables", "stock": symbol}` -- Line 499: Blank line for readability. Code: `` -- Line 500: Emit or configure a log message. Code: `app.logger.info("Found %d tables. Extracting Calls & Puts.", len(tables))` +- Line 498: Blank line for readability. Code: `` +- Line 499: Conditional branch. Code: `if not calls_full and not puts_full:` +- Line 500: Emit or configure a log message. Code: `app.logger.info("Waiting for options tables...")` - Line 501: Blank line for readability. Code: `` -- Line 502: Reserve storage for options table HTML. Code: `calls_html = tables[0].evaluate("el => el.outerHTML")` -- Line 503: Reserve storage for options table HTML. Code: `puts_html = tables[1].evaluate("el => el.outerHTML")` -- Line 504: Blank line for readability. Code: `` -- Line 505: Comment describing the next block. Code: `# --- Extract current price ---` -- Line 506: Start a try block for error handling. Code: `try:` -- Line 507: Comment describing the next block. Code: `# Primary selector` -- Line 508: Read the current price text from the page. Code: `price_text = page.locator(` -- Line 509: Execute the statement as written. Code: `"fin-streamer[data-field='regularMarketPrice']"` -- Line 510: Execute the statement as written. Code: `).inner_text()` -- Line 511: Initialize or assign the current price. Code: `price = float(price_text.replace(",", ""))` -- Line 512: Handle exceptions for the preceding try block. Code: `except Exception:` -- Line 513: Start a try block for error handling. Code: `try:` -- Line 514: Comment describing the next block. Code: `# Fallback` -- Line 515: Read the current price text from the page. Code: `price_text = page.locator("span[data-testid='qsp-price']").inner_text()` -- Line 516: Initialize or assign the current price. Code: `price = float(price_text.replace(",", ""))` -- Line 517: Handle exceptions for the preceding try block. Code: `except Exception as e:` -- Line 518: Emit or configure a log message. Code: `app.logger.warning("Failed to extract price for %s: %s", symbol, e)` -- Line 519: Blank line for readability. Code: `` -- Line 520: Emit or configure a log message. Code: `app.logger.info("Current price for %s = %s", symbol, price)` -- Line 521: Execute the statement as written. Code: `finally:` -- Line 522: Execute the statement as written. Code: `browser.close()` -- Line 523: Blank line for readability. Code: `` -- Line 524: Conditional branch. Code: `if not calls_full and not puts_full and calls_html and puts_html:` -- Line 525: Parse the full calls and puts tables. Code: `calls_full = parse_table(calls_html, "calls")` -- Line 526: Parse the full calls and puts tables. Code: `puts_full = parse_table(puts_html, "puts")` -- Line 527: Blank line for readability. Code: `` -- Line 528: Execute the statement as written. Code: `expected_code = expected_expiry_code(target_date)` -- Line 529: Conditional branch. Code: `if expected_code:` -- Line 530: Conditional branch. Code: `if not has_expected_expiry(calls_full, expected_code) and not has_expected_expiry(` -- Line 531: Execute the statement as written. Code: `puts_full, expected_code` -- Line 532: Close the current block or container. Code: `):` -- Line 533: Return a value to the caller. Code: `return {` -- Line 534: Execute the statement as written. Code: `"error": "Options chain does not match requested expiration",` -- Line 535: Execute the statement as written. Code: `"stock": symbol,` -- Line 536: Execute the statement as written. Code: `"requested_expiration": requested_expiration,` -- Line 537: Execute the statement as written. Code: `"expected_expiration_code": expected_code,` -- Line 538: Execute the statement as written. Code: `"selected_expiration": {` -- Line 539: Execute the statement as written. Code: `"value": selected_expiration_value,` -- Line 540: Execute the statement as written. Code: `"label": selected_expiration_label,` -- Line 541: Close the current block or container. Code: `},` -- Line 542: Close the current block or container. Code: `}` -- Line 543: Blank line for readability. Code: `` -- Line 544: Comment describing the next block. Code: `# ----------------------------------------------------------------------` -- Line 545: Comment describing the next block. Code: `# Pruning logic` -- Line 546: Comment describing the next block. Code: `# ----------------------------------------------------------------------` -- Line 547: Define the prune_nearest function. Code: `def prune_nearest(options, price_value, limit=26, side=""):` -- Line 548: Conditional branch. Code: `if price_value is None:` -- Line 549: Return a value to the caller. Code: `return options, 0` -- Line 550: Blank line for readability. Code: `` -- Line 551: Filter options to numeric strike entries. Code: `numeric = [o for o in options if isinstance(o.get("Strike"), (int, float))]` -- Line 552: Blank line for readability. Code: `` -- Line 553: Conditional branch. Code: `if len(numeric) <= limit:` -- Line 554: Return a value to the caller. Code: `return numeric, 0` -- Line 555: Blank line for readability. Code: `` -- Line 556: Sort options by distance to current price. Code: `sorted_opts = sorted(numeric, key=lambda x: abs(x["Strike"] - price_value))` -- Line 557: Keep the closest strike entries. Code: `pruned = sorted_opts[:limit]` -- Line 558: Compute how many rows were pruned. Code: `pruned_count = len(options) - len(pruned)` -- Line 559: Return a value to the caller. Code: `return pruned, pruned_count` +- Line 502: Collect option tables from the page. Code: `tables = wait_for_tables(page)` +- Line 503: Conditional branch. Code: `if len(tables) < 2:` +- Line 504: Emit or configure a log message. Code: `app.logger.error(` +- Line 505: Execute the statement as written. Code: `"Only %d tables found; expected 2. HTML may have changed.",` +- Line 506: Execute the statement as written. Code: `len(tables),` +- Line 507: Close the current block or container. Code: `)` +- Line 508: Return a value to the caller. Code: `return {"error": "Could not locate options tables", "stock": symbol}` +- Line 509: Blank line for readability. Code: `` +- Line 510: Emit or configure a log message. Code: `app.logger.info("Found %d tables. Extracting Calls & Puts.", len(tables))` +- Line 511: Blank line for readability. Code: `` +- Line 512: Reserve storage for options table HTML. Code: `calls_html = tables[0].evaluate("el => el.outerHTML")` +- Line 513: Reserve storage for options table HTML. Code: `puts_html = tables[1].evaluate("el => el.outerHTML")` +- Line 514: Blank line for readability. Code: `` +- Line 515: Comment describing the next block. Code: `# --- Extract current price ---` +- Line 516: Start a try block for error handling. Code: `try:` +- Line 517: Comment describing the next block. Code: `# Primary selector` +- Line 518: Read the current price text from the page. Code: `price_text = page.locator(` +- Line 519: Execute the statement as written. Code: `"fin-streamer[data-field='regularMarketPrice']"` +- Line 520: Execute the statement as written. Code: `).inner_text()` +- Line 521: Initialize or assign the current price. Code: `price = float(price_text.replace(",", ""))` +- Line 522: Handle exceptions for the preceding try block. Code: `except Exception:` +- Line 523: Start a try block for error handling. Code: `try:` +- Line 524: Comment describing the next block. Code: `# Fallback` +- Line 525: Read the current price text from the page. Code: `price_text = page.locator("span[data-testid='qsp-price']").inner_text()` +- Line 526: Initialize or assign the current price. Code: `price = float(price_text.replace(",", ""))` +- Line 527: Handle exceptions for the preceding try block. Code: `except Exception as e:` +- Line 528: Emit or configure a log message. Code: `app.logger.warning("Failed to extract price for %s: %s", symbol, e)` +- Line 529: Blank line for readability. Code: `` +- Line 530: Emit or configure a log message. Code: `app.logger.info("Current price for %s = %s", symbol, price)` +- Line 531: Execute the statement as written. Code: `finally:` +- Line 532: Execute the statement as written. Code: `browser.close()` +- Line 533: Blank line for readability. Code: `` +- Line 534: Conditional branch. Code: `if not calls_full and not puts_full and calls_html and puts_html:` +- Line 535: Parse the full calls and puts tables. Code: `calls_full = parse_table(calls_html, "calls")` +- Line 536: Parse the full calls and puts tables. Code: `puts_full = parse_table(puts_html, "puts")` +- Line 537: Blank line for readability. Code: `` +- Line 538: Execute the statement as written. Code: `expected_code = expected_expiry_code(target_date)` +- Line 539: Conditional branch. Code: `if expected_code:` +- Line 540: Conditional branch. Code: `if not has_expected_expiry(calls_full, expected_code) and not has_expected_expiry(` +- Line 541: Execute the statement as written. Code: `puts_full, expected_code` +- Line 542: Close the current block or container. Code: `):` +- Line 543: Return a value to the caller. Code: `return {` +- Line 544: Execute the statement as written. Code: `"error": "Options chain does not match requested expiration",` +- Line 545: Execute the statement as written. Code: `"stock": symbol,` +- Line 546: Execute the statement as written. Code: `"requested_expiration": requested_expiration,` +- Line 547: Execute the statement as written. Code: `"expected_expiration_code": expected_code,` +- Line 548: Execute the statement as written. Code: `"selected_expiration": {` +- Line 549: Execute the statement as written. Code: `"value": selected_expiration_value,` +- Line 550: Execute the statement as written. Code: `"label": selected_expiration_label,` +- Line 551: Close the current block or container. Code: `},` +- Line 552: Close the current block or container. Code: `}` +- Line 553: Blank line for readability. Code: `` +- Line 554: Comment describing the next block. Code: `# ----------------------------------------------------------------------` +- Line 555: Comment describing the next block. Code: `# Pruning logic` +- Line 556: Comment describing the next block. Code: `# ----------------------------------------------------------------------` +- Line 557: Define the prune_nearest function. Code: `def prune_nearest(options, price_value, limit=25, side=""):` +- Line 558: Conditional branch. Code: `if price_value is None:` +- Line 559: Return a value to the caller. Code: `return options, 0` - Line 560: Blank line for readability. Code: `` -- Line 561: Apply pruning to calls. Code: `calls, pruned_calls = prune_nearest(calls_full, price, side="calls")` -- Line 562: Apply pruning to puts. Code: `puts, pruned_puts = prune_nearest(puts_full, price, side="puts")` -- Line 563: Blank line for readability. Code: `` -- Line 564: Define the strike_range function. Code: `def strike_range(opts):` -- Line 565: Collect strike prices from the option list. Code: `strikes = [o["Strike"] for o in opts if isinstance(o.get("Strike"), (int, float))]` -- Line 566: Return a value to the caller. Code: `return [min(strikes), max(strikes)] if strikes else [None, None]` -- Line 567: Blank line for readability. Code: `` -- Line 568: Return a value to the caller. Code: `return {` -- Line 569: Execute the statement as written. Code: `"stock": symbol,` -- Line 570: Execute the statement as written. Code: `"url": url,` -- Line 571: Execute the statement as written. Code: `"requested_expiration": requested_expiration,` -- Line 572: Execute the statement as written. Code: `"selected_expiration": {` -- Line 573: Execute the statement as written. Code: `"value": selected_expiration_value,` -- Line 574: Execute the statement as written. Code: `"label": selected_expiration_label,` -- Line 575: Close the current block or container. Code: `},` -- Line 576: Execute the statement as written. Code: `"current_price": price,` -- Line 577: Execute the statement as written. Code: `"calls": calls,` -- Line 578: Execute the statement as written. Code: `"puts": puts,` -- Line 579: Execute the statement as written. Code: `"calls_strike_range": strike_range(calls),` -- Line 580: Execute the statement as written. Code: `"puts_strike_range": strike_range(puts),` -- Line 581: Execute the statement as written. Code: `"total_calls": len(calls),` -- Line 582: Execute the statement as written. Code: `"total_puts": len(puts),` -- Line 583: Execute the statement as written. Code: `"pruned_calls_count": pruned_calls,` -- Line 584: Execute the statement as written. Code: `"pruned_puts_count": pruned_puts,` -- Line 585: Close the current block or container. Code: `}` -- Line 586: Blank line for readability. Code: `` +- Line 561: Filter options to numeric strike entries. Code: `numeric = [o for o in options if isinstance(o.get("Strike"), (int, float))]` +- Line 562: Blank line for readability. Code: `` +- Line 563: Conditional branch. Code: `if len(numeric) <= limit:` +- Line 564: Return a value to the caller. Code: `return numeric, 0` +- Line 565: Blank line for readability. Code: `` +- Line 566: Sort options by distance to current price. Code: `sorted_opts = sorted(numeric, key=lambda x: abs(x["Strike"] - price_value))` +- Line 567: Keep the closest strike entries. Code: `pruned = sorted_opts[:limit]` +- Line 568: Compute how many rows were pruned. Code: `pruned_count = len(options) - len(pruned)` +- Line 569: Return a value to the caller. Code: `return pruned, pruned_count` +- Line 570: Blank line for readability. Code: `` +- Line 571: Apply pruning to calls. Code: `calls, pruned_calls = prune_nearest(` +- Line 572: Execute the statement as written. Code: `calls_full,` +- Line 573: Execute the statement as written. Code: `price,` +- Line 574: Execute the statement as written. Code: `limit=strike_limit,` +- Line 575: Execute the statement as written. Code: `side="calls",` +- Line 576: Close the current block or container. Code: `)` +- Line 577: Apply pruning to puts. Code: `puts, pruned_puts = prune_nearest(` +- Line 578: Execute the statement as written. Code: `puts_full,` +- Line 579: Execute the statement as written. Code: `price,` +- Line 580: Execute the statement as written. Code: `limit=strike_limit,` +- Line 581: Execute the statement as written. Code: `side="puts",` +- Line 582: Close the current block or container. Code: `)` +- Line 583: Blank line for readability. Code: `` +- Line 584: Define the strike_range function. Code: `def strike_range(opts):` +- Line 585: Collect strike prices from the option list. Code: `strikes = [o["Strike"] for o in opts if isinstance(o.get("Strike"), (int, float))]` +- Line 586: Return a value to the caller. Code: `return [min(strikes), max(strikes)] if strikes else [None, None]` - Line 587: Blank line for readability. Code: `` -- Line 588: Attach the route decorator to the handler. Code: `@app.route("/scrape_sync")` -- Line 589: Define the scrape_sync function. Code: `def scrape_sync():` -- Line 590: Read the stock symbol parameter. Code: `symbol = request.args.get("stock", "MSFT")` -- Line 591: Read the expiration parameters from the request. Code: `expiration = (` -- Line 592: Execute the statement as written. Code: `request.args.get("expiration")` -- Line 593: Execute the statement as written. Code: `or request.args.get("expiry")` -- Line 594: Execute the statement as written. Code: `or request.args.get("date")` -- Line 595: Close the current block or container. Code: `)` -- Line 596: Emit or configure a log message. Code: `app.logger.info(` -- Line 597: Execute the statement as written. Code: `"Received /scrape_sync request for symbol=%s expiration=%s",` -- Line 598: Execute the statement as written. Code: `symbol,` -- Line 599: Execute the statement as written. Code: `expiration,` -- Line 600: Close the current block or container. Code: `)` -- Line 601: Return a value to the caller. Code: `return jsonify(scrape_yahoo_options(symbol, expiration))` -- Line 602: Blank line for readability. Code: `` -- Line 603: Blank line for readability. Code: `` -- Line 604: Conditional branch. Code: `if __name__ == "__main__":` -- Line 605: Run the Flask development server. Code: `app.run(host="0.0.0.0", port=9777)` +- Line 588: Return a value to the caller. Code: `return {` +- Line 589: Execute the statement as written. Code: `"stock": symbol,` +- Line 590: Execute the statement as written. Code: `"url": url,` +- Line 591: Execute the statement as written. Code: `"requested_expiration": requested_expiration,` +- Line 592: Execute the statement as written. Code: `"selected_expiration": {` +- Line 593: Execute the statement as written. Code: `"value": selected_expiration_value,` +- Line 594: Execute the statement as written. Code: `"label": selected_expiration_label,` +- Line 595: Close the current block or container. Code: `},` +- Line 596: Execute the statement as written. Code: `"current_price": price,` +- Line 597: Execute the statement as written. Code: `"calls": calls,` +- Line 598: Execute the statement as written. Code: `"puts": puts,` +- Line 599: Execute the statement as written. Code: `"calls_strike_range": strike_range(calls),` +- Line 600: Execute the statement as written. Code: `"puts_strike_range": strike_range(puts),` +- Line 601: Execute the statement as written. Code: `"total_calls": len(calls),` +- Line 602: Execute the statement as written. Code: `"total_puts": len(puts),` +- Line 603: Execute the statement as written. Code: `"pruned_calls_count": pruned_calls,` +- Line 604: Execute the statement as written. Code: `"pruned_puts_count": pruned_puts,` +- Line 605: Close the current block or container. Code: `}` +- Line 606: Blank line for readability. Code: `` +- Line 607: Blank line for readability. Code: `` +- Line 608: Attach the route decorator to the handler. Code: `@app.route("/scrape_sync")` +- Line 609: Define the scrape_sync function. Code: `def scrape_sync():` +- Line 610: Read the stock symbol parameter. Code: `symbol = request.args.get("stock", "MSFT")` +- Line 611: Read the expiration parameters from the request. Code: `expiration = (` +- Line 612: Execute the statement as written. Code: `request.args.get("expiration")` +- Line 613: Execute the statement as written. Code: `or request.args.get("expiry")` +- Line 614: Execute the statement as written. Code: `or request.args.get("date")` +- Line 615: Close the current block or container. Code: `)` +- Line 616: Read or default the strikeLimit parameter. Code: `strike_limit = parse_strike_limit(request.args.get("strikeLimit"), default=25)` +- Line 617: Emit or configure a log message. Code: `app.logger.info(` +- Line 618: Execute the statement as written. Code: `"Received /scrape_sync request for symbol=%s expiration=%s strike_limit=%s",` +- Line 619: Execute the statement as written. Code: `symbol,` +- Line 620: Execute the statement as written. Code: `expiration,` +- Line 621: Read or default the strikeLimit parameter. Code: `strike_limit,` +- Line 622: Close the current block or container. Code: `)` +- Line 623: Return a value to the caller. Code: `return jsonify(scrape_yahoo_options(symbol, expiration, strike_limit))` +- Line 624: Blank line for readability. Code: `` +- Line 625: Blank line for readability. Code: `` +- Line 626: Conditional branch. Code: `if __name__ == "__main__":` +- Line 627: Run the Flask development server. Code: `app.run(host="0.0.0.0", port=9777)` diff --git a/scraper_service.py b/scraper_service.py index 22a91c8..5d957de 100644 --- a/scraper_service.py +++ b/scraper_service.py @@ -299,7 +299,17 @@ def wait_for_tables(page): return [] -def scrape_yahoo_options(symbol, expiration=None): +def parse_strike_limit(value, default=25): + if value is None: + return default + try: + limit = int(value) + except (TypeError, ValueError): + return default + return limit if limit > 0 else default + + +def scrape_yahoo_options(symbol, expiration=None, strike_limit=25): def parse_table(table_html, side): if not table_html: app.logger.warning("No %s table HTML for %s", side, symbol) @@ -544,7 +554,7 @@ def scrape_yahoo_options(symbol, expiration=None): # ---------------------------------------------------------------------- # Pruning logic # ---------------------------------------------------------------------- - def prune_nearest(options, price_value, limit=26, side=""): + def prune_nearest(options, price_value, limit=25, side=""): if price_value is None: return options, 0 @@ -558,8 +568,18 @@ def scrape_yahoo_options(symbol, expiration=None): pruned_count = len(options) - len(pruned) return pruned, pruned_count - calls, pruned_calls = prune_nearest(calls_full, price, side="calls") - puts, pruned_puts = prune_nearest(puts_full, price, side="puts") + calls, pruned_calls = prune_nearest( + calls_full, + price, + limit=strike_limit, + side="calls", + ) + puts, pruned_puts = prune_nearest( + puts_full, + price, + limit=strike_limit, + side="puts", + ) def strike_range(opts): strikes = [o["Strike"] for o in opts if isinstance(o.get("Strike"), (int, float))] @@ -593,12 +613,14 @@ def scrape_sync(): or request.args.get("expiry") or request.args.get("date") ) + strike_limit = parse_strike_limit(request.args.get("strikeLimit"), default=25) app.logger.info( - "Received /scrape_sync request for symbol=%s expiration=%s", + "Received /scrape_sync request for symbol=%s expiration=%s strike_limit=%s", symbol, expiration, + strike_limit, ) - return jsonify(scrape_yahoo_options(symbol, expiration)) + return jsonify(scrape_yahoo_options(symbol, expiration, strike_limit)) if __name__ == "__main__":