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:
@@ -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});
|
||||||
}})();
|
}})();
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|||||||
@@ -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>"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;",
|
||||||
|
)
|
||||||
|
|||||||
@@ -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>"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
11
app/main.py
11
app/main.py
@@ -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",
|
||||||
|
)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,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"):
|
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"
|
"bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900"
|
||||||
if active
|
if active
|
||||||
else "text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
|
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)
|
||||||
|
|
||||||
with ui.row().classes("w-full items-end justify-between gap-4 max-md:flex-col max-md:items-start"):
|
with ui.row().classes("w-full items-end justify-between gap-4 max-md:flex-col max-md:items-start"):
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user