from __future__ import annotations from typing import Any from nicegui import ui from app.components import GreeksTable from app.pages.common import dashboard_page, split_page_panes, strategy_catalog from app.services.runtime import get_data_service @ui.page("/options") async def options_page() -> None: data_service = get_data_service() expirations_data = await data_service.get_option_expirations("GLD") expiries = list(expirations_data.get("expirations") or []) default_expiry = expiries[0] if expiries else None chain_data = await data_service.get_options_chain_for_expiry("GLD", default_expiry) chain_state = { "data": chain_data, "rows": list(chain_data.get("rows") or [*chain_data.get("calls", []), *chain_data.get("puts", [])]), } selected_expiry = {"value": chain_data.get("selected_expiry") or default_expiry} selected_strategy = {"value": strategy_catalog()[0]["label"]} chosen_contracts: list[dict[str, Any]] = [] def strike_bounds(rows: list[dict[str, Any]]) -> tuple[float, float]: strike_values = sorted({float(row["strike"]) for row in rows}) if not strike_values: return 0.0, 0.0 return strike_values[0], strike_values[-1] initial_min_strike, initial_max_strike = strike_bounds(chain_state["rows"]) strike_range = {"min": initial_min_strike, "max": initial_max_strike} with dashboard_page( "Options Chain", "Browse GLD contracts, filter by expiry and strike range, inspect Greeks, and attach contracts to hedge workflows.", "options", ): left_pane, right_pane = split_page_panes( left_testid="options-left-pane", right_testid="options-right-pane", ) with left_pane: with ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ): ui.label("Filters").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") expiry_select = ui.select(expiries, value=selected_expiry["value"], label="Expiry").classes("w-full") min_strike = ui.number("Min strike", value=strike_range["min"], step=5).classes("w-full") max_strike = ui.number("Max strike", value=strike_range["max"], step=5).classes("w-full") strategy_select = ui.select( [item["label"] for item in strategy_catalog()], value=selected_strategy["value"], label="Add to hedge strategy", ).classes("w-full") source_html = ui.html("").classes("text-xs text-slate-500 dark:text-slate-400") error_html = ui.html("").classes("text-xs text-amber-700 dark:text-amber-300") loading_html = ui.html("").classes("text-xs text-sky-700 dark:text-sky-300") selection_card = ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ) with right_pane: chain_table = ui.html("").classes("w-full") with ui.row().classes("w-full gap-6 max-xl:flex-col"): greeks = GreeksTable([]) quick_add = ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ) def sync_status() -> None: current_data = chain_state["data"] source_label = f"Source: {current_data.get('source', 'unknown')}" if current_data.get("updated_at"): source_label += f" · Updated {current_data['updated_at']}" source_html.content = source_label source_html.update() error_message = current_data.get("error") or expirations_data.get("error") error_html.content = f"Options data unavailable: {error_message}" if error_message else "" error_html.update() def filtered_rows() -> list[dict[str, Any]]: return [ row for row in chain_state["rows"] if strike_range["min"] <= float(row["strike"]) <= strike_range["max"] ] def render_selection() -> None: selection_card.clear() with selection_card: ui.label("Strategy Integration").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") ui.label(f"Target strategy: {selected_strategy['value']}").classes( "text-sm text-slate-500 dark:text-slate-400" ) if not chosen_contracts: ui.label("No contracts added yet.").classes("text-sm text-slate-500 dark:text-slate-400") return with ui.column().classes("w-full gap-3"): for contract in chosen_contracts[-3:]: with ui.card().classes( "rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950" ): ui.label(contract["symbol"]).classes("font-semibold text-slate-900 dark:text-slate-100") ui.label( f"Premium ${float(contract['premium']):.2f} · IV {float(contract.get('impliedVolatility', 0.0)):.1%}" ).classes("text-sm text-slate-600 dark:text-slate-300") def add_to_strategy(contract: dict[str, Any]) -> None: chosen_contracts.append(contract) render_selection() greeks.set_options(chosen_contracts[-6:]) ui.notify(f"Added {contract['symbol']} to {selected_strategy['value']}", color="positive") def render_chain() -> None: rows = filtered_rows() chain_table.content = ( """
""" + "".join( f""" """ for row in rows ) + ( "" if rows else "" ) + """
Contract Type Strike Bid / Ask Last IV Greeks Action
{row['symbol']} {row['type'].upper()} ${float(row['strike']):.2f} ${float(row['bid']):.2f} / ${float(row['ask']):.2f} ${float(row.get('lastPrice', row.get('premium', 0.0))):.2f} {float(row.get('impliedVolatility', 0.0)):.1%} Δ {float(row.get('delta', 0.0)):+.3f} · Γ {float(row.get('gamma', 0.0)):.3f} · Θ {float(row.get('theta', 0.0)):+.3f} · V {float(row.get('vega', 0.0)):.3f} Use quick-add buttons below
No contracts match the current filter.
""" ) chain_table.update() quick_add.clear() with quick_add: ui.label("Quick add to hedge").classes( "text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400" ) with ui.row().classes("w-full gap-2 max-sm:flex-col"): for row in rows[:6]: ui.button( f"Add {row['type'].upper()} {float(row['strike']):.0f}", on_click=lambda _, contract=row: add_to_strategy(contract), ).props("outline color=primary") greeks.set_options(chosen_contracts[-6:] if chosen_contracts else rows[:6]) async def load_expiry_chain(expiry: str | None) -> None: selected_expiry["value"] = expiry loading_html.content = "Loading selected expiry…" if expiry else "" loading_html.update() next_chain = await data_service.get_options_chain_for_expiry("GLD", expiry) chain_state["data"] = next_chain chain_state["rows"] = list(next_chain.get("rows") or [*next_chain.get("calls", []), *next_chain.get("puts", [])]) min_value, max_value = strike_bounds(chain_state["rows"]) strike_range["min"] = min_value strike_range["max"] = max_value min_strike.value = min_value max_strike.value = max_value loading_html.content = "" loading_html.update() sync_status() render_chain() def update_filters() -> None: strike_range["min"] = float(min_strike.value or 0.0) strike_range["max"] = float(max_strike.value or 0.0) if strike_range["min"] > strike_range["max"]: strike_range["min"], strike_range["max"] = (strike_range["max"], strike_range["min"]) min_strike.value = strike_range["min"] max_strike.value = strike_range["max"] render_chain() async def on_expiry_change(event: Any) -> None: await load_expiry_chain(event.value) expiry_select.on_value_change(on_expiry_change) min_strike.on_value_change(lambda _: update_filters()) max_strike.on_value_change(lambda _: update_filters()) def on_strategy_change(event: Any) -> None: selected_strategy["value"] = event.value render_selection() strategy_select.on_value_change(on_strategy_change) sync_status() render_selection() render_chain()