"""Options flow — put/call ratios, IV smile, open interest by strike.""" from datetime import datetime from html import escape as _esc import pandas as pd import streamlit as st import streamlit.components.v1 as components from services.data_service import get_company_info, get_latest_price, get_options_chain _XMAP = {"NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ", "NCM": "NASDAQ", "ASE": "AMEX"} def _fmt_expiry_label(expiry: str) -> str: try: dt = datetime.strptime(expiry, "%Y-%m-%d") now_year = datetime.now().year if dt.year == now_year: return dt.strftime("%b %d") return dt.strftime("%b %d '%y") except Exception: return expiry def render_options(ticker: str): import json as _json info = get_company_info(ticker) or {} price = get_latest_price(ticker) data = get_options_chain(ticker) if not data or not data.get("chains"): st.info("Options data unavailable for this ticker.") return chains = data.get("chains", [])[:8] expirations = data.get("expirations", []) current_price = info.get("currentPrice") or info.get("regularMarketPrice") or price def _safe_list(df: pd.DataFrame, col: str) -> list: if col not in df.columns: return [] return pd.to_numeric(df[col], errors="coerce").fillna(0).tolist() def _chain_rows(df: pd.DataFrame) -> list[dict]: cols = ["strike", "bid", "ask", "lastPrice", "volume", "openInterest", "impliedVolatility", "inTheMoney"] rows = [] for _, r in df.iterrows(): row = {} for c in cols: if c in df.columns: v = r[c] row[c] = None if pd.isna(v) else v rows.append(row) return rows chain_list = [] for chain in chains: calls = chain.get("calls", pd.DataFrame()).copy() puts = chain.get("puts", pd.DataFrame()).copy() if current_price and not calls.empty: lo, hi = float(current_price) * 0.70, float(current_price) * 1.30 calls_atm = calls[(calls["strike"] >= lo) & (calls["strike"] <= hi)] if "strike" in calls.columns else calls puts_atm = puts[(puts["strike"] >= lo) & (puts["strike"] <= hi)] if "strike" in puts.columns else puts else: calls_atm, puts_atm = calls, puts total_call_vol = float(pd.to_numeric(calls.get("volume"), errors="coerce").fillna(0).sum()) if "volume" in calls.columns else 0.0 total_put_vol = float(pd.to_numeric(puts.get("volume"), errors="coerce").fillna(0).sum()) if "volume" in puts.columns else 0.0 total_call_oi = float(pd.to_numeric(calls.get("openInterest"), errors="coerce").fillna(0).sum()) if "openInterest" in calls.columns else 0.0 total_put_oi = float(pd.to_numeric(puts.get("openInterest"), errors="coerce").fillna(0).sum()) if "openInterest" in puts.columns else 0.0 entry = { "expiry": chain.get("expiry"), "pc_vol": (total_put_vol / total_call_vol) if total_call_vol > 0 else None, "pc_oi": (total_put_oi / total_call_oi) if total_call_oi > 0 else None, "call_vol": int(total_call_vol), "put_vol": int(total_put_vol), "call_oi": int(total_call_oi), "put_oi": int(total_put_oi), "iv_strikes_calls": _safe_list(calls_atm, "strike"), "iv_calls": _safe_list(calls_atm, "impliedVolatility"), "iv_strikes_puts": _safe_list(puts_atm, "strike"), "iv_puts": _safe_list(puts_atm, "impliedVolatility"), "oi_strikes_calls": _safe_list(calls_atm, "strike"), "oi_calls": _safe_list(calls_atm, "openInterest"), "oi_strikes_puts": _safe_list(puts_atm, "strike"), "oi_puts": _safe_list(puts_atm, "openInterest"), "rows_calls": _chain_rows(calls_atm), "rows_puts": _chain_rows(puts_atm), } chain_list.append(entry) first_chain = data["chains"][0] if data.get("chains") else {} calls0 = first_chain.get("calls", pd.DataFrame()) if current_price and not calls0.empty and "strike" in calls0.columns: calls0_atm = calls0[(calls0["strike"] >= float(current_price) * 0.70) & (calls0["strike"] <= float(current_price) * 1.30)] else: calls0_atm = calls0 n_chain_rows = max(len(calls0_atm), 20) height = 1500 + n_chain_rows * 28 chains_js = "const CHAINS=" + _json.dumps(chain_list) + ";" price_js = "const ATM_PRICE=" + _json.dumps(current_price or 0) + ";" prev_close = info.get("previousClose") if current_price and prev_close and prev_close > 0: chg_pct = (float(current_price) - float(prev_close)) / float(prev_close) * 100 sign = "+" if chg_pct >= 0 else "" arrow = "▲" if chg_pct >= 0 else "▼" chg_str = arrow + " " + sign + f"{chg_pct:.2f}%" chg_cls = "chg-pos" if chg_pct >= 0 else "chg-neg" else: chg_str, chg_cls = "—", "" raw_x = info.get("exchange") or "" exchange = _XMAP.get(raw_x, raw_x) or "—" co_name = _esc(info.get("longName") or info.get("shortName") or ticker.upper()) price_str = f"${float(current_price):,.2f}" if current_price else "—" exp_count = len(expirations) plotly_cdn = "" _ROOT = ( "" ) fonts_link = ( "" "" ) _OPTS_CSS = """""" ctx_html = ( '
Put/call ratios, implied volatility smile, and open interest by strike for ' + ticker.upper() + ". Data sourced from Yahoo Finance via yfinance.
" "