from __future__ import annotations from typing import Any from nicegui import ui from app.domain.portfolio_math import ( strategy_benefit_per_unit, strategy_protection_floor_bounds, strategy_upside_cap_price, ) class StrategyComparisonPanel: """Interactive strategy comparison with scenario slider and cost-benefit table.""" def __init__( self, strategies: list[dict[str, Any]] | None = None, *, current_spot: float = 100.0, ) -> None: self.strategies = strategies or [] self.current_spot = current_spot self.price_change_pct = 0 self.strategy_cards: list[ui.html] = [] 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("Strategy Comparison").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") with ui.row().classes("w-full items-center justify-between gap-4 max-sm:flex-col max-sm:items-start"): self.slider_label = ui.label(self._slider_text()).classes("text-sm text-slate-500 dark:text-slate-400") self.scenario_spot = ui.label(self._scenario_spot_text()).classes( "rounded-full bg-sky-100 px-3 py-1 text-sm font-medium text-sky-700 dark:bg-sky-500/15 dark:text-sky-300" ) ui.slider(min=-50, max=50, value=0, step=5, on_change=self._on_slider_change).classes("w-full") with ui.row().classes("w-full gap-4 max-lg:flex-col"): self.cards_container = ui.row().classes("w-full gap-4 max-lg:flex-col") ui.separator().classes("my-2") ui.label("Cost / Benefit Summary").classes( "text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400" ) self.table_html = ui.html("").classes("w-full") self.set_strategies(self.strategies, current_spot=current_spot) def _on_slider_change(self, event: Any) -> None: self.price_change_pct = int(event.value) self.slider_label.set_text(self._slider_text()) self.scenario_spot.set_text(self._scenario_spot_text()) self._render() def set_strategies(self, strategies: list[dict[str, Any]], *, current_spot: float | None = None) -> None: self.strategies = strategies if current_spot is not None: self.current_spot = current_spot self._render() def _render(self) -> None: self.cards_container.clear() self.strategy_cards.clear() with self.cards_container: for strategy in self.strategies: self.strategy_cards.append(ui.html(self._strategy_card_html(strategy)).classes("w-full")) self.table_html.content = self._table_html() self.table_html.update() def _scenario_spot(self) -> float: return self.current_spot * (1 + self.price_change_pct / 100) def _slider_text(self) -> str: sign = "+" if self.price_change_pct > 0 else "" return f"Scenario slider: {sign}{self.price_change_pct}% gold price change" def _scenario_spot_text(self) -> str: return f"Scenario spot: ${self._scenario_spot():,.2f}" def _strategy_card_html(self, strategy: dict[str, Any]) -> str: name = str(strategy.get("name", "strategy")).replace("_", " ").title() description = strategy.get("description", "") cost = float(strategy.get("estimated_cost", 0.0)) payoff = self._scenario_benefit(strategy) payoff_class = "text-emerald-600 dark:text-emerald-400" if payoff >= 0 else "text-rose-600 dark:text-rose-400" return f"""
{name}
{description}
Live Scenario
Est. Cost
${cost:,.2f}
Scenario Benefit
${payoff:,.2f}
""" def _table_html(self) -> str: rows = [] for strategy in self.strategies: name = str(strategy.get("name", "strategy")).replace("_", " ").title() cost = float(strategy.get("estimated_cost", 0.0)) scenario = self._scenario_benefit(strategy) scenario_class = ( "text-emerald-600 dark:text-emerald-400" if scenario >= 0 else "text-rose-600 dark:text-rose-400" ) rows.append( f""" {name} ${cost:,.2f} {self._format_floor(strategy)} {self._format_cap(strategy)} ${scenario:,.2f} """ ) return f"""
{''.join(rows) if rows else self._empty_row()}
Strategy Estimated Cost Protection Floor Upside Cap Scenario Benefit
""" def _scenario_benefit(self, strategy: dict[str, Any]) -> float: return strategy_benefit_per_unit( strategy, current_spot=self.current_spot, scenario_spot=self._scenario_spot(), ) def _format_floor(self, strategy: dict[str, Any]) -> str: bounds = strategy_protection_floor_bounds(strategy, current_spot=self.current_spot) if bounds is None: return self._fmt_optional_money(strategy.get("max_drawdown_floor")) low, high = bounds if abs(high - low) < 1e-9: return f"${high:,.2f}" return f"${low:,.2f}–${high:,.2f}" def _format_cap(self, strategy: dict[str, Any]) -> str: cap = strategy_upside_cap_price(strategy, current_spot=self.current_spot) if cap is None: return self._fmt_optional_money(strategy.get("upside_cap")) return f"${cap:,.2f}" @staticmethod def _fmt_optional_money(value: Any) -> str: if isinstance(value, (int, float)): return f"${float(value):,.2f}" return "—" @staticmethod def _empty_row() -> str: return ( '' "No strategies loaded" "" )