Initial commit: Vault Dashboard for options hedging
- FastAPI + NiceGUI web application - QuantLib-based Black-Scholes pricing with Greeks - Protective put, laddered, and LEAPS strategies - Real-time WebSocket updates - TradingView-style charts via Lightweight-Charts - Docker containerization - GitLab CI/CD pipeline for VPS deployment - VPN-only access configuration
This commit is contained in:
68
app/components/portfolio_view.py
Normal file
68
app/components/portfolio_view.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class PortfolioOverview:
|
||||
"""Portfolio summary card with LTV risk coloring and margin warning."""
|
||||
|
||||
def __init__(self, *, margin_call_ltv: float = 0.75) -> None:
|
||||
self.margin_call_ltv = margin_call_ltv
|
||||
|
||||
with ui.card().classes("w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"):
|
||||
with ui.row().classes("w-full items-center justify-between"):
|
||||
ui.label("Portfolio Overview").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
self.warning_badge = ui.label("Monitoring").classes(
|
||||
"rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700 dark:bg-amber-500/15 dark:text-amber-300"
|
||||
)
|
||||
|
||||
with ui.grid(columns=2).classes("w-full gap-4 max-sm:grid-cols-1"):
|
||||
self.gold_value = self._metric_card("Current Gold Value")
|
||||
self.loan_amount = self._metric_card("Loan Amount")
|
||||
self.ltv = self._metric_card("Current LTV")
|
||||
self.net_equity = self._metric_card("Net Equity")
|
||||
|
||||
def _metric_card(self, label: str) -> ui.label:
|
||||
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(label).classes("text-sm font-medium text-slate-500 dark:text-slate-400")
|
||||
return ui.label("--").classes("text-2xl font-bold text-slate-900 dark:text-slate-50")
|
||||
|
||||
def update(self, portfolio: dict[str, Any], *, margin_call_ltv: float | None = None) -> None:
|
||||
threshold = margin_call_ltv if margin_call_ltv is not None else portfolio.get("margin_call_ltv", self.margin_call_ltv)
|
||||
gold_value = float(portfolio.get("gold_value", portfolio.get("portfolio_value", 0.0)))
|
||||
loan_amount = float(portfolio.get("loan_amount", 0.0))
|
||||
current_ltv = float(portfolio.get("ltv_ratio", portfolio.get("current_ltv", 0.0)))
|
||||
net_equity = float(portfolio.get("net_equity", gold_value - loan_amount))
|
||||
|
||||
self.gold_value.set_text(self._money(gold_value))
|
||||
self.loan_amount.set_text(self._money(loan_amount))
|
||||
self.net_equity.set_text(self._money(net_equity))
|
||||
self.ltv.set_text(f"{current_ltv * 100:.1f}%")
|
||||
self.ltv.style(f"color: {self._ltv_color(current_ltv, threshold)}")
|
||||
|
||||
badge_text, badge_style = self._warning_state(current_ltv, threshold)
|
||||
self.warning_badge.set_text(badge_text)
|
||||
self.warning_badge.style(badge_style)
|
||||
|
||||
@staticmethod
|
||||
def _money(value: float) -> str:
|
||||
return f"${value:,.2f}"
|
||||
|
||||
@staticmethod
|
||||
def _ltv_color(ltv: float, threshold: float) -> str:
|
||||
if ltv >= threshold:
|
||||
return "#f43f5e"
|
||||
if ltv >= threshold * 0.9:
|
||||
return "#f59e0b"
|
||||
return "#22c55e"
|
||||
|
||||
@staticmethod
|
||||
def _warning_state(ltv: float, threshold: float) -> tuple[str, str]:
|
||||
base = "border-radius: 9999px; padding: 0.25rem 0.75rem; font-size: 0.75rem; font-weight: 600;"
|
||||
if ltv >= threshold:
|
||||
return ("Margin call risk", base + " background: rgba(244, 63, 94, 0.14); color: #f43f5e;")
|
||||
if ltv >= threshold * 0.9:
|
||||
return ("Approaching threshold", base + " background: rgba(245, 158, 11, 0.14); color: #f59e0b;")
|
||||
return ("Healthy collateral", base + " background: rgba(34, 197, 94, 0.14); color: #22c55e;")
|
||||
Reference in New Issue
Block a user