commit 4b122182a499081286033a51a212f86c813423f2 Author: Rushabh Gosar Date: Sat Nov 29 23:34:32 2025 -0800 Initial commit with code diff --git a/charts/SPX_1m_1d.png b/charts/SPX_1m_1d.png new file mode 100644 index 0000000..1351e04 Binary files /dev/null and b/charts/SPX_1m_1d.png differ diff --git a/charts/SPX_5d_1m.png b/charts/SPX_5d_1m.png new file mode 100644 index 0000000..cca6520 Binary files /dev/null and b/charts/SPX_5d_1m.png differ diff --git a/charts/SPX_5d_5y.png b/charts/SPX_5d_5y.png new file mode 100644 index 0000000..827514c Binary files /dev/null and b/charts/SPX_5d_5y.png differ diff --git a/charts/SPX_5m_1d.png b/charts/SPX_5m_1d.png new file mode 100644 index 0000000..4307867 Binary files /dev/null and b/charts/SPX_5m_1d.png differ diff --git a/runner.bat b/runner.bat new file mode 100644 index 0000000..d43b5f6 --- /dev/null +++ b/runner.bat @@ -0,0 +1,25 @@ +@echo off +setlocal + +:: Set the project folder to this script's directory +set "PROJECT_DIR=%~dp0" +cd /d "%PROJECT_DIR%" + +:: Check if venv folder exists; if not, create venv and install requirements +if not exist "venv\Scripts\python.exe" ( + echo Creating virtual environment... + python -m venv venv + call venv\Scripts\activate.bat + echo Installing required packages... + pip install --upgrade pip + pip install flask selenium webdriver-manager beautifulsoup4 +) else ( + call venv\Scripts\activate.bat +) + +:: Run the Flask server with logs redirected to server.log +echo Starting Flask server, logs will be written to server.log +start "" cmd /k "venv\Scripts\python.exe scraper_service.py" + +endlocal +exit /b 0 diff --git a/scraper_service(works).py b/scraper_service(works).py new file mode 100644 index 0000000..6efa2df --- /dev/null +++ b/scraper_service(works).py @@ -0,0 +1,143 @@ +import threading +from flask import Flask, jsonify, send_file +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup + +app = Flask(__name__) + +# Global variables to store scrape status and processed data +SCRAPE_STATUS = {"done": False, "error": None} +PROCESSED_DATA = [] + +def run_selenium_scrape(): + global SCRAPE_STATUS + global PROCESSED_DATA + SCRAPE_STATUS = {"done": False, "error": None} + PROCESSED_DATA = [] # Clear previous data + + chrome_options = Options() + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--start-maximized") + # HEADFUL: do NOT use --headless + + chrome_options.add_argument( + "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + ) + + service = Service(ChromeDriverManager().install()) + driver = webdriver.Chrome(service=service, options=chrome_options) + + try: + driver.get("https://finance.yahoo.com/quote/%5ESPX/options/") + + # Optional: click accept on consent popup if present + try: + consent_btn = WebDriverWait(driver, 5).until( + EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Accept')]")) + ) + consent_btn.click() + except: + pass # No consent popup, ignore + + # Wait for the options table + WebDriverWait(driver, 20).until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "section[data-testid='options-list-table']") + ) + ) + + html = driver.page_source + soup = BeautifulSoup(html, "html.parser") + + section = soup.find("section", {"data-testid": "options-list-table"}) + if section: + # Extract headers + headers = [th.get_text(strip=True) for th in section.find('thead').find_all('th')] + + # Extract rows + rows = section.find('tbody').find_all('tr') + + cleaned_data = [] + for row in rows: + cols = row.find_all('td') + row_data = {} + for i, col in enumerate(cols): + # Clean text, remove extra spans and strip whitespace + value = col.get_text(separator=' ', strip=True).replace('', '').strip() + + # Convert to appropriate types and handle 'nil' values + if headers[i] == 'Strike' or headers[i] == 'Last Price' or headers[i] == 'Bid' or headers[i] == 'Ask' or headers[i] == 'Change': + try: + value = float(value) + except ValueError: + value = None # Set to None for empty/nil values + elif headers[i] == 'Volume' or headers[i] == 'Open Interest': + try: + value = int(value) + except ValueError: + value = None # Set to None for empty/nil values + elif value == '-' or value == '': + value = None # Explicitly handle '-' and empty strings as None + + if value is not None: # Only include non-empty/non-nil values + row_data[headers[i]] = value + + if row_data: # Only add row if it contains any data after cleaning + cleaned_data.append(row_data) + + PROCESSED_DATA = cleaned_data + else: + PROCESSED_DATA = [] + + SCRAPE_STATUS = {"done": True, "error": None} + + except Exception as e: + SCRAPE_STATUS = {"done": False, "error": str(e)} + + finally: + driver.quit() + +# Option 1: synchronous scrape - request waits for scrape to finish +@app.route('/scrape_sync', methods=['GET']) +def scrape_sync(): + run_selenium_scrape() + if SCRAPE_STATUS["done"]: + return jsonify(PROCESSED_DATA) + else: + return jsonify({"error": SCRAPE_STATUS["error"]}), 500 + +# Option 2: threaded scrape + join - start thread, then wait for it in request +@app.route('/scrape_threaded', methods=['GET']) +def scrape_threaded(): + thread = threading.Thread(target=run_selenium_scrape) + thread.start() + thread.join() # wait for scraping to finish + + if SCRAPE_STATUS["done"]: + return jsonify(PROCESSED_DATA) + else: + return jsonify({"error": SCRAPE_STATUS["error"]}), 500 + +# Your existing endpoints to check status or get result directly +@app.route('/status', methods=['GET']) +def status(): + return jsonify(SCRAPE_STATUS) + +@app.route('/result', methods=['GET']) +def result(): + # This endpoint can now return the processed JSON data if a scrape was successful + if SCRAPE_STATUS["done"]: + return jsonify(PROCESSED_DATA) + else: + return jsonify({"error": "No data available or scrape not yet complete. Run /scrape_sync or /scrape_threaded first."}), 404 + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/scraper_service.py b/scraper_service.py new file mode 100644 index 0000000..5062540 --- /dev/null +++ b/scraper_service.py @@ -0,0 +1,188 @@ +from flask import Flask, jsonify, request +from playwright.sync_api import sync_playwright +from bs4 import BeautifulSoup +import urllib.parse +import logging + +app = Flask(__name__) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" +) +app.logger.setLevel(logging.INFO) + + +def scrape_yahoo_options(symbol): + encoded = urllib.parse.quote(symbol, safe="") + url = f"https://finance.yahoo.com/quote/{encoded}/options/" + + app.logger.info("Starting scrape for symbol=%s url=%s", symbol, url) + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + page.set_extra_http_headers({ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118 Safari/537.36" + }) + + # Avoid networkidle on Yahoo (it rarely goes “idle” because of ads/streaming) + page.goto(url, wait_until="domcontentloaded", timeout=60000) + app.logger.info("Page loaded (domcontentloaded) for %s", symbol) + + # Wait for the options tables + page.wait_for_selector( + "section[data-testid='options-list-table'] table.yf-wurt5d", + timeout=30000 + ) + app.logger.info("Options tables located in DOM for %s", symbol) + + # Grab CALLS and PUTS tables separately (first = Calls, second = Puts) + tables = page.evaluate(""" + () => { + const section = document.querySelector('section[data-testid="options-list-table"]'); + if (!section) return { calls: null, puts: null }; + + const tbs = section.querySelectorAll('table.yf-wurt5d'); + const getHTML = el => el ? el.outerHTML : null; + + return { + calls: getHTML(tbs[0] || null), + puts: getHTML(tbs[1] || null) + }; + } + """) + + calls_html = tables.get("calls") if tables else None + puts_html = tables.get("puts") if tables else None + + # Current price + price = None + try: + price_text = page.locator("span[data-testid='qsp-price']").inner_text() + price = float(price_text.replace(",", "")) + app.logger.info("Current price for %s = %s", symbol, price) + except Exception as e: + app.logger.warning("Failed to get current price for %s: %s", symbol, e) + + browser.close() + + if not calls_html and not puts_html: + app.logger.error("Could not locate options tables for %s", symbol) + return {"error": "Could not locate options tables", "stock": symbol} + + def parse_table(table_html, side): + if not table_html: + app.logger.warning("No %s table HTML present for %s", side, symbol) + return [] + + soup = BeautifulSoup(table_html, "html.parser") + headers = [th.get_text(strip=True) for th in soup.select("thead th")] + rows = soup.select("tbody tr") + + parsed_rows = [] + for r in rows: + cols = r.find_all("td") + if len(cols) != len(headers): + continue + + data = {} + for i, c in enumerate(cols): + key = headers[i] + val = c.get_text(" ", strip=True) + + if key in ["Strike", "Last Price", "Bid", "Ask", "Change"]: + try: + val = float(val.replace(",", "")) + except Exception: + val = None + elif key in ["Volume", "Open Interest"]: + try: + val = int(val.replace(",", "")) + except Exception: + val = None + elif val in ["-", ""]: + val = None + + data[key] = val + + parsed_rows.append(data) + + app.logger.info("Parsed %d %s rows for %s", len(parsed_rows), side, symbol) + return parsed_rows + + calls_full = parse_table(calls_html, "calls") + puts_full = parse_table(puts_html, "puts") + + def rng(opts): + strikes = [r.get("Strike") for r in opts + if isinstance(r.get("Strike"), (int, float))] + return [min(strikes), max(strikes)] if strikes else [None, None] + + def prune_nearest(options, price_value, limit=26, side=""): + if price_value is None: + app.logger.info( + "No current price for %s; skipping pruning for %s (keeping %d rows)", + symbol, side, len(options) + ) + return options, 0 + + numeric_opts = [o for o in options if isinstance(o.get("Strike"), (int, float))] + if len(numeric_opts) <= limit: + app.logger.info( + "Not enough %s rows for pruning for %s: total=%d, limit=%d", + side, symbol, len(numeric_opts), limit + ) + return numeric_opts, 0 + + sorted_opts = sorted( + numeric_opts, + key=lambda o: abs(o["Strike"] - price_value) + ) + pruned_list = sorted_opts[:limit] + pruned_count = len(options) - len(pruned_list) + + app.logger.info( + "Pruned %s for %s: original=%d, kept=%d, pruned=%d (limit=%d)", + side, symbol, len(options), len(pruned_list), pruned_count, limit + ) + return pruned_list, pruned_count + + # ✅ 26 closest by strike on each side + calls, pruned_calls_count = prune_nearest(calls_full, price, limit=26, side="calls") + puts, pruned_puts_count = prune_nearest(puts_full, price, limit=26, side="puts") + + calls_range = rng(calls) + puts_range = rng(puts) + + app.logger.info( + "Final summary for %s: calls_kept=%d, puts_kept=%d, " + "calls_strike_range=%s, puts_strike_range=%s", + symbol, len(calls), len(puts), calls_range, puts_range + ) + + return { + "stock": symbol, + "url": url, + "current_price": price, + "calls": calls, + "puts": puts, + "calls_strike_range": calls_range, + "puts_strike_range": puts_range, + "total_calls": len(calls), + "total_puts": len(puts), + "pruned_calls_count": pruned_calls_count, + "pruned_puts_count": pruned_puts_count, + } + +@app.route("/scrape_sync") +def scrape_sync(): + symbol = request.args.get("stock", "MSFT") + app.logger.info("Received /scrape_sync request for symbol=%s", symbol) + data = scrape_yahoo_options(symbol) + return jsonify(data) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=9777) diff --git a/scraper_service.working.backup.py b/scraper_service.working.backup.py new file mode 100644 index 0000000..be8ae11 --- /dev/null +++ b/scraper_service.working.backup.py @@ -0,0 +1,608 @@ +import threading +from flask import Flask, jsonify, request +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup +import urllib.parse + +app = Flask(__name__) + +SCRAPE_STATUS = {"done": False, "error": None} +PROCESSED_DATA = {} + +EARNINGS_STATUS = {"done": False, "error": None} +EARNINGS_DATA = {} + +def run_selenium_scrape(stock_symbol): + global SCRAPE_STATUS + global PROCESSED_DATA + + SCRAPE_STATUS = {"done": False, "error": None} + PROCESSED_DATA = {} + + removed_rows = [] + + chrome_options = Options() + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--headless") + chrome_options.add_argument("--window-size=1920,1080") + chrome_options.add_argument( + "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + ) + + service = Service(ChromeDriverManager().install()) + driver = webdriver.Chrome(service=service, options=chrome_options) + + try: + encoded_symbol = urllib.parse.quote(stock_symbol) + url = f"https://finance.yahoo.com/quote/{encoded_symbol}/options/" + driver.get(url) + + try: + consent_btn = WebDriverWait(driver, 5).until( + EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Accept')]")) + ) + consent_btn.click() + except: + pass + + WebDriverWait(driver, 20).until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "span[data-testid='qsp-price']") + ) + ) + + html = driver.page_source + soup = BeautifulSoup(html, "html.parser") + + price_span = soup.find("span", {"data-testid": "qsp-price"}) + if price_span: + current_price = float(price_span.text.replace(",", "")) + else: + raise Exception("Could not find current price!") + + section = soup.find("section", {"data-testid": "options-list-table"}) + if not section: + raise Exception("Could not find options table!") + + headers = [th.get_text(strip=True) for th in section.find('thead').find_all('th')] + rows = section.find('tbody').find_all('tr') + + all_options = [] + for row in rows: + cols = row.find_all('td') + row_data = {} + for i, col in enumerate(cols): + value = col.get_text(separator=' ', strip=True) + header = headers[i] + + if header in ['Strike', 'Last Price', 'Bid', 'Ask', 'Change']: + try: + value = float(value) + except ValueError: + value = None + elif header in ['Volume', 'Open Interest']: + try: + value = int(value) + except ValueError: + value = None + elif header == '% Chance': + try: + value = float(value.strip('%')) + except: + value = None + + if value == '-' or value == '': + value = None + + if value is not None: + row_data[header] = value + + bid = row_data.get('Bid', 0) + ask = row_data.get('Ask', 0) + pct_chance = row_data.get('% Chance', None) + + if (pct_chance == 0) or (bid == 0 and ask == 0): + removed_rows.append(row_data) + elif row_data: + all_options.append(row_data) + + calls_all = sorted([opt for opt in all_options if 'C' in opt.get('Contract Name', '')], key=lambda x: x.get('Strike', 0)) + puts_all = sorted([opt for opt in all_options if 'P' in opt.get('Contract Name', '')], key=lambda x: x.get('Strike', 0)) + + def limit_nearest(options, num, price, removed): + strikes = [o['Strike'] for o in options if 'Strike' in o] + if not strikes: + return [] + nearest_idx = min(range(len(strikes)), key=lambda i: abs(strikes[i] - price)) + half = num // 2 + + start = max(nearest_idx - half, 0) + end = min(nearest_idx + half + (num % 2), len(strikes)) + + kept = options[start:end] + removed += options[:start] + options[end:] + return kept + + calls_near = limit_nearest(calls_all, 16, current_price, removed_rows) + puts_near = limit_nearest(puts_all, 16, current_price, removed_rows) + + def get_range(options): + strikes = [o['Strike'] for o in options if 'Strike' in o] + if not strikes: + return [None, None] + return [min(strikes), max(strikes)] + + PROCESSED_DATA = { + "stock": stock_symbol, + "url": url, + "current_price": current_price, + "calls": calls_near, + "puts": puts_near, + "calls_strike_range": get_range(calls_near), + "puts_strike_range": get_range(puts_near), + "calls_strike_range_all": get_range(calls_all), + "puts_strike_range_all": get_range(puts_all), + "removed_count": len(removed_rows) + } + + SCRAPE_STATUS = {"done": True, "error": None} + + except Exception as e: + SCRAPE_STATUS = {"done": False, "error": str(e)} + + finally: + driver.quit() + +def run_earnings_scrape(): + import time + + global EARNINGS_STATUS + global EARNINGS_DATA + + EARNINGS_STATUS = {"done": False, "error": None} + EARNINGS_DATA = {} + + chrome_options = Options() + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + # chrome_options.add_argument("--headless") + chrome_options.add_argument("--window-size=1920,1080") + chrome_options.add_argument( + "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + ) + + print("[EARNINGS] Starting ChromeDriver...") + service = Service(ChromeDriverManager().install()) + driver = webdriver.Chrome(service=service, options=chrome_options) + + try: + url = "https://www.nasdaq.com/market-activity/earnings" + print(f"[EARNINGS] Navigating to: {url}") + driver.get(url) + + try: + consent_btn = WebDriverWait(driver, 5).until( + EC.element_to_be_clickable( + (By.XPATH, "//button[contains(text(),'Accept')]") + ) + ) + consent_btn.click() + print("[EARNINGS] Clicked cookie consent button.") + except Exception: + print("[EARNINGS] No cookie consent button found — skipping.") + + print("[EARNINGS] Locating element...") + host = WebDriverWait(driver, 20).until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "nsdq-table-sort") + ) + ) + + print("[EARNINGS] Accessing shadowRoot...") + rows = driver.execute_script(""" + const host = arguments[0]; + const shadowRoot = host.shadowRoot; + if (!shadowRoot) return []; + return Array.from(shadowRoot.querySelectorAll("div[part='table-row']")).map(r => r.outerHTML); + """, host) + + print(f"[EARNINGS] Found {len(rows)} rows in shadowRoot.") + + earnings_list = [] + + for row_html in rows: + # parse using BeautifulSoup + from bs4 import BeautifulSoup + row_soup = BeautifulSoup(row_html, "html.parser") + cells = row_soup.select("div[part='table-cell']") + if len(cells) >= 9: + time_icon = cells[0].img['alt'] if cells[0].img else "" + symbol = cells[1].get_text(strip=True) + company = cells[2].get_text(strip=True) + market_cap = cells[3].get_text(strip=True) + fiscal_qtr = cells[4].get_text(strip=True) + consensus_eps = cells[5].get_text(strip=True) + num_ests = cells[6].get_text(strip=True) + last_year_date = cells[7].get_text(strip=True) + last_year_eps = cells[8].get_text(strip=True) + + earnings_list.append({ + "time_icon": time_icon, + "symbol": symbol, + "company": company, + "market_cap": market_cap, + "fiscal_quarter_ending": fiscal_qtr, + "consensus_eps_forecast": consensus_eps, + "number_of_estimates": num_ests, + "last_year_report_date": last_year_date, + "last_year_eps": last_year_eps + }) + + print(f"[EARNINGS] Parsed {len(earnings_list)} rows.") + EARNINGS_DATA = { + "url": url, + "earnings": earnings_list + } + EARNINGS_STATUS = {"done": True, "error": None} + + except Exception as e: + print(f"[EARNINGS] ERROR: {e}") + ts = int(time.time()) + driver.save_screenshot(f"earnings_error_{ts}.png") + with open(f"earnings_error_{ts}.html", "w", encoding="utf-8") as f: + f.write(driver.page_source) + EARNINGS_STATUS = {"done": False, "error": str(e)} + + finally: + driver.quit() + print("[EARNINGS] Closed ChromeDriver.") + + + +@app.route('/scrape_sync', methods=['GET']) +def scrape_sync(): + stock = request.args.get('stock') + if not stock: + return jsonify({"error": "Missing 'stock' query parameter. Example: /scrape_sync?stock=%5ESPX"}), 400 + + run_selenium_scrape(stock) + if SCRAPE_STATUS["done"]: + return jsonify(PROCESSED_DATA) + else: + return jsonify({"error": SCRAPE_STATUS["error"]}), 500 + +@app.route('/scrape_earnings', methods=['GET']) +def scrape_earnings(): + run_earnings_scrape() + if EARNINGS_STATUS["done"]: + return jsonify(EARNINGS_DATA) + else: + return jsonify({"error": EARNINGS_STATUS["error"]}), 500 + +@app.route('/status', methods=['GET']) +def status(): + return jsonify({ + "options_status": SCRAPE_STATUS, + "earnings_status": EARNINGS_STATUS + }) + +@app.route('/result', methods=['GET']) +def result(): + if SCRAPE_STATUS["done"]: + return jsonify(PROCESSED_DATA) + else: + return jsonify({"error": "No data available or scrape not yet complete. Run /scrape_sync?stock= first."}), 404 +import logging +import time +import urllib.parse + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from bs4 import BeautifulSoup + +from webdriver_manager.chrome import ChromeDriverManager + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +SCRAPE_STATUS_ALL_DATES = {"done": False, "error": None} + + +def parse_options_table(html): + """ + Parse the options chain table HTML and return a list of option dicts. + You can customize this based on your original parsing logic. + """ + soup = BeautifulSoup(html, "html.parser") + section = soup.select_one("section[data-test='option-chain']") + if not section: + logger.warning("Options table section not found in HTML") + return [] + + headers = [th.get_text(strip=True) for th in section.select('thead th')] + rows = section.select('tbody tr') + + options_list = [] + for row in rows: + cols = row.find_all('td') + if len(cols) != len(headers): + continue # skip malformed row + + option_data = {} + for i, col in enumerate(cols): + header = headers[i] + text = col.get_text(separator=' ', strip=True) + # Convert numeric fields where applicable + if header in ['Strike', 'Last Price', 'Bid', 'Ask', 'Change']: + try: + text = float(text.replace(',', '')) + except: + text = None + elif header in ['Volume', 'Open Interest']: + try: + text = int(text.replace(',', '')) + except: + text = None + elif header == '% Chance': + try: + text = float(text.strip('%')) + except: + text = None + elif text in ['', '-']: + text = None + + option_data[header] = text + options_list.append(option_data) + return options_list + + +def run_selenium_scrape_per_day(stock_symbol): + logger.info(f"Starting scrape for: {stock_symbol}") + + options = Options() + # Comment this line to disable headless mode and see the browser window + # options.add_argument("--headless") + options.add_argument("--no-sandbox") + options.add_argument("--disable-dev-shm-usage") + options.add_argument("--window-size=1920,1080") + options.add_argument( + "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + ) + + service = Service(ChromeDriverManager().install()) + driver = webdriver.Chrome(service=service, options=options) + wait = WebDriverWait(driver, 20) + + try: + encoded_symbol = urllib.parse.quote(stock_symbol) + url = f"https://finance.yahoo.com/quote/{encoded_symbol}/options/" + driver.get(url) + + # Accept consent if present + try: + consent_btn = wait.until( + EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Accept')]")) + ) + consent_btn.click() + logger.info("Clicked consent accept button") + except: + logger.info("No consent button to click") + + # Wait for main price span to confirm page load + wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "span[data-testid='qsp-price']"))) + + # Click expiration dropdown button + dropdown_button = wait.until( + EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-type='date']")) + ) + dropdown_button.click() + logger.info("Clicked expiration date dropdown") + + # Get menu container id dynamically + menu_id = dropdown_button.get_attribute("aria-controls") + logger.info(f"Dropdown menu container ID: {menu_id}") + + # Wait for menu container visible + wait.until( + EC.visibility_of_element_located( + (By.CSS_SELECTOR, f"div#{menu_id}.dialog-container:not([aria-hidden='true'])") + ) + ) + menu_container = driver.find_element(By.ID, menu_id) + + # Get all date option buttons + date_buttons = menu_container.find_elements(By.CSS_SELECTOR, "button[data-type='date']") + logger.info(f"Found {len(date_buttons)} expiration dates") + + all_data = {} + + for index in range(len(date_buttons)): + # Need to reopen dropdown after first iteration, because menu closes on selection + if index > 0: + dropdown_button = wait.until( + EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-type='date']")) + ) + dropdown_button.click() + wait.until( + EC.visibility_of_element_located( + (By.CSS_SELECTOR, f"div#{menu_id}.dialog-container:not([aria-hidden='true'])") + ) + ) + menu_container = driver.find_element(By.ID, menu_id) + date_buttons = menu_container.find_elements(By.CSS_SELECTOR, "button[data-type='date']") + + date_button = date_buttons[index] + date_value = date_button.get_attribute("title") or date_button.text + logger.info(f"Selecting expiration date: {date_value}") + + # Use JS click to avoid any overlay issues + driver.execute_script("arguments[0].click();", date_button) + + # Wait for options chain section to reload + wait.until( + EC.presence_of_element_located((By.CSS_SELECTOR, "section[data-test='option-chain']")) + ) + + # Small wait to allow table content to settle + time.sleep(1) + + html = driver.page_source + options_data = parse_options_table(html) + logger.info(f"Scraped {len(options_data)} options for date {date_value}") + + all_data[date_value] = options_data + + logger.info(f"Completed scraping all expiration dates for {stock_symbol}") + return all_data + + except Exception as e: + logger.error(f"Exception during scrape: {e}", exc_info=True) + return {} + finally: + driver.quit() + +@app.route("/scrape_sync_all_dates") +def scrape_sync_all_dates(): + global SCRAPE_STATUS_ALL_DATES + SCRAPE_STATUS_ALL_DATES["done"] = False + stock = request.args.get("stock", "^SPX") + logger.info(f"Starting scrape for: {stock}") + try: + result = run_selenium_scrape_per_day(stock) + SCRAPE_STATUS_ALL_DATES["done"] = True + return jsonify(result) + except Exception as e: + SCRAPE_STATUS_ALL_DATES["error"] = str(e) + logger.error(e, exc_info=True) + return jsonify({"error": str(e)}), 500 + +from flask import send_file +import io +import os +from flask import Flask, request, jsonify, send_from_directory # ✅ FIXED import + +# Where to save charts locally +CHART_DIR = os.path.join(os.getcwd(), "charts") +os.makedirs(CHART_DIR, exist_ok=True) + + +@app.route("/chart_screenshot", methods=["GET"]) +def chart_screenshot(): + stock = request.args.get("stock") + interval = request.args.get("interval", "5m") + chart_range = request.args.get("range", "1D") + timeout = int(request.args.get("timeout", "10")) + + if not stock: + return jsonify({"error": "Missing 'stock' query parameter"}), 400 + + user_data_dir = r"C:\Users\Rushabh\AppData\Local\Google\Chrome\SeleniumProfile" + + chrome_options = Options() + chrome_options.add_argument(f"--user-data-dir={user_data_dir}") + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--disable-blink-features=AutomationControlled") + chrome_options.add_argument("--window-size=3840,2160") + chrome_options.add_argument("--force-device-scale-factor=1") + + driver = webdriver.Chrome( + service=Service(ChromeDriverManager().install()), options=chrome_options + ) + + png = None + try: + encoded_symbol = urllib.parse.quote(stock) + url = f"https://finance.yahoo.com/chart/{encoded_symbol}" + logger.info(f"Navigating to: {url}") + driver.get(url) + + # ------------------------- + # RANGE TABS (example) + # ------------------------- + try: + target_range = chart_range.upper() + tab_container = WebDriverWait(driver, timeout).until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "div[data-testid='tabs-container']") + ) + ) + buttons = tab_container.find_elements(By.TAG_NAME, "button") + for btn in buttons: + if btn.text.strip().upper() == target_range: + driver.execute_script("arguments[0].click();", btn) + logger.info(f"Clicked range tab: {target_range}") + break + except Exception as e: + logger.warning(f"Failed to select chart range {chart_range}: {e}") + + # ------------------------- + # SCREENSHOT + # ------------------------- + try: + chart = WebDriverWait(driver, timeout).until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "div[data-testid='chart-container']") + ) + ) + WebDriverWait(driver, timeout).until( + lambda d: chart.size['height'] > 0 and chart.size['width'] > 0 + ) + png = chart.screenshot_as_png + logger.info("Screenshot captured from chart container") + except Exception as e: + logger.warning(f"Chart container not found: {e}") + png = driver.get_screenshot_as_png() + logger.info("Fallback full page screenshot captured") + + except Exception as e: + logger.exception("Unhandled exception in chart_screenshot") + return jsonify({"error": str(e)}), 500 + finally: + driver.quit() + + # ------------------------- + # SAVE TO FILE + RETURN URL + # ------------------------- + filename = f"{stock}_{interval}_{chart_range}.png".replace("^", "") + out_path = os.path.join(CHART_DIR, filename) + + with open(out_path, "wb") as f: + f.write(png) + + file_url = f"http://{request.host}/charts/{filename}" + + return jsonify({ + "stock": stock, + "interval": interval, + "range": chart_range, + "url": file_url + }) + + +# ✅ Serve files from /charts +@app.route("/charts/") +def serve_chart(filename): + return send_from_directory(CHART_DIR, filename) + + + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=9777) \ No newline at end of file diff --git a/yahoo.html b/yahoo.html new file mode 100644 index 0000000..f72f891 --- /dev/null +++ b/yahoo.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Apple Inc. (AAPL) Options Chain - Yahoo Finance + + + +
+
NasdaqGS - Nasdaq Real Time Price USD

Apple Inc. (AAPL)

278.85
+1.30
+(0.47%)
At close: November 28 at 1:00:02 PM EST
278.37
-0.48
(-0.17%)
After hours: November 28 at 4:59:51 PM EST

Calls

In The Money

Contract Name Last Trade Date (EST) Strike Last Price Bid Ask Change % Change Volume Open Interest Implied Volatility
AAPL251205C00110000 11/26/2025 1:52 PM 110 168.32 166.35 168.60 0.00 0.00% 6 7 0.00%
AAPL251205C00120000 11/5/2025 11:30 AM 120 150.51 156.10 158.90 0.00 0.00% - 1 239.06%
AAPL251205C00130000 11/5/2025 11:31 AM 130 140.54 146.10 148.90 0.00 0.00% - 0 217.97%
AAPL251205C00140000 11/5/2025 11:30 AM 140 130.59 135.80 137.90 0.00 0.00% - 1 0.00%
AAPL251205C00150000 11/18/2025 2:10 PM 150 119.14 126.55 127.90 0.00 0.00% 1 3 0.00%
AAPL251205C00155000 11/25/2025 2:22 PM 155 123.85 121.05 123.95 0.00 0.00% 2 2 183.59%
AAPL251205C00160000 11/4/2025 11:37 AM 160 111.66 115.85 117.90 0.00 0.00% - 0 0.00%
AAPL251205C00165000 11/20/2025 2:08 PM 165 101.75 110.75 113.00 0.00 0.00% 1 0 0.00%
AAPL251205C00170000 11/26/2025 11:23 AM 170 108.55 106.40 108.90 0.00 0.00% 2 4 145.31%
AAPL251205C00175000 11/3/2025 12:24 PM 175 93.39 101.50 103.00 0.00 0.00% - 1 0.00%
AAPL251205C00180000 11/26/2025 10:15 AM 180 97.42 96.40 98.90 0.00 0.00% 2 0 129.69%
AAPL251205C00185000 11/28/2025 10:25 AM 185 91.85 91.30 94.00 -0.58 -0.63% 1 12 139.45%
AAPL251205C00190000 11/20/2025 10:22 AM 190 84.18 86.15 88.95 0.00 0.00% 1 0 124.61%
AAPL251205C00195000 11/28/2025 10:57 AM 195 81.81 81.35 84.00 1.42 1.77% 1 0 123.24%
AAPL251205C00200000 11/28/2025 11:25 AM 200 76.65 76.70 77.95 -2.06 -2.62% 49 87 0.00%
AAPL251205C00205000 11/28/2025 10:45 AM 205 71.70 71.10 73.40 5.53 8.36% 5 5 0.00%
AAPL251205C00210000 11/28/2025 12:16 PM 210 66.64 67.40 68.85 -2.22 -3.22% 6 0 50.00%
AAPL251205C00215000 11/28/2025 12:50 PM 215 62.90 61.90 62.90 -0.40 -0.63% 4 191 0.00%
AAPL251205C00220000 11/28/2025 12:41 PM 220 56.95 57.15 58.35 -1.97 -3.34% 8 318 0.00%
AAPL251205C00225000 11/28/2025 12:12 PM 225 51.74 52.80 53.45 -1.32 -2.49% 173 0 0.00%
AAPL251205C00230000 11/28/2025 11:59 AM 230 46.80 47.15 49.05 -1.71 -3.53% 20 204 74.71%
AAPL251205C00232500 11/28/2025 11:37 AM 232.5 44.12 45.15 46.10 -2.21 -4.77% 800 3 0.00%
AAPL251205C00235000 11/28/2025 12:45 PM 235 40.80 42.60 42.95 -3.00 -6.85% 14 293 0.00%
AAPL251205C00237500 11/28/2025 11:59 AM 237.5 39.18 38.90 41.25 -1.00 -2.49% 204 10 0.00%
AAPL251205C00240000 11/28/2025 12:46 PM 240 37.37 38.50 38.70 -0.92 -2.40% 1,167 580 0.00%
AAPL251205C00242500 11/26/2025 1:57 PM 242.5 35.90 33.35 35.60 0.00 0.00% 4 0 0.00%
AAPL251205C00245000 11/28/2025 12:54 PM 245 33.88 33.90 34.70 -0.11 -0.32% 143 318 61.96%
AAPL251205C00247500 11/28/2025 12:40 PM 247.5 29.85 29.75 31.20 -1.33 -4.27% 23 0 0.00%
AAPL251205C00250000 11/28/2025 12:58 PM 250 29.16 28.55 29.35 0.31 1.07% 216 339 55.27%
AAPL251205C00252500 11/28/2025 12:56 PM 252.5 26.56 26.10 27.70 0.90 3.51% 14 74 52.39%
AAPL251205C00255000 11/28/2025 12:55 PM 255 24.03 23.70 24.55 -0.15 -0.62% 112 0 51.25%
AAPL251205C00257500 11/28/2025 12:54 PM 257.5 20.80 21.15 22.10 -0.13 -0.62% 41 0 47.83%
AAPL251205C00260000 11/28/2025 12:57 PM 260 19.15 18.70 19.35 0.65 3.51% 238 736 39.11%
AAPL251205C00262500 11/28/2025 12:57 PM 262.5 16.61 16.30 17.00 0.53 3.30% 207 1,732 37.50%
AAPL251205C00265000 11/28/2025 12:58 PM 265 14.35 13.95 14.40 1.09 8.22% 312 0 31.62%
AAPL251205C00267500 11/28/2025 12:57 PM 267.5 11.81 11.75 12.10 0.25 2.16% 234 0 30.01%
AAPL251205C00270000 11/28/2025 12:58 PM 270 9.68 9.55 9.65 0.93 10.63% 1,028 4,141 25.88%
AAPL251205C00272500 11/28/2025 12:59 PM 272.5 7.44 7.30 7.45 0.72 10.71% 1,624 0 23.85%
AAPL251205C00275000 11/28/2025 12:59 PM 275 5.40 5.30 5.45 0.53 10.88% 11,315 6,451 22.30%
AAPL251205C00277500 11/28/2025 12:59 PM 277.5 3.66 3.65 3.70 0.26 7.65% 19,531 0 20.92%
AAPL251205C00280000 11/28/2025 12:59 PM 280 2.29 2.31 2.34 0.19 9.05% 22,804 0 20.14%
AAPL251205C00282500 11/28/2025 12:59 PM 282.5 1.39 1.33 1.37 0.14 11.20% 10,014 17,199 19.70%
AAPL251205C00285000 11/28/2025 12:59 PM 285 0.74 0.71 0.74 0.06 8.82% 13,802 13,606 19.46%
AAPL251205C00287500 11/28/2025 12:59 PM 287.5 0.37 0.35 0.38 0.02 5.71% 4,114 0 19.51%
AAPL251205C00290000 11/28/2025 1:00 PM 290 0.19 0.17 0.20 0.00 0.00% 6,434 7,763 20.02%
AAPL251205C00292500 11/28/2025 12:59 PM 292.5 0.10 0.09 0.10 0.00 0.00% 1,052 3,534 20.41%
AAPL251205C00295000 11/28/2025 12:59 PM 295 0.07 0.05 0.07 0.00 0.00% 1,204 0 22.07%
AAPL251205C00297500 11/28/2025 12:58 PM 297.5 0.04 0.03 0.05 -0.01 -20.00% 300 440 23.54%
AAPL251205C00300000 11/28/2025 12:57 PM 300 0.03 0.03 0.04 -0.01 -25.00% 769 0 25.39%
AAPL251205C00302500 11/28/2025 12:02 PM 302.5 0.02 0.01 0.03 -0.02 -50.00% 16 0 26.76%
AAPL251205C00305000 11/28/2025 12:57 PM 305 0.02 0.01 0.02 0.00 0.00% 206 1,695 27.74%
AAPL251205C00310000 11/28/2025 12:51 PM 310 0.02 0.00 0.03 0.00 0.00% 374 1,903 33.79%
AAPL251205C00315000 11/28/2025 12:42 PM 315 0.01 0.00 0.01 0.00 0.00% 3 0 33.99%
AAPL251205C00320000 11/26/2025 1:22 PM 320 0.01 0.00 0.01 0.00 0.00% 1,780 0 38.28%
AAPL251205C00325000 11/26/2025 11:51 AM 325 0.01 0.00 0.02 0.00 0.00% 63 0 44.53%
AAPL251205C00330000 11/28/2025 12:41 PM 330 0.01 0.00 0.01 0.00 0.00% 1,751 190 45.31%
AAPL251205C00335000 11/26/2025 9:40 AM 335 0.02 0.00 0.01 0.00 0.00% 10 0 49.22%
AAPL251205C00340000 11/5/2025 2:39 PM 340 0.03 0.00 0.02 0.00 0.00% 5 45 53.13%
AAPL251205C00345000 11/7/2025 9:59 AM 345 0.02 0.00 0.01 0.00 0.00% 2 106 53.13%
AAPL251205C00350000 11/13/2025 3:08 PM 350 0.02 0.00 0.01 0.00 0.00% 1 24 56.25%
AAPL251205C00355000 10/31/2025 2:29 PM 355 0.01 0.00 0.01 0.00 0.00% 2 4 59.38%
AAPL251205C00360000 11/28/2025 12:32 PM 360 0.01 0.00 0.01 -0.03 -75.00% 1 0 62.50%
AAPL251205C00370000 11/21/2025 11:51 AM 370 0.01 0.00 0.01 0.00 0.00% 2 0 68.75%
AAPL251205C00375000 11/12/2025 1:24 PM 375 0.01 0.00 0.01 0.00 0.00% 1 2 71.88%
AAPL251205C00380000 10/31/2025 12:40 PM 380 0.04 0.00 0.01 0.00 0.00% 1 3 75.00%
AAPL251205C00385000 11/6/2025 1:54 PM 385 0.01 0.00 0.01 0.00 0.00% - 0 78.13%
AAPL251205C00390000 11/24/2025 2:42 PM 390 0.01 0.00 0.01 0.00 0.00% 4 4 81.25%
AAPL251205C00400000 11/28/2025 9:30 AM 400 0.01 0.00 0.01 0.00 0.00% 2 1 84.38%

Puts

In The Money

Contract Name Last Trade Date (EST) Strike Last Price Bid Ask Change % Change Volume Open Interest Implied Volatility
AAPL251205P00110000 11/21/2025 3:26 PM 110 0.01 0.00 0.01 0.00 0.00% 5 26 218.75%
AAPL251205P00125000 11/6/2025 1:53 PM 125 0.01 0.00 0.01 0.00 0.00% - 1 187.50%
AAPL251205P00140000 11/6/2025 1:53 PM 140 0.01 0.00 0.01 0.00 0.00% 1 14 162.50%
AAPL251205P00155000 11/21/2025 11:56 AM 155 0.01 0.00 0.01 0.00 0.00% 1,375 0 137.50%
AAPL251205P00160000 11/28/2025 9:30 AM 160 0.01 0.00 0.01 -0.02 -66.67% 33 0 131.25%
AAPL251205P00165000 11/5/2025 3:24 PM 165 0.02 0.00 0.01 0.00 0.00% 1 0 125.00%
AAPL251205P00175000 11/24/2025 9:49 AM 175 0.01 0.00 0.01 0.00 0.00% 3 6 112.50%
AAPL251205P00180000 11/21/2025 11:03 AM 180 0.02 0.00 0.01 0.00 0.00% 10 38 106.25%
AAPL251205P00185000 11/21/2025 9:55 AM 185 0.03 0.00 0.01 0.00 0.00% 1 7 98.44%
AAPL251205P00190000 11/25/2025 10:31 AM 190 0.01 0.00 0.01 0.00 0.00% 2 16 93.75%
AAPL251205P00195000 11/25/2025 11:12 AM 195 0.01 0.00 0.01 0.00 0.00% 30 0 87.50%
AAPL251205P00200000 11/26/2025 1:11 PM 200 0.01 0.00 0.01 0.00 0.00% 8 0 81.25%
AAPL251205P00205000 11/28/2025 9:33 AM 205 0.15 0.00 0.01 0.13 650.00% 1 1,171 75.00%
AAPL251205P00210000 11/25/2025 10:33 AM 210 0.02 0.00 0.02 0.00 0.00% 1 688 75.00%
AAPL251205P00215000 11/28/2025 11:00 AM 215 0.01 0.01 0.02 0.00 0.00% 157 1,216 71.88%
AAPL251205P00220000 11/28/2025 11:59 AM 220 0.01 0.01 0.02 -0.01 -50.00% 40 630 65.63%
AAPL251205P00225000 11/28/2025 12:13 PM 225 0.02 0.01 0.02 0.00 0.00% 8 845 60.16%
AAPL251205P00227500 11/26/2025 2:51 PM 227.5 0.03 0.00 0.03 0.00 0.00% 1 0 57.03%
AAPL251205P00230000 11/28/2025 12:36 PM 230 0.01 0.00 0.02 -0.01 -50.00% 140 1,470 52.34%
AAPL251205P00232500 11/26/2025 11:40 AM 232.5 0.03 0.01 0.03 0.00 0.00% 1,193 0 53.13%
AAPL251205P00235000 11/28/2025 12:10 PM 235 0.02 0.01 0.03 -0.01 -33.33% 31 0 50.00%
AAPL251205P00237500 11/28/2025 11:32 AM 237.5 0.03 0.02 0.04 -0.02 -40.00% 1,112 0 51.17%
AAPL251205P00240000 11/28/2025 12:59 PM 240 0.03 0.02 0.03 -0.01 -25.00% 1,018 1,759 46.48%
AAPL251205P00242500 11/28/2025 12:44 PM 242.5 0.03 0.02 0.04 -0.03 -50.00% 218 157 45.31%
AAPL251205P00245000 11/28/2025 12:59 PM 245 0.04 0.03 0.04 -0.02 -33.33% 1,328 2,420 42.19%
AAPL251205P00247500 11/28/2025 12:16 PM 247.5 0.05 0.04 0.05 -0.03 -37.50% 47 0 40.43%
AAPL251205P00250000 11/28/2025 12:59 PM 250 0.05 0.04 0.06 -0.03 -37.50% 532 0 38.38%
AAPL251205P00252500 11/28/2025 12:55 PM 252.5 0.06 0.05 0.06 -0.04 -40.00% 131 1,236 35.35%
AAPL251205P00255000 11/28/2025 12:59 PM 255 0.06 0.06 0.07 -0.06 -50.00% 765 0 33.01%
AAPL251205P00257500 11/28/2025 12:59 PM 257.5 0.09 0.08 0.09 -0.05 -35.71% 201 0 31.06%
AAPL251205P00260000 11/28/2025 12:59 PM 260 0.10 0.10 0.12 -0.08 -44.44% 2,556 0 29.20%
AAPL251205P00262500 11/28/2025 12:59 PM 262.5 0.13 0.13 0.15 -0.11 -45.83% 997 1,584 26.91%
AAPL251205P00265000 11/28/2025 12:59 PM 265 0.20 0.19 0.21 -0.13 -39.39% 2,198 3,946 25.05%
AAPL251205P00267500 11/28/2025 12:59 PM 267.5 0.30 0.29 0.31 -0.21 -41.18% 2,441 0 23.39%
AAPL251205P00270000 11/28/2025 12:59 PM 270 0.49 0.47 0.50 -0.28 -36.36% 7,858 6,311 22.17%
AAPL251205P00272500 11/28/2025 12:59 PM 272.5 0.81 0.78 0.81 -0.44 -35.20% 4,090 0 21.02%
AAPL251205P00275000 11/28/2025 12:59 PM 275 1.30 1.28 1.32 -0.58 -30.85% 10,972 0 20.04%
AAPL251205P00277500 11/28/2025 12:59 PM 277.5 2.17 2.07 2.13 -0.71 -24.65% 3,462 3,740 19.35%
AAPL251205P00280000 11/28/2025 12:59 PM 280 3.30 3.20 3.30 -0.85 -20.48% 1,646 0 18.80%
AAPL251205P00282500 11/28/2025 12:57 PM 282.5 4.87 4.75 4.85 -0.79 -13.96% 826 0 18.34%
AAPL251205P00285000 11/28/2025 12:58 PM 285 6.67 6.45 6.70 -1.03 -13.38% 505 0 17.44%
AAPL251205P00287500 11/28/2025 12:57 PM 287.5 9.01 8.60 9.20 -0.90 -9.08% 53 74 21.80%
AAPL251205P00290000 11/28/2025 12:56 PM 290 10.20 10.95 11.50 -1.80 -15.00% 38 146 22.85%
AAPL251205P00292500 11/26/2025 9:53 AM 292.5 15.20 13.25 14.00 0.00 0.00% 1 3 26.51%
AAPL251205P00295000 11/26/2025 10:37 AM 295 17.60 16.95 18.35 0.00 0.00% 3 0 52.75%
AAPL251205P00297500 11/25/2025 3:44 PM 297.5 20.17 17.35 20.85 0.00 0.00% 36 0 57.32%
AAPL251205P00300000 11/28/2025 9:32 AM 300 22.00 20.85 23.10 0.00 0.00% 8 9 59.11%
AAPL251205P00302500 11/25/2025 10:50 AM 302.5 23.67 23.55 25.65 0.00 0.00% 22 - 50.71%
AAPL251205P00305000 11/26/2025 2:32 PM 305 26.85 24.85 28.35 0.00 0.00% 22 0 70.17%
AAPL251205P00315000 10/31/2025 11:57 AM 315 44.25 36.35 38.15 0.00 0.00% 1 0 70.53%
AAPL251205P00320000 11/5/2025 2:10 PM 320 49.40 40.85 43.25 0.00 0.00% 45 0 73.49%
AAPL251205P00325000 11/3/2025 9:40 AM 325 57.23 46.55 48.35 0.00 0.00% 12 0 86.91%
AAPL251205P00330000 11/25/2025 12:19 PM 330 51.10 50.85 53.10 0.00 0.00% 1 0 83.98%

Related Tickers

+ + + +
\ No newline at end of file