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
if _CHARTS_SCRIPT_ADDED:
return
ui.add_head_html(
"""
ui.add_head_html("""
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
"""
)
""")
_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});
}})();
"""
)
""")

View File

@@ -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 (
'<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'{cells}'
'</tr>'
f"{cells}"
"</tr>"
)
@staticmethod
@@ -99,6 +101,6 @@ class GreeksTable:
def _empty_row() -> str:
return (
'<tr><td colspan="6" class="px-4 py-6 text-center text-slate-500 dark:text-slate-400">'
'No options selected'
'</td></tr>'
"No options selected"
"</td></tr>"
)

View File

@@ -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;",
)

View File

@@ -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"""
<tr class=\"border-b border-slate-200 dark:border-slate-800\">
<td class=\"px-4 py-3 font-medium text-slate-900 dark:text-slate-100\">{name}</td>
<td class=\"px-4 py-3 text-slate-600 dark:text-slate-300\">${cost:,.2f}</td>
@@ -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 font-semibold {scenario_class}\">${scenario:,.2f}</td>
</tr>
"""
)
""")
return f"""
<div class=\"overflow-x-auto\">
<table class=\"min-w-full rounded-xl overflow-hidden\">
@@ -153,6 +162,6 @@ class StrategyComparisonPanel:
def _empty_row() -> str:
return (
'<tr><td colspan="5" class="px-4 py-6 text-center text-slate-500 dark:text-slate-400">'
'No strategies loaded'
'</td></tr>'
"No strategies loaded"
"</td></tr>"
)

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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",
)

View File

@@ -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:

View File

@@ -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,14 +195,11 @@ 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 "
+ (
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)
with ui.row().classes("w-full items-end justify-between gap-4 max-md:flex-col max-md:items-start"):

View File

@@ -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")

View File

@@ -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 = (
"""
<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'>
<thead class='bg-slate-100 dark:bg-slate-800'>
@@ -76,8 +103,8 @@ def options_page() -> None:
</tr>
</thead>
<tbody>
""" + "".join(
f"""
"""
+ "".join(f"""
<tr class='border-b border-slate-200 dark:border-slate-800'>
<td class='px-4 py-3 font-medium text-slate-900 dark:text-slate-100'>{row['symbol']}</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-sky-600 dark:text-sky-300'>Use quick-add buttons below</td>
</tr>
"""
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>") + """
""" 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>"
)
+ """
</tbody>
</table>
</div>
"""
)
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()

View File

@@ -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")

View File

@@ -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")

View File

@@ -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(),
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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"

View File

@@ -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()

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(),
"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:

View File

@@ -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()