Files
vault-dash/app/components/strategy_panel.py
Bu5hm4nn 6bcf78e5df style: format UI files and remove lint excludes
- Remove app/components/ and app/pages/ from ruff/black excludes
- Pre-commit reformatted multi-line strings for consistency
- All files now follow the same code style
2026-04-01 13:55:55 +02:00

183 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"""
<div class=\"h-full rounded-xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-950\">
<div class=\"mb-3 flex items-start justify-between gap-3\">
<div>
<div class=\"text-base font-semibold text-slate-900 dark:text-slate-100\">{name}</div>
<div class=\"mt-1 text-sm text-slate-500 dark:text-slate-400\">{description}</div>
</div>
<div class=\"rounded-full bg-slate-900 px-2 py-1 text-xs font-semibold text-white dark:bg-slate-100 dark:text-slate-900\">Live Scenario</div>
</div>
<div class=\"grid grid-cols-2 gap-3\">
<div class=\"rounded-lg bg-white p-3 dark:bg-slate-900\">
<div class=\"text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400\">Est. Cost</div>
<div class=\"mt-1 text-lg font-bold text-slate-900 dark:text-slate-100\">${cost:,.2f}</div>
</div>
<div class=\"rounded-lg bg-white p-3 dark:bg-slate-900\">
<div class=\"text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400\">Scenario Benefit</div>
<div class=\"mt-1 text-lg font-bold {payoff_class}\">${payoff:,.2f}</div>
</div>
</div>
</div>
"""
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"""
<tr class=\"border-b border-slate-200 dark:border-slate-800\">
<td class=\"px-4 py-3 font-medium text-slate-900 dark:text-slate-100\">{name}</td>
<td class=\"px-4 py-3 text-slate-600 dark:text-slate-300\">${cost:,.2f}</td>
<td class=\"px-4 py-3 text-slate-600 dark:text-slate-300\">{self._format_floor(strategy)}</td>
<td class=\"px-4 py-3 text-slate-600 dark:text-slate-300\">{self._format_cap(strategy)}</td>
<td class=\"px-4 py-3 font-semibold {scenario_class}\">${scenario:,.2f}</td>
</tr>
"""
)
return f"""
<div class=\"overflow-x-auto\">
<table class=\"min-w-full rounded-xl overflow-hidden\">
<thead class=\"bg-slate-100 dark:bg-slate-800\">
<tr>
<th class=\"px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300\">Strategy</th>
<th class=\"px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300\">Estimated Cost</th>
<th class=\"px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300\">Protection Floor</th>
<th class=\"px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300\">Upside Cap</th>
<th class=\"px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300\">Scenario Benefit</th>
</tr>
</thead>
<tbody>{''.join(rows) if rows else self._empty_row()}</tbody>
</table>
</div>
"""
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 (
'<tr><td colspan="5" class="px-4 py-6 text-center text-slate-500 dark:text-slate-400">'
"No strategies loaded"
"</td></tr>"
)