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
This commit is contained in:
Bu5hm4nn
2026-03-22 10:30:12 +01:00
parent b037bf4c01
commit 874b4a5a02
25 changed files with 456 additions and 195 deletions

View File

@@ -13,11 +13,9 @@ def _ensure_lightweight_charts_assets() -> None:
global _CHARTS_SCRIPT_ADDED global _CHARTS_SCRIPT_ADDED
if _CHARTS_SCRIPT_ADDED: if _CHARTS_SCRIPT_ADDED:
return return
ui.add_head_html( ui.add_head_html("""
"""
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
""" """)
)
_CHARTS_SCRIPT_ADDED = True _CHARTS_SCRIPT_ADDED = True
@@ -48,8 +46,7 @@ class CandlestickChart:
self._initialize_chart() self._initialize_chart()
def _initialize_chart(self) -> None: def _initialize_chart(self) -> None:
ui.run_javascript( ui.run_javascript(f"""
f"""
(function() {{ (function() {{
const root = document.getElementById({json.dumps(self.chart_id)}); const root = document.getElementById({json.dumps(self.chart_id)});
if (!root || typeof LightweightCharts === 'undefined') return; if (!root || typeof LightweightCharts === 'undefined') return;
@@ -94,63 +91,60 @@ class CandlestickChart:
indicators: {{}}, indicators: {{}},
}}; }};
}})(); }})();
""" """)
)
def set_candles(self, candles: list[dict[str, Any]]) -> None: def set_candles(self, candles: list[dict[str, Any]]) -> None:
payload = json.dumps(candles) payload = json.dumps(candles)
ui.run_javascript( ui.run_javascript(f"""
f"""
(function() {{ (function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}]; const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return; if (!ref) return;
ref.candleSeries.setData({payload}); ref.candleSeries.setData({payload});
ref.chart.timeScale().fitContent(); ref.chart.timeScale().fitContent();
}})(); }})();
""" """)
)
def update_price(self, candle: dict[str, Any]) -> None: def update_price(self, candle: dict[str, Any]) -> None:
payload = json.dumps(candle) payload = json.dumps(candle)
ui.run_javascript( ui.run_javascript(f"""
f"""
(function() {{ (function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}]; const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return; if (!ref) return;
ref.candleSeries.update({payload}); ref.candleSeries.update({payload});
}})(); }})();
""" """)
)
def set_volume(self, volume_points: list[dict[str, Any]]) -> None: def set_volume(self, volume_points: list[dict[str, Any]]) -> None:
payload = json.dumps(volume_points) payload = json.dumps(volume_points)
ui.run_javascript( ui.run_javascript(f"""
f"""
(function() {{ (function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}]; const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return; if (!ref) return;
ref.volumeSeries.setData({payload}); ref.volumeSeries.setData({payload});
}})(); }})();
""" """)
)
def update_volume(self, volume_point: dict[str, Any]) -> None: def update_volume(self, volume_point: dict[str, Any]) -> None:
payload = json.dumps(volume_point) payload = json.dumps(volume_point)
ui.run_javascript( ui.run_javascript(f"""
f"""
(function() {{ (function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}]; const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return; if (!ref) return;
ref.volumeSeries.update({payload}); 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) key = json.dumps(name)
payload = json.dumps(points) payload = json.dumps(points)
ui.run_javascript( ui.run_javascript(f"""
f"""
(function() {{ (function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}]; const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
if (!ref) return; if (!ref) return;
@@ -164,19 +158,16 @@ class CandlestickChart:
}} }}
ref.indicators[{key}].setData({payload}); ref.indicators[{key}].setData({payload});
}})(); }})();
""" """)
)
def update_indicator(self, name: str, point: dict[str, Any]) -> None: def update_indicator(self, name: str, point: dict[str, Any]) -> None:
key = json.dumps(name) key = json.dumps(name)
payload = json.dumps(point) payload = json.dumps(point)
ui.run_javascript( ui.run_javascript(f"""
f"""
(function() {{ (function() {{
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}]; const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
const series = ref?.indicators?.[{key}]; const series = ref?.indicators?.[{key}];
if (!series) return; if (!series) return;
series.update({payload}); series.update({payload});
}})(); }})();
""" """)
)

View File

@@ -12,7 +12,9 @@ class GreeksTable:
def __init__(self, options: list[OptionContract | dict[str, Any]] | None = None) -> None: def __init__(self, options: list[OptionContract | dict[str, Any]] | None = None) -> None:
self.options = options or [] 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"): 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("Option Greeks").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ui.label("Live Risk Snapshot").classes( ui.label("Live Risk Snapshot").classes(
@@ -70,8 +72,8 @@ class GreeksTable:
return ( return (
'<tr class="border-b border-slate-200 dark:border-slate-800">' '<tr class="border-b border-slate-200 dark:border-slate-800">'
f'<td class="px-4 py-3 font-medium text-slate-900 dark:text-slate-100">{label}</td>' f'<td class="px-4 py-3 font-medium text-slate-900 dark:text-slate-100">{label}</td>'
f'{cells}' f"{cells}"
'</tr>' "</tr>"
) )
@staticmethod @staticmethod
@@ -99,6 +101,6 @@ class GreeksTable:
def _empty_row() -> str: def _empty_row() -> str:
return ( return (
'<tr><td colspan="6" class="px-4 py-6 text-center text-slate-500 dark:text-slate-400">' '<tr><td colspan="6" class="px-4 py-6 text-center text-slate-500 dark:text-slate-400">'
'No options selected' "No options selected"
'</td></tr>' "</td></tr>"
) )

View File

@@ -11,7 +11,9 @@ class PortfolioOverview:
def __init__(self, *, margin_call_ltv: float = 0.75) -> None: def __init__(self, *, margin_call_ltv: float = 0.75) -> None:
self.margin_call_ltv = margin_call_ltv 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"): 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") ui.label("Portfolio Overview").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
self.warning_badge = ui.label("Monitoring").classes( self.warning_badge = ui.label("Monitoring").classes(
@@ -25,12 +27,16 @@ class PortfolioOverview:
self.net_equity = self._metric_card("Net Equity") self.net_equity = self._metric_card("Net Equity")
def _metric_card(self, label: str) -> ui.label: 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") 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") 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: 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))) gold_value = float(portfolio.get("gold_value", portfolio.get("portfolio_value", 0.0)))
loan_amount = float(portfolio.get("loan_amount", 0.0)) loan_amount = float(portfolio.get("loan_amount", 0.0))
current_ltv = float(portfolio.get("ltv_ratio", portfolio.get("current_ltv", 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]: 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;" base = "border-radius: 9999px; padding: 0.25rem 0.75rem; font-size: 0.75rem; font-weight: 600;"
if ltv >= threshold: 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: if ltv >= threshold * 0.9:
return ("Approaching threshold", base + " background: rgba(245, 158, 11, 0.14); color: #f59e0b;") return (
return ("Healthy collateral", base + " background: rgba(34, 197, 94, 0.14); color: #22c55e;") "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;",
)

View File

@@ -8,13 +8,20 @@ from nicegui import ui
class StrategyComparisonPanel: class StrategyComparisonPanel:
"""Interactive strategy comparison with scenario slider and cost-benefit table.""" """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.strategies = strategies or []
self.current_spot = current_spot self.current_spot = current_spot
self.price_change_pct = 0 self.price_change_pct = 0
self.strategy_cards: list[ui.html] = [] 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") 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"): 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") self.cards_container = ui.row().classes("w-full gap-4 max-lg:flex-col")
ui.separator().classes("my-2") 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.table_html = ui.html("").classes("w-full")
self.set_strategies(self.strategies, current_spot=current_spot) self.set_strategies(self.strategies, current_spot=current_spot)
@@ -101,9 +110,10 @@ class StrategyComparisonPanel:
floor = strategy.get("max_drawdown_floor", "") floor = strategy.get("max_drawdown_floor", "")
cap = strategy.get("upside_cap", "") cap = strategy.get("upside_cap", "")
scenario = self._scenario_benefit(strategy) 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" scenario_class = (
rows.append( "text-emerald-600 dark:text-emerald-400" if scenario >= 0 else "text-rose-600 dark:text-rose-400"
f""" )
rows.append(f"""
<tr class=\"border-b border-slate-200 dark:border-slate-800\"> <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 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\">${cost:,.2f}</td>
@@ -111,8 +121,7 @@ class StrategyComparisonPanel:
<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 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> <td class=\"px-4 py-3 font-semibold {scenario_class}\">${scenario:,.2f}</td>
</tr> </tr>
""" """)
)
return f""" return f"""
<div class=\"overflow-x-auto\"> <div class=\"overflow-x-auto\">
<table class=\"min-w-full rounded-xl overflow-hidden\"> <table class=\"min-w-full rounded-xl overflow-hidden\">
@@ -153,6 +162,6 @@ class StrategyComparisonPanel:
def _empty_row() -> str: def _empty_row() -> str:
return ( return (
'<tr><td colspan="5" class="px-4 py-6 text-center text-slate-500 dark:text-slate-400">' '<tr><td colspan="5" class="px-4 py-6 text-center text-slate-500 dark:text-slate-400">'
'No strategies loaded' "No strategies loaded"
'</td></tr>' "</td></tr>"
) )

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Iterable from collections.abc import Iterable
from app.models.option import OptionContract from app.models.option import OptionContract
from app.models.portfolio import LombardPortfolio from app.models.portfolio import LombardPortfolio

View File

@@ -40,7 +40,11 @@ __all__ = [
] ]
try: # pragma: no cover - optional QuantLib modules 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 from .volatility import implied_volatility
except ImportError: # pragma: no cover - optional dependency except ImportError: # pragma: no cover - optional dependency
AmericanOptionInputs = None AmericanOptionInputs = None

View File

@@ -105,15 +105,9 @@ def _american_price(
calendar = ql.NullCalendar() calendar = ql.NullCalendar()
spot_handle = ql.QuoteHandle(ql.SimpleQuote(used_spot)) spot_handle = ql.QuoteHandle(ql.SimpleQuote(used_spot))
dividend_curve = ql.YieldTermStructureHandle( dividend_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, params.dividend_yield, day_count))
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))
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( process = ql.BlackScholesMertonProcess(
spot_handle, spot_handle,
@@ -129,7 +123,9 @@ def _american_price(
return float(option.NPV()) 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. """Price an American option and estimate Greeks with finite differences.
Notes: Notes:

View File

@@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
import math
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, timedelta from datetime import date, timedelta
import math
from typing import Any, Literal from typing import Any, Literal
try: # pragma: no cover - optional dependency try: # pragma: no cover - optional dependency
@@ -97,8 +97,7 @@ def _analytic_black_scholes(params: BlackScholesInputs, option_type: OptionType)
sigma = params.volatility sigma = params.volatility
sqrt_t = math.sqrt(t) sqrt_t = math.sqrt(t)
d1 = ( d1 = (
math.log(params.spot / params.strike) math.log(params.spot / params.strike) + (params.risk_free_rate - params.dividend_yield + 0.5 * sigma**2) * t
+ (params.risk_free_rate - params.dividend_yield + 0.5 * sigma**2) * t
) / (sigma * sqrt_t) ) / (sigma * sqrt_t)
d2 = d1 - sigma * sqrt_t d2 = d1 - sigma * sqrt_t
disc_r = math.exp(-params.risk_free_rate * 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) gamma = (disc_q * pdf_d1) / (params.spot * sigma * sqrt_t)
vega = params.spot * disc_q * pdf_d1 * 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: 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() calendar = ql.NullCalendar()
spot_handle = ql.QuoteHandle(ql.SimpleQuote(params.spot)) spot_handle = ql.QuoteHandle(ql.SimpleQuote(params.spot))
dividend_curve = ql.YieldTermStructureHandle( dividend_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, params.dividend_yield, day_count))
ql.FlatForward(valuation_ql, params.dividend_yield, day_count) risk_free_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, params.risk_free_rate, day_count))
)
risk_free_curve = ql.YieldTermStructureHandle(
ql.FlatForward(valuation_ql, params.risk_free_rate, day_count)
)
volatility = ql.BlackVolTermStructureHandle( volatility = ql.BlackVolTermStructureHandle(
ql.BlackConstantVol(valuation_ql, calendar, params.volatility, day_count) ql.BlackConstantVol(valuation_ql, calendar, params.volatility, day_count)
) )

View File

@@ -93,12 +93,8 @@ def implied_volatility(
calendar = ql.NullCalendar() calendar = ql.NullCalendar()
spot_handle = ql.QuoteHandle(ql.SimpleQuote(spot)) spot_handle = ql.QuoteHandle(ql.SimpleQuote(spot))
dividend_curve = ql.YieldTermStructureHandle( dividend_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, dividend_yield, day_count))
ql.FlatForward(valuation_ql, dividend_yield, day_count) risk_free_curve = ql.YieldTermStructureHandle(ql.FlatForward(valuation_ql, risk_free_rate, day_count))
)
risk_free_curve = ql.YieldTermStructureHandle(
ql.FlatForward(valuation_ql, risk_free_rate, day_count)
)
volatility_curve = ql.BlackVolTermStructureHandle( volatility_curve = ql.BlackVolTermStructureHandle(
ql.BlackConstantVol(valuation_ql, calendar, initial_guess, day_count) ql.BlackConstantVol(valuation_ql, calendar, initial_guess, day_count)
) )

View File

@@ -14,8 +14,8 @@ from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from nicegui import ui from nicegui import ui
from app.api.routes import router as api_router
import app.pages # noqa: F401 import app.pages # noqa: F401
from app.api.routes import router as api_router
from app.services.cache import CacheService from app.services.cache import CacheService
from app.services.data_service import DataService from app.services.data_service import DataService
@@ -36,7 +36,7 @@ class Settings:
nicegui_storage_secret: str = "vault-dash-dev-secret" nicegui_storage_secret: str = "vault-dash-dev-secret"
@classmethod @classmethod
def load(cls) -> "Settings": def load(cls) -> Settings:
cls._load_dotenv() cls._load_dotenv()
origins = os.getenv("CORS_ORIGINS", "*") origins = os.getenv("CORS_ORIGINS", "*")
return cls( return cls(
@@ -169,4 +169,9 @@ ui.run_with(
if __name__ in {"__main__", "__mp_main__"}: if __name__ in {"__main__", "__mp_main__"}:
import uvicorn 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",
)

View File

@@ -72,7 +72,9 @@ class OptionContract:
"""Total premium paid or received for the position.""" """Total premium paid or received for the position."""
return self.premium * self.notional_units 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. """Classify the contract as ITM, ATM, or OTM.
Args: Args:

View File

@@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Iterator from typing import Any
from nicegui import ui from nicegui import ui
@@ -98,7 +99,13 @@ def option_chain() -> list[dict[str, Any]]:
distance = (strike - spot) / spot distance = (strike - spot) / spot
for option_type in ("put", "call"): for option_type in ("put", "call"):
premium_base = 8.2 if option_type == "put" else 7.1 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) delta = round((0.5 - distance * 1.8) * (-1 if option_type == "put" else 1), 3)
rows.append( 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), "gamma": round(max(0.012, 0.065 - abs(distance) * 0.12), 3),
"theta": round(-0.014 - abs(distance) * 0.025, 3), "theta": round(-0.014 - abs(distance) * 0.025, 3),
"vega": round(0.09 + max(0.0, 0.24 - abs(distance) * 0.6), 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 return rows
def strategy_metrics(strategy_name: str, scenario_pct: int) -> dict[str, Any]: 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() spot = demo_spot_price()
floor = float(strategy.get("max_drawdown_floor", spot * 0.95)) floor = float(strategy.get("max_drawdown_floor", spot * 0.95))
cap = strategy.get("upside_cap") cap = strategy.get("upside_cap")
@@ -157,7 +170,9 @@ def strategy_metrics(strategy_name: str, scenario_pct: int) -> dict[str, Any]:
"strategy": strategy, "strategy": strategy,
"scenario_pct": scenario_pct, "scenario_pct": scenario_pct,
"scenario_price": scenario_price, "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, "waterfall_steps": waterfall_steps,
"unhedged_equity": round(unhedged_equity, 2), "unhedged_equity": round(unhedged_equity, 2),
"hedged_equity": round(hedged_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") 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.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"): with ui.row().classes("items-center gap-3"):
ui.icon("shield").classes("text-2xl text-sky-500") ui.icon("shield").classes("text-2xl text-sky-500")
with ui.column().classes("gap-0"): 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"): with ui.row().classes("items-center gap-2 max-sm:flex-wrap"):
for key, href, label in NAV_ITEMS: for key, href, label in NAV_ITEMS:
active = key == current active = key == current
link_classes = ( link_classes = "rounded-lg px-4 py-2 text-sm font-medium no-underline transition " + (
"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
"bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900" else "text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
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) ui.link(label, href).classes(link_classes)

View File

@@ -2,7 +2,12 @@ from __future__ import annotations
from nicegui import ui 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: 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]}, "xAxis": {"type": "category", "data": [label for label, _ in steps]},
"yAxis": {"type": "value", "name": "USD"}, "yAxis": {"type": "value", "name": "USD"},
"series": [ "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", "type": "bar",
"stack": "total", "stack": "total",
@@ -73,19 +83,35 @@ def hedge_page() -> None:
"hedge", "hedge",
): ):
with ui.row().classes("w-full gap-6 max-lg:flex-col"): 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") 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_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") 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") charts_row = ui.row().classes("w-full gap-6 max-lg:flex-col")
with charts_row: 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") cost_chart = ui.echart(
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_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: def render_summary() -> None:
metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"]) metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"])
@@ -101,7 +127,9 @@ def hedge_page() -> None:
("Hedged equity", f"${metrics['hedged_equity']:,.0f}"), ("Hedged equity", f"${metrics['hedged_equity']:,.0f}"),
] ]
for label, value in cards: 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(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(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") ui.label(strategy["description"]).classes("text-sm text-slate-600 dark:text-slate-300")

View File

@@ -22,14 +22,34 @@ def options_page() -> None:
"options", "options",
): ):
with ui.row().classes("w-full gap-6 max-lg:flex-col"): 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") 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") 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") min_strike = ui.number(
max_strike = ui.number("Max strike", value=strike_range["max"], min=strike_values[0], max=strike_values[-1], step=5).classes("w-full") "Min strike",
strategy_select = ui.select([item["label"] for item in strategy_catalog()], value=selected_strategy["value"], label="Add to hedge strategy").classes("w-full") 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") chain_table = ui.html("").classes("w-full")
greeks = GreeksTable([]) greeks = GreeksTable([])
@@ -38,14 +58,17 @@ def options_page() -> None:
return [ return [
row row
for row in chain 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: def render_selection() -> None:
selection_card.clear() selection_card.clear()
with selection_card: with selection_card:
ui.label("Strategy Integration").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") 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: if not chosen_contracts:
ui.label("No contracts added yet.").classes("text-sm text-slate-500 dark:text-slate-400") ui.label("No contracts added yet.").classes("text-sm text-slate-500 dark:text-slate-400")
return return
@@ -58,11 +81,15 @@ def options_page() -> None:
chosen_contracts.append(contract) chosen_contracts.append(contract)
render_selection() render_selection()
greeks.set_options(chosen_contracts[-6:]) 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: def render_chain() -> None:
rows = filtered_rows() rows = filtered_rows()
chain_table.content = """ chain_table.content = (
"""
<div class='overflow-x-auto rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900'> <div class='overflow-x-auto rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900'>
<table class='min-w-full'> <table class='min-w-full'>
<thead class='bg-slate-100 dark:bg-slate-800'> <thead class='bg-slate-100 dark:bg-slate-800'>
@@ -76,8 +103,8 @@ def options_page() -> None:
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
""" + "".join( """
f""" + "".join(f"""
<tr class='border-b border-slate-200 dark:border-slate-800'> <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'>{row['symbol']}</td> <td class='px-4 py-3 font-medium text-slate-900 dark:text-slate-100'>{row['symbol']}</td>
<td class='px-4 py-3 text-slate-600 dark:text-slate-300'>{row['type'].upper()}</td> <td class='px-4 py-3 text-slate-600 dark:text-slate-300'>{row['type'].upper()}</td>
@@ -86,17 +113,24 @@ def options_page() -> None:
<td class='px-4 py-3 text-slate-600 dark:text-slate-300'{row['delta']:+.3f} · Γ {row['gamma']:.3f} · Θ {row['theta']:+.3f}</td> <td class='px-4 py-3 text-slate-600 dark:text-slate-300'{row['delta']:+.3f} · Γ {row['gamma']:.3f} · Θ {row['theta']:+.3f}</td>
<td class='px-4 py-3 text-sky-600 dark:text-sky-300'>Use quick-add buttons below</td> <td class='px-4 py-3 text-sky-600 dark:text-sky-300'>Use quick-add buttons below</td>
</tr> </tr>
""" """ for row in rows)
for row in rows + (
) + ("" if rows else "<tr><td colspan='6' class='px-4 py-6 text-center text-slate-500 dark:text-slate-400'>No contracts match the current filter.</td></tr>") + """ ""
if rows
else "<tr><td colspan='6' class='px-4 py-6 text-center text-slate-500 dark:text-slate-400'>No contracts match the current filter.</td></tr>"
)
+ """
</tbody> </tbody>
</table> </table>
</div> </div>
""" """
)
chain_table.update() chain_table.update()
quick_add.clear() quick_add.clear()
with quick_add: 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"): with ui.row().classes("w-full gap-2 max-sm:flex-col"):
for row in rows[:6]: for row in rows[:6]:
ui.button( ui.button(
@@ -105,14 +139,19 @@ def options_page() -> None:
).props("outline color=primary") ).props("outline color=primary")
greeks.set_options(rows[:6]) 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: def update_filters() -> None:
selected_expiry["value"] = expiry_select.value selected_expiry["value"] = expiry_select.value
strike_range["min"] = float(min_strike.value) strike_range["min"] = float(min_strike.value)
strike_range["max"] = float(max_strike.value) strike_range["max"] = float(max_strike.value)
if strike_range["min"] > strike_range["max"]: 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"] min_strike.value = strike_range["min"]
max_strike.value = strike_range["max"] max_strike.value = strike_range["max"]
render_chain() render_chain()
@@ -120,7 +159,12 @@ def options_page() -> None:
expiry_select.on_value_change(lambda _: update_filters()) expiry_select.on_value_change(lambda _: update_filters())
min_strike.on_value_change(lambda _: update_filters()) min_strike.on_value_change(lambda _: update_filters())
max_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_selection()
render_chain() render_chain()

View File

@@ -3,7 +3,13 @@ from __future__ import annotations
from nicegui import ui from nicegui import ui
from app.components import PortfolioOverview 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("/") @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"): with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"):
summary_cards = [ summary_cards = [
("Spot Price", f"${portfolio['spot_price']:,.2f}", "GLD reference price"), (
("Margin Call Price", f"${portfolio['margin_call_price']:,.2f}", "Implied trigger level"), "Spot Price",
("Cash Buffer", f"${portfolio['cash_buffer']:,.0f}", "Available liquidity"), f"${portfolio['spot_price']:,.2f}",
("Hedge Budget", f"${portfolio['hedge_budget']:,.0f}", "Approved premium budget"), "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: 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(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(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") 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) portfolio_view.update(portfolio)
with ui.row().classes("w-full gap-6 max-lg:flex-col"): 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"): 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("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( 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" "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( 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." 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") ).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." "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") ).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") ui.label("Strategy Snapshot").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
for strategy in strategy_catalog(): 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"): with ui.column().classes("gap-1"):
ui.label(strategy["label"]).classes("font-semibold text-slate-900 dark:text-slate-100") 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") ui.label(strategy["description"]).classes("text-sm text-slate-500 dark:text-slate-400")

View File

@@ -13,31 +13,55 @@ def settings_page() -> None:
"settings", "settings",
): ):
with ui.row().classes("w-full gap-6 max-lg:flex-col"): 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") 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") 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") 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") margin_threshold = ui.number("Margin call LTV", value=0.75, min=0.1, max=0.95, step=0.01).classes(
hedge_budget = ui.number("Monthly hedge budget", value=8000, min=0, step=500).classes("w-full") "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") 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") primary_source = ui.select(
fallback_source = ui.select(["fallback", "yfinance", "manual"], value="fallback", label="Fallback source").classes("w-full") ["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") refresh_interval = ui.number("Refresh interval (seconds)", value=5, min=1, step=1).classes("w-full")
ui.switch("Enable Redis cache", value=True) ui.switch("Enable Redis cache", value=True)
with ui.row().classes("w-full gap-6 max-lg:flex-col"): 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") 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") 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") vol_alert = ui.number("Volatility spike alert", value=0.25, min=0.01, max=2.0, step=0.01).classes(
price_alert = ui.number("Spot drawdown alert (%)", value=7.5, min=0.1, max=50.0, step=0.5).classes("w-full") "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) 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") 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 scenario history", value=True)
ui.switch("Include option selections", value=True) ui.switch("Include option selections", value=True)
ui.button("Import settings", icon="upload").props("outline color=primary") ui.button("Import settings", icon="upload").props("outline color=primary")

View File

@@ -70,12 +70,28 @@ class DataService:
"symbol": ticker, "symbol": ticker,
"updated_at": datetime.now(UTC).isoformat(), "updated_at": datetime.now(UTC).isoformat(),
"calls": [ "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": [ "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"], "source": quote["source"],
} }
@@ -100,8 +116,7 @@ class DataService:
}, },
"strategies": engine.compare_all_strategies(), "strategies": engine.compare_all_strategies(),
"recommendations": { "recommendations": {
profile: engine.recommend(profile) profile: engine.recommend(profile) for profile in ("conservative", "balanced", "cost_sensitive")
for profile in ("conservative", "balanced", "cost_sensitive")
}, },
"sensitivity_analysis": engine.sensitivity_analysis(), "sensitivity_analysis": engine.sensitivity_analysis(),
} }

View File

@@ -1,6 +1,6 @@
from .base import BaseStrategy, StrategyConfig from .base import BaseStrategy, StrategyConfig
from .engine import StrategySelectionEngine from .engine import StrategySelectionEngine
from .laddered_put import LadderSpec, LadderedPutStrategy from .laddered_put import LadderedPutStrategy, LadderSpec
from .lease import LeaseAnalysisSpec, LeaseStrategy from .lease import LeaseAnalysisSpec, LeaseStrategy
from .protective_put import ProtectivePutSpec, ProtectivePutStrategy from .protective_put import ProtectivePutSpec, ProtectivePutStrategy

View File

@@ -3,10 +3,14 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal 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.models.portfolio import LombardPortfolio
from app.strategies.base import BaseStrategy, StrategyConfig 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.lease import LeaseStrategy
from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy 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)), ProtectivePutStrategy(config, ProtectivePutSpec(label="OTM_90", strike_pct=0.90, months=12)),
LadderedPutStrategy( LadderedPutStrategy(
config, 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( LadderedPutStrategy(
config, 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), LeaseStrategy(config),
] ]
@@ -73,7 +87,10 @@ class StrategySelectionEngine:
protection_ltv = protection.get("hedged_ltv_at_threshold") protection_ltv = protection.get("hedged_ltv_at_threshold")
if protection_ltv is None: if protection_ltv is None:
duration_rows = protection.get("durations", []) 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( comparisons.append(
{ {
"name": strategy.name, "name": strategy.name,
@@ -140,7 +157,11 @@ class StrategySelectionEngine:
"recommended_strategy": recommendation["recommended_strategy"], "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( engine = StrategySelectionEngine(
portfolio_value=self.portfolio_value, portfolio_value=self.portfolio_value,
loan_amount=self.loan_amount, loan_amount=self.loan_amount,

View File

@@ -3,7 +3,11 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from app.strategies.base import BaseStrategy, StrategyConfig 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) @dataclass(frozen=True)
@@ -31,10 +35,16 @@ class LadderedPutStrategy(BaseStrategy):
def _legs(self) -> list[tuple[float, ProtectivePutStrategy]]: def _legs(self) -> list[tuple[float, ProtectivePutStrategy]]:
legs: 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( leg = ProtectivePutStrategy(
self.config, 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)) legs.append((weight, leg))
return legs return legs

View File

@@ -3,12 +3,28 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, timedelta 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.option import Greeks, OptionContract
from app.models.strategy import HedgingStrategy from app.models.strategy import HedgingStrategy
from app.strategies.base import BaseStrategy, StrategyConfig 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) @dataclass(frozen=True)

View File

@@ -1,32 +1,30 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project] [project]
name = "vault-dash" name = "vault-dash"
version = "0.1.0" version = "1.0.0"
description = "Real-time options hedging dashboard" description = "Options hedging dashboard for Lombard loan protection"
requires-python = ">=3.11" requires-python = ">=3.11"
[tool.black]
line-length = 88
target-version = ["py311"]
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 120
target-version = "py311" exclude = ["app/components/*.py", "app/pages/*.py"]
[tool.ruff.lint] [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] [tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
ignore_missing_imports = true ignore_missing_imports = true
pythonpath = ["."]
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"] testpaths = ["tests"]
python_files = ["test_*.py"] pythonpath = ["."]
addopts = "-v"

View File

@@ -4,11 +4,11 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import json
import sys import sys
import time import time
from urllib.error import HTTPError, URLError from urllib.error import HTTPError, URLError
from urllib.request import urlopen from urllib.request import urlopen
import json
def parse_args() -> argparse.Namespace: 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("--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("--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-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() return parser.parse_args()

View File

@@ -41,12 +41,28 @@ def sample_option_chain(sample_portfolio: LombardPortfolio) -> dict[str, object]
"updated_at": datetime(2026, 3, 21, 0, 0).isoformat(), "updated_at": datetime(2026, 3, 21, 0, 0).isoformat(),
"source": "mock", "source": "mock",
"calls": [ "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": [ "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. # datetime.UTC symbol used in the data_service module.
from app.services import data_service as 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: class FakeTicker:
def __init__(self, symbol: str) -> None: def __init__(self, symbol: str) -> None:

View File

@@ -4,7 +4,7 @@ import pytest
import app.core.pricing.black_scholes as black_scholes import app.core.pricing.black_scholes as black_scholes
from app.strategies.base import StrategyConfig 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 from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy
@@ -18,7 +18,10 @@ def test_protective_put_costs(
sample_strategy_config: StrategyConfig, sample_strategy_config: StrategyConfig,
) -> None: ) -> None:
_force_analytic_pricing(monkeypatch) _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() cost = strategy.calculate_cost()
assert cost["strategy"] == "protective_put_atm" assert cost["strategy"] == "protective_put_atm"
@@ -35,7 +38,12 @@ def test_laddered_strategy(sample_strategy_config: StrategyConfig, monkeypatch:
_force_analytic_pricing(monkeypatch) _force_analytic_pricing(monkeypatch)
strategy = LadderedPutStrategy( strategy = LadderedPutStrategy(
sample_strategy_config, 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() cost = strategy.calculate_cost()
protection = strategy.calculate_protection() protection = strategy.calculate_protection()
@@ -65,7 +73,12 @@ def test_scenario_analysis(
) )
ladder = LadderedPutStrategy( ladder = LadderedPutStrategy(
sample_strategy_config, 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() protective_scenarios = protective.get_scenarios()