from __future__ import annotations import json import sys import warnings from pathlib import Path import gradio as gr import matplotlib.pyplot as plt import pandas as pd ROOT = Path(__file__).resolve().parent SRC = ROOT / "src" if str(SRC) not in sys.path: sys.path.insert(0, str(SRC)) from option_implied_lab.pipeline_v2 import analyze_ticker_v2 from option_implied_lab.universe import CURATED_TICKERS def _plot_density(density_df: pd.DataFrame): fig, ax = plt.subplots(figsize=(7, 4)) ax.plot(density_df["strike"], density_df["density"], linewidth=2) ax.set_title("Implied Density") ax.set_xlabel("Strike") ax.set_ylabel("Density proxy") ax.grid(alpha=0.25) fig.tight_layout() return fig def _prepare_ticker(ticker_pick: str, custom_ticker: str) -> str: ticker = ( custom_ticker.strip().upper() if custom_ticker.strip() else ticker_pick.strip().upper() ) if not ticker: raise gr.Error("Ticker is required") return ticker def _prepare_expiry(expiry_mode: str, manual_expiry: str): mode = "auto" expiry = None if expiry_mode == "Manual expiry": mode = "manual" if not manual_expiry.strip(): raise gr.Error("Manual expiry selected; enter date like 2026-06-19") try: expiry = pd.to_datetime(manual_expiry).to_pydatetime() except Exception as exc: raise gr.Error(f"Could not parse expiry date: {exc}") return mode, expiry def _enrich_strategy_table(scored_df: pd.DataFrame, risk_lambda: float) -> pd.DataFrame: if scored_df.empty: return scored_df out = scored_df.copy() out["downside_penalty"] = out["downside_q05"].apply(lambda x: max(-float(x), 0.0)) out["objective_from_formula"] = ( out["expected_payoff"] - float(risk_lambda) * out["downside_penalty"] ) out = out.sort_values("objective", ascending=False).reset_index(drop=True) return out[ [ "strategy", "expected_payoff", "downside_q05", "downside_penalty", "objective", "objective_from_formula", ] ] def _diagnostics_note(diag_df: pd.DataFrame) -> str: if diag_df.empty: return "No diagnostics output available." failed_checks = int((~diag_df["passed"]).sum()) mean_rate = float(diag_df["violation_rate"].mean()) if failed_checks == 0: return "Diagnostics are clean for this snapshot." return ( f"Diagnostics are noisy ({failed_checks} failed checks, mean violation rate {mean_rate:.2%}). " "Treat these outputs as research signals, not direct execution instructions." ) def _strategy_note(scored_df: pd.DataFrame, risk_lambda: float) -> str: if scored_df.empty: return "No strategy scores available." top = scored_df.iloc[0] return ( "Score formula: objective = E[payoff] - lambda * max(-q05, 0). " f"Current lambda = {float(risk_lambda):.2f}. " f"Top strategy = {top['strategy']} with objective {float(top['objective']):.4f}." ) def _worked_example_md(scored_df: pd.DataFrame, risk_lambda: float) -> str: if scored_df.empty: return "No worked example available." top = scored_df.iloc[0] return ( "### Worked example from this run\n" f"- strategy: `{top['strategy']}`\n" f"- expected_payoff: `{float(top['expected_payoff']):.4f}`\n" f"- downside_q05: `{float(top['downside_q05']):.4f}`\n" f"- downside_penalty: `max(-q05, 0) = {float(top['downside_penalty']):.4f}`\n" f"- lambda: `{float(risk_lambda):.2f}`\n" f"- objective: `{float(top['expected_payoff']):.4f} - {float(risk_lambda):.2f} * {float(top['downside_penalty']):.4f} = {float(top['objective_from_formula']):.4f}`" ) def _arbitrage_table_for_display(cands_df: pd.DataFrame) -> pd.DataFrame: if cands_df.empty: return cands_df out = cands_df.copy() for col in ["k2", "k3"]: if col in out.columns: out[col] = out.apply( lambda r: "n/a" if r.get("family") in {"parity", "calendar"} and pd.isna(r[col]) else r[col], axis=1, ) return out def run_walkthrough( ticker_pick: str, custom_ticker: str, expiry_mode: str, manual_expiry: str, moneyness_band: float, min_open_interest: int, risk_lambda: float, ): ticker = _prepare_ticker(ticker_pick, custom_ticker) mode, expiry = _prepare_expiry(expiry_mode, manual_expiry) try: with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning, module="oipd") out = analyze_ticker_v2( ticker=ticker, max_expiries=8, expiry=expiry, expiry_mode=mode, moneyness_band=moneyness_band, min_open_interest=min_open_interest, min_volume=0, risk_lambda=risk_lambda, ) except Exception as exc: raise gr.Error(f"Analysis failed: {exc}") scored = _enrich_strategy_table(out["scored"], risk_lambda=float(risk_lambda)) diagnostics_note = _diagnostics_note(out["diagnostics"]) strategy_note = _strategy_note(scored, risk_lambda=float(risk_lambda)) worked_example = _worked_example_md(scored, risk_lambda=float(risk_lambda)) density_fig = _plot_density(out["density"]) run_summary = ( f"Ticker: {out['ticker']} | Spot: {out['spot']:.2f} | " f"Expiry: {out['selected_expiry'].date()}" ) best_json = json.dumps(out["best_strategy"], indent=2, default=str) return ( run_summary, diagnostics_note, out["diagnostics"], density_fig, scored, strategy_note, worked_example, out["arbitrage_summary"], _arbitrage_table_for_display(out["arbitrage_candidates"]), best_json, ) def run_sweep( sweep_size: int, moneyness_band: float, min_open_interest: int, risk_lambda: float, focus_override: str, ): tickers = CURATED_TICKERS[: int(sweep_size)] rows: list[dict[str, object]] = [] errors: list[dict[str, str]] = [] artifacts: dict[str, dict[str, object]] = {} with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning, module="oipd") for ticker in tickers: try: out = analyze_ticker_v2( ticker=ticker, max_expiries=8, moneyness_band=moneyness_band, min_open_interest=min_open_interest, min_volume=0, risk_lambda=risk_lambda, ) artifacts[ticker] = out diag = out["diagnostics"] arb = out["arbitrage_summary"].iloc[0] high = int(arb.get("high_conf_count", 0)) medium = int(arb.get("medium_conf_count", 0)) low = int(arb.get("low_conf_count", 0)) score = 3.0 * high + 1.0 * medium + 0.2 * low rows.append( { "ticker": ticker, "status": "ok", "selected_expiry": out["selected_expiry"], "diag_mean_violation_rate": float( diag["violation_rate"].mean() ), "best_strategy": out["best_strategy"]["name"], "best_objective": float(out["best_strategy"]["objective"]), "arb_candidates": int(arb["candidate_count"]), "arb_max_edge": float(arb["max_edge"]), "high_conf": high, "medium_conf": medium, "low_conf": low, "score": score, } ) except Exception as exc: msg = str(exc) errors.append({"ticker": ticker, "error": msg}) rows.append({"ticker": ticker, "status": f"error: {msg}"}) sweep_df = pd.DataFrame(rows) ok = sweep_df[sweep_df["status"] == "ok"].copy() ranked = ( ok.sort_values( ["score", "arb_candidates", "arb_max_edge"], ascending=[False, False, False] ).reset_index(drop=True) if not ok.empty else pd.DataFrame() ) errors_df = pd.DataFrame(errors) if ranked.empty: fig, ax = plt.subplots(figsize=(7, 4)) ax.set_title("No successful sweep results") ax.axis("off") return ( f"Sweep complete: 0 success, {len(errors)} errors.", pd.DataFrame(), errors_df, "", "{}", pd.DataFrame(), fig, ) focus_ticker = str(ranked.iloc[0]["ticker"]) override = (focus_override or "").strip().upper() summary = ( f"Sweep complete: {len(ok)} success, {len(errors)} errors. " f"Top ticker by score: {focus_ticker}." ) if override: if override in artifacts: focus_ticker = override summary = summary + f" Focus override applied: {focus_ticker}." else: summary = ( summary + f" Override '{override}' not available; kept {focus_ticker}." ) focus_out = artifacts[focus_ticker] focus_best = json.dumps(focus_out["best_strategy"], indent=2, default=str) focus_candidates = _arbitrage_table_for_display( pd.DataFrame(focus_out["arbitrage_candidates"]).head(25) ) focus_density = _plot_density(pd.DataFrame(focus_out["density"])) return ( summary, ranked.head(25), errors_df, focus_ticker, focus_best, focus_candidates, focus_density, ) with gr.Blocks(title="Option-Implied Strategy Lab") as demo: gr.Markdown( """ # Option-Implied Strategy Lab We start from market option prices and work backward to infer what shape of future outcomes the market is implying. Then we use that shape to compare strategies and flag possible pricing inconsistencies. Live demo: https://huggingface.co/spaces/junaid-hasan/implied-lab """ ) with gr.Tab("Demo Walkthrough"): gr.Markdown( r""" ## Big-picture introduction This app follows the same flow as `notebooks/demo.ipynb`: 1) run diagnostics, 2) view implied density, 3) compare strategy scores, 4) inspect arbitrage candidates. Breeden-Litzenberger idea (under European assumptions): $$f_{RN}(K) = e^{rT} \frac{\partial^2 C(K,T)}{\partial K^2}$$ Plain meaning: the way call prices bend across strikes gives a market-implied probability shape. ### Strategy meanings - `long_stock`: buy and hold the underlying with no option hedge. - `protective_put`: hold stock and buy a put to limit large downside losses. - `collar`: hold stock, buy a put, and sell a call to reduce hedge cost while capping upside. ### Arbitrage candidate families - `vertical`: checks strike ordering consistency. - `butterfly`: checks three-strike curvature consistency. - `parity`: checks call-put parity at one strike and expiry. - `calendar`: checks maturity ordering at the same strike. """ ) with gr.Row(): ticker_pick = gr.Dropdown( choices=CURATED_TICKERS, value="GOOGL", label="Ticker (curated)", filterable=True, ) custom_ticker = gr.Textbox( label="Custom ticker (optional)", value="", placeholder="Leave blank to use curated ticker", ) expiry_mode = gr.Dropdown( choices=["Auto (best quality)", "Manual expiry"], value="Auto (best quality)", label="Expiry mode", ) manual_expiry = gr.Textbox( label="Manual expiry (YYYY-MM-DD)", value="", placeholder="2026-06-19", ) with gr.Row(): moneyness_band = gr.Slider( 0.1, 0.4, value=0.2, step=0.05, label="Moneyness band (+/-)", ) min_open_interest = gr.Slider( 0, 2000, value=1, step=1, label="Min open interest", ) risk_lambda = gr.Slider( 0.0, 2.0, value=0.5, step=0.1, label="Risk lambda", ) gr.Examples( label="Quick examples", examples=[ ["GOOGL", "", "Auto (best quality)", "", 0.2, 1, 0.5], ["IWM", "", "Auto (best quality)", "", 0.2, 1, 0.5], ["NVDA", "", "Auto (best quality)", "", 0.2, 1, 0.5], ], inputs=[ ticker_pick, custom_ticker, expiry_mode, manual_expiry, moneyness_band, min_open_interest, risk_lambda, ], ) run_btn = gr.Button("Run walkthrough") run_summary = gr.Textbox(label="Run summary") diagnostics_note = gr.Textbox(label="Diagnostics note") diagnostics_table = gr.Dataframe(label="Diagnostics") density_plot = gr.Plot(label="Implied density") strategy_table = gr.Dataframe(label="Strategy ranking") strategy_note = gr.Textbox(label="Strategy note") worked_example = gr.Markdown(label="Objective worked example") arbitrage_summary = gr.Dataframe(label="Arbitrage summary") arbitrage_candidates = gr.Dataframe(label="Arbitrage candidates") best_strategy_json = gr.Code(label="Best strategy JSON", language="json") run_btn.click( fn=run_walkthrough, inputs=[ ticker_pick, custom_ticker, expiry_mode, manual_expiry, moneyness_band, min_open_interest, risk_lambda, ], outputs=[ run_summary, diagnostics_note, diagnostics_table, density_plot, strategy_table, strategy_note, worked_example, arbitrage_summary, arbitrage_candidates, best_strategy_json, ], ) with gr.Tab("Universe Sweep"): gr.Markdown( r""" Sweep the curated universe and rank tickers with confidence-weighted score: $$\text{score} = 3\cdot\text{high} + 1\cdot\text{medium} + 0.2\cdot\text{low}$$ """ ) with gr.Row(): sweep_size = gr.Slider( 5, min(len(CURATED_TICKERS), 60), value=12, step=1, label="Sweep size", ) sweep_band = gr.Slider( 0.1, 0.4, value=0.2, step=0.05, label="Moneyness band (+/-)", ) sweep_min_oi = gr.Slider( 0, 2000, value=1, step=1, label="Min open interest", ) sweep_lambda = gr.Slider( 0.0, 2.0, value=0.5, step=0.1, label="Risk lambda", ) focus_override = gr.Dropdown( choices=CURATED_TICKERS, value="", label="Focus ticker override (optional)", filterable=True, allow_custom_value=True, ) gr.Examples( label="Sweep examples", examples=[ [12, 0.2, 1, 0.5, ""], [20, 0.2, 1, 0.5, "GOOGL"], [15, 0.25, 1, 0.7, ""], ], inputs=[ sweep_size, sweep_band, sweep_min_oi, sweep_lambda, focus_override, ], ) sweep_btn = gr.Button("Run sweep") sweep_summary = gr.Textbox(label="Sweep summary") ranked_table = gr.Dataframe(label="Ranked tickers") sweep_errors = gr.Dataframe(label="Sweep errors") focus_ticker = gr.Textbox(label="Focus ticker") focus_best = gr.Code(label="Focus best strategy JSON", language="json") focus_candidates = gr.Dataframe(label="Focus arbitrage candidates") focus_density = gr.Plot(label="Focus density") sweep_btn.click( fn=run_sweep, inputs=[ sweep_size, sweep_band, sweep_min_oi, sweep_lambda, focus_override, ], outputs=[ sweep_summary, ranked_table, sweep_errors, focus_ticker, focus_best, focus_candidates, focus_density, ], ) if __name__ == "__main__": demo.launch()