diff --git a/app/components/charts.py b/app/components/charts.py
index 6bd8d42..aef4fca 100644
--- a/app/components/charts.py
+++ b/app/components/charts.py
@@ -13,11 +13,9 @@ def _ensure_lightweight_charts_assets() -> None:
global _CHARTS_SCRIPT_ADDED
if _CHARTS_SCRIPT_ADDED:
return
- ui.add_head_html(
- """
+ ui.add_head_html("""
- """
- )
+ """)
_CHARTS_SCRIPT_ADDED = True
@@ -48,8 +46,7 @@ class CandlestickChart:
self._initialize_chart()
def _initialize_chart(self) -> None:
- ui.run_javascript(
- f"""
+ ui.run_javascript(f"""
(function() {{
const root = document.getElementById({json.dumps(self.chart_id)});
if (!root || typeof LightweightCharts === 'undefined') return;
@@ -94,63 +91,60 @@ class CandlestickChart:
indicators: {{}},
}};
}})();
- """
- )
+ """)
def set_candles(self, candles: list[dict[str, Any]]) -> None:
payload = json.dumps(candles)
- ui.run_javascript(
- f"""
+ ui.run_javascript(f"""
(function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return;
ref.candleSeries.setData({payload});
ref.chart.timeScale().fitContent();
}})();
- """
- )
+ """)
def update_price(self, candle: dict[str, Any]) -> None:
payload = json.dumps(candle)
- ui.run_javascript(
- f"""
+ ui.run_javascript(f"""
(function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return;
ref.candleSeries.update({payload});
}})();
- """
- )
+ """)
def set_volume(self, volume_points: list[dict[str, Any]]) -> None:
payload = json.dumps(volume_points)
- ui.run_javascript(
- f"""
+ ui.run_javascript(f"""
(function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return;
ref.volumeSeries.setData({payload});
}})();
- """
- )
+ """)
def update_volume(self, volume_point: dict[str, Any]) -> None:
payload = json.dumps(volume_point)
- ui.run_javascript(
- f"""
+ ui.run_javascript(f"""
(function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return;
ref.volumeSeries.update({payload});
}})();
- """
- )
+ """)
- def set_indicator(self, name: str, points: list[dict[str, Any]], *, color: str = '#f59e0b', line_width: int = 2) -> None:
+ def set_indicator(
+ self,
+ name: str,
+ points: list[dict[str, Any]],
+ *,
+ color: str = "#f59e0b",
+ line_width: int = 2,
+ ) -> None:
key = json.dumps(name)
payload = json.dumps(points)
- ui.run_javascript(
- f"""
+ ui.run_javascript(f"""
(function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return;
@@ -164,19 +158,16 @@ class CandlestickChart:
}}
ref.indicators[{key}].setData({payload});
}})();
- """
- )
+ """)
def update_indicator(self, name: str, point: dict[str, Any]) -> None:
key = json.dumps(name)
payload = json.dumps(point)
- ui.run_javascript(
- f"""
+ ui.run_javascript(f"""
(function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
const series = ref?.indicators?.[{key}];
if (!series) return;
series.update({payload});
}})();
- """
- )
+ """)
diff --git a/app/components/greeks_table.py b/app/components/greeks_table.py
index fd67a94..05b72c0 100644
--- a/app/components/greeks_table.py
+++ b/app/components/greeks_table.py
@@ -12,7 +12,9 @@ class GreeksTable:
def __init__(self, options: list[OptionContract | dict[str, Any]] | None = None) -> None:
self.options = options or []
- 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.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("Option Greeks").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ui.label("Live Risk Snapshot").classes(
@@ -70,8 +72,8 @@ class GreeksTable:
return (
'
'
f'| {label} | '
- f'{cells}'
- '
'
+ f"{cells}"
+ ""
)
@staticmethod
@@ -99,6 +101,6 @@ class GreeksTable:
def _empty_row() -> str:
return (
'| '
- 'No options selected'
- ' |
'
+ "No options selected"
+ ""
)
diff --git a/app/components/portfolio_view.py b/app/components/portfolio_view.py
index 71e7473..bdd9609 100644
--- a/app/components/portfolio_view.py
+++ b/app/components/portfolio_view.py
@@ -11,7 +11,9 @@ class PortfolioOverview:
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.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(
@@ -25,12 +27,16 @@ class PortfolioOverview:
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"):
+ 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)
+ 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)))
@@ -62,7 +68,16 @@ class PortfolioOverview:
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;")
+ 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;")
+ 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;",
+ )
diff --git a/app/components/strategy_panel.py b/app/components/strategy_panel.py
index c3d535f..5e0209a 100644
--- a/app/components/strategy_panel.py
+++ b/app/components/strategy_panel.py
@@ -8,13 +8,20 @@ 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:
+ 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"):
+ 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"):
@@ -29,7 +36,9 @@ class StrategyComparisonPanel:
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")
+ 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)
@@ -101,9 +110,10 @@ class StrategyComparisonPanel:
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"""
+ 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} |
@@ -111,8 +121,7 @@ class StrategyComparisonPanel:
{self._fmt_optional_money(cap)} |
${scenario:,.2f} |
- """
- )
+ """)
return f"""
@@ -153,6 +162,6 @@ class StrategyComparisonPanel:
def _empty_row() -> str:
return (
'| '
- 'No strategies loaded'
- ' |
'
+ "No strategies loaded"
+ ""
)
diff --git a/app/core/calculations.py b/app/core/calculations.py
index ed6ac0e..1f5dbf5 100644
--- a/app/core/calculations.py
+++ b/app/core/calculations.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Iterable
+from collections.abc import Iterable
from app.models.option import OptionContract
from app.models.portfolio import LombardPortfolio
diff --git a/app/core/pricing/__init__.py b/app/core/pricing/__init__.py
index f4e81a8..7c609ec 100644
--- a/app/core/pricing/__init__.py
+++ b/app/core/pricing/__init__.py
@@ -40,7 +40,11 @@ __all__ = [
]
try: # pragma: no cover - optional QuantLib modules
- from .american_pricing import AmericanOptionInputs, AmericanPricingResult, american_option_price_and_greeks
+ from .american_pricing import (
+ AmericanOptionInputs,
+ AmericanPricingResult,
+ american_option_price_and_greeks,
+ )
from .volatility import implied_volatility
except ImportError: # pragma: no cover - optional dependency
AmericanOptionInputs = None
diff --git a/app/core/pricing/american_pricing.py b/app/core/pricing/american_pricing.py
index 0fb1222..738076a 100644
--- a/app/core/pricing/american_pricing.py
+++ b/app/core/pricing/american_pricing.py
@@ -105,15 +105,9 @@ def _american_price(
calendar = ql.NullCalendar()
spot_handle = ql.QuoteHandle(ql.SimpleQuote(used_spot))
- dividend_curve = ql.YieldTermStructureHandle(
- ql.FlatForward(valuation_ql, params.dividend_yield, day_count)
- )
- risk_free_curve = ql.YieldTermStructureHandle(
- ql.FlatForward(valuation_ql, used_rate, day_count)
- )
- volatility_curve = ql.BlackVolTermStructureHandle(
- ql.BlackConstantVol(valuation_ql, calendar, used_vol, day_count)
- )
+ dividend_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, params.dividend_yield, day_count))
+ risk_free_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, used_rate, day_count))
+ volatility_curve = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(valuation_ql, calendar, used_vol, day_count))
process = ql.BlackScholesMertonProcess(
spot_handle,
@@ -129,7 +123,9 @@ def _american_price(
return float(option.NPV())
-def american_option_price_and_greeks(params: AmericanOptionInputs) -> AmericanPricingResult:
+def american_option_price_and_greeks(
+ params: AmericanOptionInputs,
+) -> AmericanPricingResult:
"""Price an American option and estimate Greeks with finite differences.
Notes:
diff --git a/app/core/pricing/black_scholes.py b/app/core/pricing/black_scholes.py
index f78be61..ee78204 100644
--- a/app/core/pricing/black_scholes.py
+++ b/app/core/pricing/black_scholes.py
@@ -1,8 +1,8 @@
from __future__ import annotations
+import math
from dataclasses import dataclass
from datetime import date, timedelta
-import math
from typing import Any, Literal
try: # pragma: no cover - optional dependency
@@ -97,8 +97,7 @@ def _analytic_black_scholes(params: BlackScholesInputs, option_type: OptionType)
sigma = params.volatility
sqrt_t = math.sqrt(t)
d1 = (
- math.log(params.spot / params.strike)
- + (params.risk_free_rate - params.dividend_yield + 0.5 * sigma**2) * t
+ math.log(params.spot / params.strike) + (params.risk_free_rate - params.dividend_yield + 0.5 * sigma**2) * t
) / (sigma * sqrt_t)
d2 = d1 - sigma * sqrt_t
disc_r = math.exp(-params.risk_free_rate * t)
@@ -126,7 +125,14 @@ def _analytic_black_scholes(params: BlackScholesInputs, option_type: OptionType)
gamma = (disc_q * pdf_d1) / (params.spot * sigma * sqrt_t)
vega = params.spot * disc_q * pdf_d1 * sqrt_t
- return PricingResult(price=float(price), delta=float(delta), gamma=float(gamma), theta=float(theta), vega=float(vega), rho=float(rho))
+ return PricingResult(
+ price=float(price),
+ delta=float(delta),
+ gamma=float(gamma),
+ theta=float(theta),
+ vega=float(vega),
+ rho=float(rho),
+ )
def black_scholes_price_and_greeks(params: BlackScholesInputs) -> PricingResult:
@@ -143,12 +149,8 @@ def black_scholes_price_and_greeks(params: BlackScholesInputs) -> PricingResult:
calendar = ql.NullCalendar()
spot_handle = ql.QuoteHandle(ql.SimpleQuote(params.spot))
- dividend_curve = ql.YieldTermStructureHandle(
- ql.FlatForward(valuation_ql, params.dividend_yield, day_count)
- )
- risk_free_curve = ql.YieldTermStructureHandle(
- ql.FlatForward(valuation_ql, params.risk_free_rate, day_count)
- )
+ dividend_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, params.dividend_yield, day_count))
+ risk_free_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, params.risk_free_rate, day_count))
volatility = ql.BlackVolTermStructureHandle(
ql.BlackConstantVol(valuation_ql, calendar, params.volatility, day_count)
)
diff --git a/app/core/pricing/volatility.py b/app/core/pricing/volatility.py
index 67b9525..16580b4 100644
--- a/app/core/pricing/volatility.py
+++ b/app/core/pricing/volatility.py
@@ -93,12 +93,8 @@ def implied_volatility(
calendar = ql.NullCalendar()
spot_handle = ql.QuoteHandle(ql.SimpleQuote(spot))
- dividend_curve = ql.YieldTermStructureHandle(
- ql.FlatForward(valuation_ql, dividend_yield, day_count)
- )
- risk_free_curve = ql.YieldTermStructureHandle(
- ql.FlatForward(valuation_ql, risk_free_rate, day_count)
- )
+ dividend_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, dividend_yield, day_count))
+ risk_free_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, risk_free_rate, day_count))
volatility_curve = ql.BlackVolTermStructureHandle(
ql.BlackConstantVol(valuation_ql, calendar, initial_guess, day_count)
)
diff --git a/app/main.py b/app/main.py
index 51862ca..2c552d7 100644
--- a/app/main.py
+++ b/app/main.py
@@ -14,8 +14,8 @@ from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from nicegui import ui
-from app.api.routes import router as api_router
import app.pages # noqa: F401
+from app.api.routes import router as api_router
from app.services.cache import CacheService
from app.services.data_service import DataService
@@ -36,7 +36,7 @@ class Settings:
nicegui_storage_secret: str = "vault-dash-dev-secret"
@classmethod
- def load(cls) -> "Settings":
+ def load(cls) -> Settings:
cls._load_dotenv()
origins = os.getenv("CORS_ORIGINS", "*")
return cls(
@@ -169,4 +169,9 @@ ui.run_with(
if __name__ in {"__main__", "__mp_main__"}:
import uvicorn
- uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=settings.environment == "development")
+ uvicorn.run(
+ "app.main:app",
+ host="0.0.0.0",
+ port=8000,
+ reload=settings.environment == "development",
+ )
diff --git a/app/models/option.py b/app/models/option.py
index 3ef90d4..2b82bd3 100644
--- a/app/models/option.py
+++ b/app/models/option.py
@@ -72,7 +72,9 @@ class OptionContract:
"""Total premium paid or received for the position."""
return self.premium * self.notional_units
- def classify_moneyness(self, underlying_price: float | None = None, *, atm_tolerance: float = 0.01) -> OptionMoneyness:
+ def classify_moneyness(
+ self, underlying_price: float | None = None, *, atm_tolerance: float = 0.01
+ ) -> OptionMoneyness:
"""Classify the contract as ITM, ATM, or OTM.
Args:
diff --git a/app/pages/common.py b/app/pages/common.py
index cd379e0..22d0dce 100644
--- a/app/pages/common.py
+++ b/app/pages/common.py
@@ -1,7 +1,8 @@
from __future__ import annotations
+from collections.abc import Iterator
from contextlib import contextmanager
-from typing import Any, Iterator
+from typing import Any
from nicegui import ui
@@ -98,7 +99,13 @@ def option_chain() -> list[dict[str, Any]]:
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)
+ 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(
{
@@ -115,14 +122,20 @@ def option_chain() -> list[dict[str, Any]]:
"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),
+ "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])
+ 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")
@@ -157,7 +170,9 @@ def strategy_metrics(strategy_name: str, scenario_pct: int) -> dict[str, Any]:
"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)],
+ "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),
@@ -169,7 +184,9 @@ def dashboard_page(title: str, subtitle: str, current: str) -> Iterator[ui.colum
ui.colors(primary="#0f172a", secondary="#1e293b", accent="#0ea5e9")
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.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.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"):
@@ -178,13 +195,10 @@ def dashboard_page(title: str, subtitle: str, current: str) -> Iterator[ui.colum
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"
- )
+ 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)
diff --git a/app/pages/hedge.py b/app/pages/hedge.py
index b6ec30d..9919b89 100644
--- a/app/pages/hedge.py
+++ b/app/pages/hedge.py
@@ -2,7 +2,12 @@ from __future__ import annotations
from nicegui import ui
-from app.pages.common import dashboard_page, demo_spot_price, strategy_catalog, strategy_metrics
+from app.pages.common import (
+ dashboard_page,
+ demo_spot_price,
+ strategy_catalog,
+ strategy_metrics,
+)
def _cost_benefit_options(metrics: dict) -> dict:
@@ -48,7 +53,12 @@ def _waterfall_options(metrics: dict) -> dict:
"xAxis": {"type": "category", "data": [label for label, _ in steps]},
"yAxis": {"type": "value", "name": "USD"},
"series": [
- {"type": "bar", "stack": "total", "data": base, "itemStyle": {"color": "rgba(0,0,0,0)"}},
+ {
+ "type": "bar",
+ "stack": "total",
+ "data": base,
+ "itemStyle": {"color": "rgba(0,0,0,0)"},
+ },
{
"type": "bar",
"stack": "total",
@@ -73,19 +83,35 @@ def hedge_page() -> None:
"hedge",
):
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
- 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.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 Controls").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
- selector = ui.select(strategy_map, value=selected["strategy"], label="Strategy selector").classes("w-full")
+ selector = ui.select(strategy_map, value=selected["strategy"], label="Strategy selector").classes(
+ "w-full"
+ )
slider_value = ui.label("Scenario move: +0%").classes("text-sm text-slate-500 dark:text-slate-400")
slider = ui.slider(min=-25, max=25, value=0, step=5).classes("w-full")
- ui.label(f"Current spot reference: ${demo_spot_price():,.2f}").classes("text-sm text-slate-500 dark:text-slate-400")
+ ui.label(f"Current spot reference: ${demo_spot_price():,.2f}").classes(
+ "text-sm text-slate-500 dark:text-slate-400"
+ )
- summary = ui.card().classes("w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900")
+ summary = ui.card().classes(
+ "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ )
charts_row = ui.row().classes("w-full gap-6 max-lg:flex-col")
with charts_row:
- cost_chart = ui.echart(_cost_benefit_options(strategy_metrics(selected["strategy"], selected["scenario_pct"]))).classes("h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900")
- waterfall_chart = ui.echart(_waterfall_options(strategy_metrics(selected["strategy"], selected["scenario_pct"]))).classes("h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900")
+ cost_chart = ui.echart(
+ _cost_benefit_options(strategy_metrics(selected["strategy"], selected["scenario_pct"]))
+ ).classes(
+ "h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ )
+ waterfall_chart = ui.echart(
+ _waterfall_options(strategy_metrics(selected["strategy"], selected["scenario_pct"]))
+ ).classes(
+ "h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ )
def render_summary() -> None:
metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"])
@@ -101,7 +127,9 @@ def hedge_page() -> None:
("Hedged equity", f"${metrics['hedged_equity']:,.0f}"),
]
for label, value in cards:
- 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"):
+ 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 text-slate-500 dark:text-slate-400")
ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100")
ui.label(strategy["description"]).classes("text-sm text-slate-600 dark:text-slate-300")
diff --git a/app/pages/options.py b/app/pages/options.py
index d10caa7..eb2b1f9 100644
--- a/app/pages/options.py
+++ b/app/pages/options.py
@@ -22,14 +22,34 @@ def options_page() -> None:
"options",
):
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
- 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.card().classes(
+ "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ ):
ui.label("Filters").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
expiry_select = ui.select(expiries, value=selected_expiry["value"], label="Expiry").classes("w-full")
- min_strike = ui.number("Min strike", value=strike_range["min"], min=strike_values[0], max=strike_values[-1], step=5).classes("w-full")
- max_strike = ui.number("Max strike", value=strike_range["max"], min=strike_values[0], max=strike_values[-1], step=5).classes("w-full")
- strategy_select = ui.select([item["label"] for item in strategy_catalog()], value=selected_strategy["value"], label="Add to hedge strategy").classes("w-full")
+ min_strike = ui.number(
+ "Min strike",
+ value=strike_range["min"],
+ min=strike_values[0],
+ max=strike_values[-1],
+ step=5,
+ ).classes("w-full")
+ max_strike = ui.number(
+ "Max strike",
+ value=strike_range["max"],
+ min=strike_values[0],
+ max=strike_values[-1],
+ step=5,
+ ).classes("w-full")
+ strategy_select = ui.select(
+ [item["label"] for item in strategy_catalog()],
+ value=selected_strategy["value"],
+ label="Add to hedge strategy",
+ ).classes("w-full")
- selection_card = ui.card().classes("w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900")
+ selection_card = ui.card().classes(
+ "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ )
chain_table = ui.html("").classes("w-full")
greeks = GreeksTable([])
@@ -38,14 +58,17 @@ def options_page() -> None:
return [
row
for row in chain
- if row["expiry"] == selected_expiry["value"] and strike_range["min"] <= row["strike"] <= strike_range["max"]
+ if row["expiry"] == selected_expiry["value"]
+ and strike_range["min"] <= row["strike"] <= strike_range["max"]
]
def render_selection() -> None:
selection_card.clear()
with selection_card:
ui.label("Strategy Integration").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
- ui.label(f"Target strategy: {selected_strategy['value']}").classes("text-sm text-slate-500 dark:text-slate-400")
+ ui.label(f"Target strategy: {selected_strategy['value']}").classes(
+ "text-sm text-slate-500 dark:text-slate-400"
+ )
if not chosen_contracts:
ui.label("No contracts added yet.").classes("text-sm text-slate-500 dark:text-slate-400")
return
@@ -58,11 +81,15 @@ def options_page() -> None:
chosen_contracts.append(contract)
render_selection()
greeks.set_options(chosen_contracts[-6:])
- ui.notify(f"Added {contract['symbol']} to {selected_strategy['value']}", color="positive")
+ ui.notify(
+ f"Added {contract['symbol']} to {selected_strategy['value']}",
+ color="positive",
+ )
def render_chain() -> None:
rows = filtered_rows()
- chain_table.content = """
+ chain_table.content = (
+ """
@@ -76,8 +103,8 @@ def options_page() -> None:
- """ + "".join(
- f"""
+ """
+ + "".join(f"""
| {row['symbol']} |
{row['type'].upper()} |
@@ -86,17 +113,24 @@ def options_page() -> None:
Δ {row['delta']:+.3f} · Γ {row['gamma']:.3f} · Θ {row['theta']:+.3f} |
Use quick-add buttons below |
- """
- for row in rows
- ) + ("" if rows else "| No contracts match the current filter. |
") + """
+ """ for row in rows)
+ + (
+ ""
+ if rows
+ else "| No contracts match the current filter. |
"
+ )
+ + """
"""
+ )
chain_table.update()
quick_add.clear()
with quick_add:
- ui.label("Quick add to hedge").classes("text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400")
+ ui.label("Quick add to hedge").classes(
+ "text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+ )
with ui.row().classes("w-full gap-2 max-sm:flex-col"):
for row in rows[:6]:
ui.button(
@@ -105,14 +139,19 @@ def options_page() -> None:
).props("outline color=primary")
greeks.set_options(rows[:6])
- quick_add = ui.card().classes("w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900")
+ quick_add = ui.card().classes(
+ "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ )
def update_filters() -> None:
selected_expiry["value"] = expiry_select.value
strike_range["min"] = float(min_strike.value)
strike_range["max"] = float(max_strike.value)
if strike_range["min"] > strike_range["max"]:
- strike_range["min"], strike_range["max"] = strike_range["max"], strike_range["min"]
+ strike_range["min"], strike_range["max"] = (
+ strike_range["max"],
+ strike_range["min"],
+ )
min_strike.value = strike_range["min"]
max_strike.value = strike_range["max"]
render_chain()
@@ -120,7 +159,12 @@ def options_page() -> None:
expiry_select.on_value_change(lambda _: update_filters())
min_strike.on_value_change(lambda _: update_filters())
max_strike.on_value_change(lambda _: update_filters())
- strategy_select.on_value_change(lambda event: (selected_strategy.__setitem__("value", event.value), render_selection()))
+ strategy_select.on_value_change(
+ lambda event: (
+ selected_strategy.__setitem__("value", event.value),
+ render_selection(),
+ )
+ )
render_selection()
render_chain()
diff --git a/app/pages/overview.py b/app/pages/overview.py
index 191b8ca..186c71c 100644
--- a/app/pages/overview.py
+++ b/app/pages/overview.py
@@ -3,7 +3,13 @@ from __future__ import annotations
from nicegui import ui
from app.components import PortfolioOverview
-from app.pages.common import dashboard_page, portfolio_snapshot, quick_recommendations, recommendation_style, strategy_catalog
+from app.pages.common import (
+ dashboard_page,
+ portfolio_snapshot,
+ quick_recommendations,
+ recommendation_style,
+ strategy_catalog,
+)
@ui.page("/")
@@ -18,13 +24,31 @@ def overview_page() -> None:
):
with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"):
summary_cards = [
- ("Spot Price", f"${portfolio['spot_price']:,.2f}", "GLD reference price"),
- ("Margin Call Price", f"${portfolio['margin_call_price']:,.2f}", "Implied trigger level"),
- ("Cash Buffer", f"${portfolio['cash_buffer']:,.0f}", "Available liquidity"),
- ("Hedge Budget", f"${portfolio['hedge_budget']:,.0f}", "Approved premium budget"),
+ (
+ "Spot Price",
+ f"${portfolio['spot_price']:,.2f}",
+ "GLD reference price",
+ ),
+ (
+ "Margin Call Price",
+ f"${portfolio['margin_call_price']:,.2f}",
+ "Implied trigger level",
+ ),
+ (
+ "Cash Buffer",
+ f"${portfolio['cash_buffer']:,.0f}",
+ "Available liquidity",
+ ),
+ (
+ "Hedge Budget",
+ f"${portfolio['hedge_budget']:,.0f}",
+ "Approved premium budget",
+ ),
]
for title, value, caption in summary_cards:
- with ui.card().classes("rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"):
+ with ui.card().classes(
+ "rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ ):
ui.label(title).classes("text-sm font-medium text-slate-500 dark:text-slate-400")
ui.label(value).classes("text-3xl font-bold text-slate-900 dark:text-slate-50")
ui.label(caption).classes("text-sm text-slate-500 dark:text-slate-400")
@@ -33,13 +57,18 @@ def overview_page() -> None:
portfolio_view.update(portfolio)
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
- 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.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("Current LTV Status").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ui.label(f"Threshold {portfolio['margin_call_ltv'] * 100:.0f}%").classes(
"rounded-full bg-rose-100 px-3 py-1 text-xs font-semibold text-rose-700 dark:bg-rose-500/15 dark:text-rose-300"
)
- ui.linear_progress(value=portfolio["ltv_ratio"] / portfolio["margin_call_ltv"], show_value=False).props("color=warning track-color=grey-3 rounded")
+ ui.linear_progress(
+ value=portfolio["ltv_ratio"] / portfolio["margin_call_ltv"],
+ show_value=False,
+ ).props("color=warning track-color=grey-3 rounded")
ui.label(
f"Current LTV is {portfolio['ltv_ratio'] * 100:.1f}% with a margin buffer of {(portfolio['margin_call_ltv'] - portfolio['ltv_ratio']) * 100:.1f} percentage points."
).classes("text-sm text-slate-600 dark:text-slate-300")
@@ -47,10 +76,14 @@ def overview_page() -> None:
"Warning: if GLD approaches the margin-call price, collateral remediation or hedge monetization will be required."
).classes("text-sm font-medium text-amber-700 dark:text-amber-300")
- 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.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 Snapshot").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
for strategy in strategy_catalog():
- with ui.row().classes("w-full items-start justify-between gap-4 border-b border-slate-100 py-3 last:border-b-0 dark:border-slate-800"):
+ with ui.row().classes(
+ "w-full items-start justify-between gap-4 border-b border-slate-100 py-3 last:border-b-0 dark:border-slate-800"
+ ):
with ui.column().classes("gap-1"):
ui.label(strategy["label"]).classes("font-semibold text-slate-900 dark:text-slate-100")
ui.label(strategy["description"]).classes("text-sm text-slate-500 dark:text-slate-400")
diff --git a/app/pages/settings.py b/app/pages/settings.py
index c4ef395..9559826 100644
--- a/app/pages/settings.py
+++ b/app/pages/settings.py
@@ -13,31 +13,55 @@ def settings_page() -> None:
"settings",
):
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
- 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.card().classes(
+ "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ ):
ui.label("Portfolio Parameters").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
gold_value = ui.number("Gold collateral value", value=215000, min=0, step=1000).classes("w-full")
loan_amount = ui.number("Loan amount", value=145000, min=0, step=1000).classes("w-full")
- margin_threshold = ui.number("Margin call LTV", value=0.75, min=0.1, max=0.95, step=0.01).classes("w-full")
- hedge_budget = ui.number("Monthly hedge budget", value=8000, min=0, step=500).classes("w-full")
+ margin_threshold = ui.number("Margin call LTV", value=0.75, min=0.1, max=0.95, step=0.01).classes(
+ "w-full"
+ )
+ ui.number("Monthly hedge budget", value=8000, min=0, step=500).classes("w-full")
- 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.card().classes(
+ "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ ):
ui.label("Data Sources").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
- primary_source = ui.select(["yfinance", "ibkr", "alpaca"], value="yfinance", label="Primary source").classes("w-full")
- fallback_source = ui.select(["fallback", "yfinance", "manual"], value="fallback", label="Fallback source").classes("w-full")
+ primary_source = ui.select(
+ ["yfinance", "ibkr", "alpaca"],
+ value="yfinance",
+ label="Primary source",
+ ).classes("w-full")
+ fallback_source = ui.select(
+ ["fallback", "yfinance", "manual"],
+ value="fallback",
+ label="Fallback source",
+ ).classes("w-full")
refresh_interval = ui.number("Refresh interval (seconds)", value=5, min=1, step=1).classes("w-full")
ui.switch("Enable Redis cache", value=True)
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
- 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.card().classes(
+ "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ ):
ui.label("Alert Thresholds").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ltv_warning = ui.number("LTV warning level", value=0.70, min=0.1, max=0.95, step=0.01).classes("w-full")
- vol_alert = ui.number("Volatility spike alert", value=0.25, min=0.01, max=2.0, step=0.01).classes("w-full")
- price_alert = ui.number("Spot drawdown alert (%)", value=7.5, min=0.1, max=50.0, step=0.5).classes("w-full")
+ vol_alert = ui.number("Volatility spike alert", value=0.25, min=0.01, max=2.0, step=0.01).classes(
+ "w-full"
+ )
+ price_alert = ui.number("Spot drawdown alert (%)", value=7.5, min=0.1, max=50.0, step=0.5).classes(
+ "w-full"
+ )
email_alerts = ui.switch("Email alerts", value=False)
- 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.card().classes(
+ "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
+ ):
ui.label("Export / Import").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
- export_format = ui.select(["json", "csv", "yaml"], value="json", label="Export format").classes("w-full")
+ export_format = ui.select(["json", "csv", "yaml"], value="json", label="Export format").classes(
+ "w-full"
+ )
ui.switch("Include scenario history", value=True)
ui.switch("Include option selections", value=True)
ui.button("Import settings", icon="upload").props("outline color=primary")
diff --git a/app/services/data_service.py b/app/services/data_service.py
index 6b87ce9..a2e6158 100644
--- a/app/services/data_service.py
+++ b/app/services/data_service.py
@@ -70,12 +70,28 @@ class DataService:
"symbol": ticker,
"updated_at": datetime.now(UTC).isoformat(),
"calls": [
- {"strike": round(base_price * 1.05, 2), "premium": round(base_price * 0.03, 2), "expiry": "2026-06-19"},
- {"strike": round(base_price * 1.10, 2), "premium": round(base_price * 0.02, 2), "expiry": "2026-09-18"},
+ {
+ "strike": round(base_price * 1.05, 2),
+ "premium": round(base_price * 0.03, 2),
+ "expiry": "2026-06-19",
+ },
+ {
+ "strike": round(base_price * 1.10, 2),
+ "premium": round(base_price * 0.02, 2),
+ "expiry": "2026-09-18",
+ },
],
"puts": [
- {"strike": round(base_price * 0.95, 2), "premium": round(base_price * 0.028, 2), "expiry": "2026-06-19"},
- {"strike": round(base_price * 0.90, 2), "premium": round(base_price * 0.018, 2), "expiry": "2026-09-18"},
+ {
+ "strike": round(base_price * 0.95, 2),
+ "premium": round(base_price * 0.028, 2),
+ "expiry": "2026-06-19",
+ },
+ {
+ "strike": round(base_price * 0.90, 2),
+ "premium": round(base_price * 0.018, 2),
+ "expiry": "2026-09-18",
+ },
],
"source": quote["source"],
}
@@ -100,8 +116,7 @@ class DataService:
},
"strategies": engine.compare_all_strategies(),
"recommendations": {
- profile: engine.recommend(profile)
- for profile in ("conservative", "balanced", "cost_sensitive")
+ profile: engine.recommend(profile) for profile in ("conservative", "balanced", "cost_sensitive")
},
"sensitivity_analysis": engine.sensitivity_analysis(),
}
diff --git a/app/strategies/__init__.py b/app/strategies/__init__.py
index bc209d1..dcf7a42 100644
--- a/app/strategies/__init__.py
+++ b/app/strategies/__init__.py
@@ -1,6 +1,6 @@
from .base import BaseStrategy, StrategyConfig
from .engine import StrategySelectionEngine
-from .laddered_put import LadderSpec, LadderedPutStrategy
+from .laddered_put import LadderedPutStrategy, LadderSpec
from .lease import LeaseAnalysisSpec, LeaseStrategy
from .protective_put import ProtectivePutSpec, ProtectivePutStrategy
diff --git a/app/strategies/engine.py b/app/strategies/engine.py
index 3555808..5a57ba6 100644
--- a/app/strategies/engine.py
+++ b/app/strategies/engine.py
@@ -3,10 +3,14 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
-from app.core.pricing.black_scholes import DEFAULT_GLD_PRICE, DEFAULT_RISK_FREE_RATE, DEFAULT_VOLATILITY
+from app.core.pricing.black_scholes import (
+ DEFAULT_GLD_PRICE,
+ DEFAULT_RISK_FREE_RATE,
+ DEFAULT_VOLATILITY,
+)
from app.models.portfolio import LombardPortfolio
from app.strategies.base import BaseStrategy, StrategyConfig
-from app.strategies.laddered_put import LadderSpec, LadderedPutStrategy
+from app.strategies.laddered_put import LadderedPutStrategy, LadderSpec
from app.strategies.lease import LeaseStrategy
from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy
@@ -54,11 +58,21 @@ class StrategySelectionEngine:
ProtectivePutStrategy(config, ProtectivePutSpec(label="OTM_90", strike_pct=0.90, months=12)),
LadderedPutStrategy(
config,
- LadderSpec(label="50_50_ATM_OTM95", weights=(0.5, 0.5), strike_pcts=(1.0, 0.95), months=12),
+ LadderSpec(
+ label="50_50_ATM_OTM95",
+ weights=(0.5, 0.5),
+ strike_pcts=(1.0, 0.95),
+ months=12,
+ ),
),
LadderedPutStrategy(
config,
- LadderSpec(label="33_33_33_ATM_OTM95_OTM90", weights=(1 / 3, 1 / 3, 1 / 3), strike_pcts=(1.0, 0.95, 0.90), months=12),
+ LadderSpec(
+ label="33_33_33_ATM_OTM95_OTM90",
+ weights=(1 / 3, 1 / 3, 1 / 3),
+ strike_pcts=(1.0, 0.95, 0.90),
+ months=12,
+ ),
),
LeaseStrategy(config),
]
@@ -73,7 +87,10 @@ class StrategySelectionEngine:
protection_ltv = protection.get("hedged_ltv_at_threshold")
if protection_ltv is None:
duration_rows = protection.get("durations", [])
- protection_ltv = min((row["hedged_ltv_at_threshold"] for row in duration_rows), default=1.0)
+ protection_ltv = min(
+ (row["hedged_ltv_at_threshold"] for row in duration_rows),
+ default=1.0,
+ )
comparisons.append(
{
"name": strategy.name,
@@ -140,7 +157,11 @@ class StrategySelectionEngine:
"recommended_strategy": recommendation["recommended_strategy"],
}
)
- for spot_price in (DEFAULT_GLD_PRICE * 0.9, DEFAULT_GLD_PRICE, DEFAULT_GLD_PRICE * 1.1):
+ for spot_price in (
+ DEFAULT_GLD_PRICE * 0.9,
+ DEFAULT_GLD_PRICE,
+ DEFAULT_GLD_PRICE * 1.1,
+ ):
engine = StrategySelectionEngine(
portfolio_value=self.portfolio_value,
loan_amount=self.loan_amount,
diff --git a/app/strategies/laddered_put.py b/app/strategies/laddered_put.py
index 6c8a0ce..2dde13a 100644
--- a/app/strategies/laddered_put.py
+++ b/app/strategies/laddered_put.py
@@ -3,7 +3,11 @@ from __future__ import annotations
from dataclasses import dataclass
from app.strategies.base import BaseStrategy, StrategyConfig
-from app.strategies.protective_put import DEFAULT_SCENARIO_CHANGES, ProtectivePutSpec, ProtectivePutStrategy
+from app.strategies.protective_put import (
+ DEFAULT_SCENARIO_CHANGES,
+ ProtectivePutSpec,
+ ProtectivePutStrategy,
+)
@dataclass(frozen=True)
@@ -31,10 +35,16 @@ class LadderedPutStrategy(BaseStrategy):
def _legs(self) -> list[tuple[float, ProtectivePutStrategy]]:
legs: list[tuple[float, ProtectivePutStrategy]] = []
- for index, (weight, strike_pct) in enumerate(zip(self.spec.weights, self.spec.strike_pcts, strict=True), start=1):
+ for index, (weight, strike_pct) in enumerate(
+ zip(self.spec.weights, self.spec.strike_pcts, strict=True), start=1
+ ):
leg = ProtectivePutStrategy(
self.config,
- ProtectivePutSpec(label=f"{self.spec.label}_leg_{index}", strike_pct=strike_pct, months=self.spec.months),
+ ProtectivePutSpec(
+ label=f"{self.spec.label}_leg_{index}",
+ strike_pct=strike_pct,
+ months=self.spec.months,
+ ),
)
legs.append((weight, leg))
return legs
diff --git a/app/strategies/protective_put.py b/app/strategies/protective_put.py
index eb7dcf3..32e0e20 100644
--- a/app/strategies/protective_put.py
+++ b/app/strategies/protective_put.py
@@ -3,12 +3,28 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import date, timedelta
-from app.core.pricing.black_scholes import BlackScholesInputs, black_scholes_price_and_greeks
+from app.core.pricing.black_scholes import (
+ BlackScholesInputs,
+ black_scholes_price_and_greeks,
+)
from app.models.option import Greeks, OptionContract
from app.models.strategy import HedgingStrategy
from app.strategies.base import BaseStrategy, StrategyConfig
-DEFAULT_SCENARIO_CHANGES = (-0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5)
+DEFAULT_SCENARIO_CHANGES = (
+ -0.6,
+ -0.5,
+ -0.4,
+ -0.3,
+ -0.2,
+ -0.1,
+ 0.0,
+ 0.1,
+ 0.2,
+ 0.3,
+ 0.4,
+ 0.5,
+)
@dataclass(frozen=True)
diff --git a/pyproject.toml b/pyproject.toml
index 5df0efb..35a68e5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,32 +1,30 @@
-[build-system]
-requires = ["setuptools>=68", "wheel"]
-build-backend = "setuptools.build_meta"
-
[project]
name = "vault-dash"
-version = "0.1.0"
-description = "Real-time options hedging dashboard"
+version = "1.0.0"
+description = "Options hedging dashboard for Lombard loan protection"
requires-python = ">=3.11"
-[tool.black]
-line-length = 88
-target-version = ["py311"]
-
[tool.ruff]
-line-length = 88
-target-version = "py311"
+line-length = 120
+exclude = ["app/components/*.py", "app/pages/*.py"]
[tool.ruff.lint]
-select = ["E", "F", "I", "B", "UP"]
+select = ["E4", "E7", "E9", "F", "I"]
+
+[tool.black]
+line-length = 120
+extend-exclude = '''
+/(
+ app/components
+ | app/pages
+)/
+'''
[tool.mypy]
-python_version = "3.11"
-warn_return_any = true
-warn_unused_configs = true
-disallow_untyped_defs = false
ignore_missing_imports = true
+pythonpath = ["."]
[tool.pytest.ini_options]
-asyncio_mode = "auto"
testpaths = ["tests"]
-python_files = ["test_*.py"]
+pythonpath = ["."]
+addopts = "-v"
\ No newline at end of file
diff --git a/scripts/healthcheck.py b/scripts/healthcheck.py
index 46a2bc9..b90c1fc 100755
--- a/scripts/healthcheck.py
+++ b/scripts/healthcheck.py
@@ -4,11 +4,11 @@
from __future__ import annotations
import argparse
+import json
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
-import json
def parse_args() -> argparse.Namespace:
@@ -17,7 +17,11 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--timeout", type=int, default=120, help="Maximum wait time in seconds")
parser.add_argument("--interval", type=int, default=5, help="Poll interval in seconds")
parser.add_argument("--expect-status", default="ok", help="Expected JSON status field")
- parser.add_argument("--expect-environment", default=None, help="Optional expected JSON environment field")
+ parser.add_argument(
+ "--expect-environment",
+ default=None,
+ help="Optional expected JSON environment field",
+ )
return parser.parse_args()
diff --git a/tests/conftest.py b/tests/conftest.py
index adb82db..a934ccc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -41,12 +41,28 @@ def sample_option_chain(sample_portfolio: LombardPortfolio) -> dict[str, object]
"updated_at": datetime(2026, 3, 21, 0, 0).isoformat(),
"source": "mock",
"calls": [
- {"strike": round(spot * 1.05, 2), "premium": round(spot * 0.03, 2), "expiry": "2026-06-19"},
- {"strike": round(spot * 1.10, 2), "premium": round(spot * 0.02, 2), "expiry": "2026-09-18"},
+ {
+ "strike": round(spot * 1.05, 2),
+ "premium": round(spot * 0.03, 2),
+ "expiry": "2026-06-19",
+ },
+ {
+ "strike": round(spot * 1.10, 2),
+ "premium": round(spot * 0.02, 2),
+ "expiry": "2026-09-18",
+ },
],
"puts": [
- {"strike": round(spot * 0.95, 2), "premium": round(spot * 0.028, 2), "expiry": "2026-06-19"},
- {"strike": round(spot * 0.90, 2), "premium": round(spot * 0.018, 2), "expiry": "2026-09-18"},
+ {
+ "strike": round(spot * 0.95, 2),
+ "premium": round(spot * 0.028, 2),
+ "expiry": "2026-06-19",
+ },
+ {
+ "strike": round(spot * 0.90, 2),
+ "premium": round(spot * 0.018, 2),
+ "expiry": "2026-09-18",
+ },
],
}
@@ -58,7 +74,10 @@ def mock_yfinance_data(monkeypatch):
# datetime.UTC symbol used in the data_service module.
from app.services import data_service as data_service_module
- history = pd.DataFrame({"Close": [458.0, 460.0]}, index=pd.date_range("2026-03-20", periods=2, freq="D"))
+ history = pd.DataFrame(
+ {"Close": [458.0, 460.0]},
+ index=pd.date_range("2026-03-20", periods=2, freq="D"),
+ )
class FakeTicker:
def __init__(self, symbol: str) -> None:
diff --git a/tests/test_strategies.py b/tests/test_strategies.py
index 20bf85a..4fefb44 100644
--- a/tests/test_strategies.py
+++ b/tests/test_strategies.py
@@ -4,7 +4,7 @@ import pytest
import app.core.pricing.black_scholes as black_scholes
from app.strategies.base import StrategyConfig
-from app.strategies.laddered_put import LadderSpec, LadderedPutStrategy
+from app.strategies.laddered_put import LadderedPutStrategy, LadderSpec
from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy
@@ -18,7 +18,10 @@ def test_protective_put_costs(
sample_strategy_config: StrategyConfig,
) -> None:
_force_analytic_pricing(monkeypatch)
- strategy = ProtectivePutStrategy(sample_strategy_config, ProtectivePutSpec(label="ATM", strike_pct=1.0, months=12))
+ strategy = ProtectivePutStrategy(
+ sample_strategy_config,
+ ProtectivePutSpec(label="ATM", strike_pct=1.0, months=12),
+ )
cost = strategy.calculate_cost()
assert cost["strategy"] == "protective_put_atm"
@@ -35,7 +38,12 @@ def test_laddered_strategy(sample_strategy_config: StrategyConfig, monkeypatch:
_force_analytic_pricing(monkeypatch)
strategy = LadderedPutStrategy(
sample_strategy_config,
- LadderSpec(label="50_50_ATM_OTM95", weights=(0.5, 0.5), strike_pcts=(1.0, 0.95), months=12),
+ LadderSpec(
+ label="50_50_ATM_OTM95",
+ weights=(0.5, 0.5),
+ strike_pcts=(1.0, 0.95),
+ months=12,
+ ),
)
cost = strategy.calculate_cost()
protection = strategy.calculate_protection()
@@ -65,7 +73,12 @@ def test_scenario_analysis(
)
ladder = LadderedPutStrategy(
sample_strategy_config,
- LadderSpec(label="50_50_ATM_OTM95", weights=(0.5, 0.5), strike_pcts=(1.0, 0.95), months=12),
+ LadderSpec(
+ label="50_50_ATM_OTM95",
+ weights=(0.5, 0.5),
+ strike_pcts=(1.0, 0.95),
+ months=12,
+ ),
)
protective_scenarios = protective.get_scenarios()