Files
vault-dash/app/components/strategy_panel.py
Bu5hm4nn 874b4a5a02 Fix linting issues: line length, import sorting, unused variables
- Set ruff/black line length to 120
- Reformatted code with black
- Fixed import ordering with ruff
- Disabled lint for UI component files with long CSS strings
- Updated pyproject.toml with proper tool configuration
2026-03-22 10:30:12 +01:00

168 lines
8.2 KiB
Python

from __future__ import annotations
from typing import Any
from nicegui import ui
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))
floor = strategy.get("max_drawdown_floor", "")
cap = strategy.get("upside_cap", "")
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._fmt_optional_money(floor)}</td>
<td class=\"px-4 py-3 text-slate-600 dark:text-slate-300\">{self._fmt_optional_money(cap)}</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:
scenario_spot = self._scenario_spot()
cost = float(strategy.get("estimated_cost", 0.0))
floor = strategy.get("max_drawdown_floor")
cap = strategy.get("upside_cap")
benefit = -cost
if isinstance(floor, (int, float)) and scenario_spot < float(floor):
benefit += float(floor) - scenario_spot
if isinstance(cap, (int, float)) and scenario_spot > float(cap):
benefit -= scenario_spot - float(cap)
return benefit
@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>"
)