from __future__ import annotations from collections.abc import Iterator from contextlib import contextmanager from typing import Any from nicegui import ui from app.services.strategy_templates import StrategyTemplateService NAV_ITEMS: list[tuple[str, str, str]] = [ ("overview", "/", "Overview"), ("hedge", "/hedge", "Hedge Analysis"), ("options", "/options", "Options Chain"), ("backtests", "/backtests", "Backtests"), ("settings", "/settings", "Settings"), ] def demo_spot_price() -> float: return 215.0 def portfolio_snapshot() -> dict[str, float]: gold_units = 1_000.0 spot = demo_spot_price() gold_value = gold_units * spot loan_amount = 145_000.0 margin_call_ltv = 0.75 return { "gold_value": gold_value, "loan_amount": loan_amount, "ltv_ratio": loan_amount / gold_value, "net_equity": gold_value - loan_amount, "spot_price": spot, "margin_call_ltv": margin_call_ltv, "margin_call_price": loan_amount / (margin_call_ltv * gold_units), "cash_buffer": 18_500.0, "hedge_budget": 8_000.0, } def strategy_catalog() -> list[dict[str, Any]]: return StrategyTemplateService().catalog_items() def quick_recommendations(portfolio: dict[str, Any] | None = None) -> list[dict[str, str]]: portfolio = portfolio or portfolio_snapshot() ltv_gap = (portfolio["margin_call_ltv"] - portfolio["ltv_ratio"]) * 100 return [ { "title": "Balanced hedge favored", "summary": "A 95% protective put balances margin-call protection with a lower upfront hedge cost.", "tone": "positive", }, { "title": f"{ltv_gap:.1f} pts LTV headroom", "summary": "You still have room before a margin trigger, so prefer cost-efficient protection over maximum convexity.", "tone": "info", }, { "title": "Roll window approaching", "summary": "Stage long-dated puts now and keep a near-dated layer for event risk over the next quarter.", "tone": "warning", }, ] def option_chain() -> list[dict[str, Any]]: spot = demo_spot_price() expiries = ["2026-04-17", "2026-06-19", "2026-09-18"] strikes = [190.0, 200.0, 210.0, 215.0, 220.0, 230.0] rows: list[dict[str, Any]] = [] for expiry in expiries: for strike in strikes: distance = (strike - spot) / spot for option_type in ("put", "call"): premium_base = 8.2 if option_type == "put" else 7.1 premium = round( max( 1.1, premium_base - abs(distance) * 18 + (0.8 if expiry == "2026-09-18" else 0.0), ), 2, ) delta = round((0.5 - distance * 1.8) * (-1 if option_type == "put" else 1), 3) rows.append( { "symbol": f"GLD {expiry} {option_type.upper()} {strike:.0f}", "expiry": expiry, "type": option_type, "strike": strike, "premium": premium, "bid": round(max(premium - 0.18, 0.5), 2), "ask": round(premium + 0.18, 2), "open_interest": int(200 + abs(spot - strike) * 14), "volume": int(75 + abs(spot - strike) * 8), "delta": max(-0.95, min(0.95, delta)), "gamma": round(max(0.012, 0.065 - abs(distance) * 0.12), 3), "theta": round(-0.014 - abs(distance) * 0.025, 3), "vega": round(0.09 + max(0.0, 0.24 - abs(distance) * 0.6), 3), "rho": round( (0.04 + abs(distance) * 0.09) * (-1 if option_type == "put" else 1), 3, ), } ) return rows def strategy_metrics(strategy_name: str, scenario_pct: int) -> dict[str, Any]: strategy = next( (item for item in strategy_catalog() if item["name"] == strategy_name), strategy_catalog()[0], ) spot = demo_spot_price() floor = float(strategy.get("max_drawdown_floor", spot * 0.95)) cap = strategy.get("upside_cap") cost = float(strategy["estimated_cost"]) scenario_prices = [round(spot * (1 + pct / 100), 2) for pct in range(-25, 30, 5)] benefits: list[float] = [] for price in scenario_prices: payoff = max(floor - price, 0.0) if isinstance(cap, (int, float)) and price > float(cap): payoff -= price - float(cap) benefits.append(round(payoff - cost, 2)) scenario_price = round(spot * (1 + scenario_pct / 100), 2) unhedged_equity = scenario_price * 1_000 - 145_000.0 scenario_payoff = max(floor - scenario_price, 0.0) capped_upside = 0.0 if isinstance(cap, (int, float)) and scenario_price > float(cap): capped_upside = -(scenario_price - float(cap)) hedged_equity = unhedged_equity + scenario_payoff + capped_upside - cost * 1_000 waterfall_steps = [ ("Base equity", round(70_000.0, 2)), ("Spot move", round((scenario_price - spot) * 1_000, 2)), ("Option payoff", round(scenario_payoff * 1_000, 2)), ("Call cap", round(capped_upside * 1_000, 2)), ("Hedge cost", round(-cost * 1_000, 2)), ("Net equity", round(hedged_equity, 2)), ] return { "strategy": strategy, "scenario_pct": scenario_pct, "scenario_price": scenario_price, "scenario_series": [ {"price": price, "benefit": benefit} for price, benefit in zip(scenario_prices, benefits, strict=True) ], "waterfall_steps": waterfall_steps, "unhedged_equity": round(unhedged_equity, 2), "hedged_equity": round(hedged_equity, 2), } @contextmanager def dashboard_page(title: str, subtitle: str, current: str) -> Iterator[ui.column]: ui.colors(primary="#0f172a", secondary="#1e293b", accent="#0ea5e9") # Header must be at page level, not inside container with ui.header(elevated=False).classes( "items-center justify-between border-b border-slate-200 bg-white/90 px-6 py-4 backdrop-blur dark:border-slate-800 dark:bg-slate-950/90" ): with ui.row().classes("items-center gap-3"): ui.icon("shield").classes("text-2xl text-sky-500") with ui.column().classes("gap-0"): ui.label("Vault Dashboard").classes("text-lg font-bold text-slate-900 dark:text-slate-50") ui.label("NiceGUI hedging cockpit").classes("text-xs text-slate-500 dark:text-slate-400") with ui.row().classes("items-center gap-2 max-sm:flex-wrap"): for key, href, label in NAV_ITEMS: active = key == current link_classes = "rounded-lg px-4 py-2 text-sm font-medium no-underline transition " + ( "bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900" if active else "text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800" ) ui.link(label, href).classes(link_classes) with ui.column().classes("mx-auto w-full max-w-7xl gap-6 bg-slate-50 p-6 dark:bg-slate-950") as container: with ui.row().classes("w-full items-end justify-between gap-4 max-md:flex-col max-md:items-start"): with ui.column().classes("gap-1"): ui.label(title).classes("text-3xl font-bold text-slate-900 dark:text-slate-50") ui.label(subtitle).classes("text-slate-500 dark:text-slate-400") yield container def recommendation_style(tone: str) -> str: return { "positive": "border-emerald-200 bg-emerald-50 dark:border-emerald-900/60 dark:bg-emerald-950/30", "warning": "border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30", "info": "border-sky-200 bg-sky-50 dark:border-sky-900/60 dark:bg-sky-950/30", }.get(tone, "border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900")