feat: prioritize lazy options loading and live overview wiring

- queue OPS-001 Caddy route for vd1.uncloud.vpn
- lazy-load options expirations/chains per expiry
- wire overview to live quote data and persisted portfolio config
- extend browser test to verify live quote metadata
This commit is contained in:
Bu5hm4nn
2026-03-23 23:23:59 +01:00
parent d51fa05d5a
commit 133908dd36
6 changed files with 320 additions and 121 deletions

View File

@@ -11,19 +11,29 @@ from app.services.runtime import get_data_service
@ui.page("/options")
async def options_page() -> None:
chain_data = await get_data_service().get_options_chain("GLD")
chain = list(chain_data.get("rows") or [*chain_data.get("calls", []), *chain_data.get("puts", [])])
expiries = list(chain_data.get("expirations") or sorted({row["expiry"] for row in chain}))
strike_values = sorted({float(row["strike"]) for row in chain})
data_service = get_data_service()
expirations_data = await data_service.get_option_expirations("GLD")
expiries = list(expirations_data.get("expirations") or [])
default_expiry = expiries[0] if expiries else None
chain_data = await data_service.get_options_chain_for_expiry("GLD", default_expiry)
selected_expiry = {"value": expiries[0] if expiries else None}
strike_range = {
"min": strike_values[0] if strike_values else 0.0,
"max": strike_values[-1] if strike_values else 0.0,
chain_state = {
"data": chain_data,
"rows": list(chain_data.get("rows") or [*chain_data.get("calls", []), *chain_data.get("puts", [])]),
}
selected_expiry = {"value": chain_data.get("selected_expiry") or default_expiry}
selected_strategy = {"value": strategy_catalog()[0]["label"]}
chosen_contracts: list[dict[str, Any]] = []
def strike_bounds(rows: list[dict[str, Any]]) -> tuple[float, float]:
strike_values = sorted({float(row["strike"]) for row in rows})
if not strike_values:
return 0.0, 0.0
return strike_values[0], strike_values[-1]
initial_min_strike, initial_max_strike = strike_bounds(chain_state["rows"])
strike_range = {"min": initial_min_strike, "max": initial_max_strike}
with dashboard_page(
"Options Chain",
"Browse GLD contracts, filter by expiry and strike range, inspect Greeks, and attach contracts to hedge workflows.",
@@ -35,34 +45,17 @@ async def options_page() -> None:
):
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] if strike_values else 0.0,
max=strike_values[-1] if strike_values else 0.0,
step=5,
).classes("w-full")
max_strike = ui.number(
"Max strike",
value=strike_range["max"],
min=strike_values[0] if strike_values else 0.0,
max=strike_values[-1] if strike_values else 0.0,
step=5,
).classes("w-full")
min_strike = ui.number("Min strike", value=strike_range["min"], step=5).classes("w-full")
max_strike = ui.number("Max strike", value=strike_range["max"], 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")
source_label = f"Source: {chain_data.get('source', 'unknown')}"
if chain_data.get("updated_at"):
source_label += f" · Updated {chain_data['updated_at']}"
ui.label(source_label).classes("text-xs text-slate-500 dark:text-slate-400")
if chain_data.get("error"):
ui.label(f"Options data unavailable: {chain_data['error']}").classes(
"text-xs text-amber-700 dark:text-amber-300"
)
source_html = ui.html("").classes("text-xs text-slate-500 dark:text-slate-400")
error_html = ui.html("").classes("text-xs text-amber-700 dark:text-amber-300")
loading_html = ui.html("").classes("text-xs text-sky-700 dark:text-sky-300")
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"
@@ -70,15 +63,27 @@ async def options_page() -> None:
chain_table = ui.html("").classes("w-full")
greeks = GreeksTable([])
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 sync_status() -> None:
current_data = chain_state["data"]
source_label = f"Source: {current_data.get('source', 'unknown')}"
if current_data.get("updated_at"):
source_label += f" · Updated {current_data['updated_at']}"
source_html.content = source_label
source_html.update()
error_message = current_data.get("error") or expirations_data.get("error")
error_html.content = f"Options data unavailable: {error_message}" if error_message else ""
error_html.update()
def filtered_rows() -> list[dict[str, Any]]:
if not selected_expiry["value"]:
return []
return [
row
for row in chain
if row["expiry"] == selected_expiry["value"]
and strike_range["min"] <= float(row["strike"]) <= strike_range["max"]
for row in chain_state["rows"]
if strike_range["min"] <= float(row["strike"]) <= strike_range["max"]
]
def render_selection() -> None:
@@ -100,10 +105,7 @@ async 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()
@@ -125,7 +127,8 @@ async def options_page() -> None:
</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>
@@ -136,7 +139,9 @@ async def options_page() -> None:
<td class='px-4 py-3 text-slate-600 dark:text-slate-300'{float(row.get('delta', 0.0)):+.3f} · Γ {float(row.get('gamma', 0.0)):.3f} · Θ {float(row.get('theta', 0.0)):+.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)
"""
for row in rows
)
+ (
""
if rows
@@ -162,32 +167,48 @@ async 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"
)
async def load_expiry_chain(expiry: str | None) -> None:
selected_expiry["value"] = expiry
loading_html.content = "Loading selected expiry…" if expiry else ""
loading_html.update()
next_chain = await data_service.get_options_chain_for_expiry("GLD", expiry)
chain_state["data"] = next_chain
chain_state["rows"] = list(next_chain.get("rows") or [*next_chain.get("calls", []), *next_chain.get("puts", [])])
min_value, max_value = strike_bounds(chain_state["rows"])
strike_range["min"] = min_value
strike_range["max"] = max_value
min_strike.value = min_value
max_strike.value = max_value
loading_html.content = ""
loading_html.update()
sync_status()
render_chain()
def update_filters() -> None:
selected_expiry["value"] = expiry_select.value
strike_range["min"] = float(min_strike.value or 0.0)
strike_range["max"] = float(max_strike.value or 0.0)
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()
expiry_select.on_value_change(lambda _: update_filters())
async def on_expiry_change(event: Any) -> None:
await load_expiry_chain(event.value)
expiry_select.on_value_change(on_expiry_change)
min_strike.on_value_change(lambda _: update_filters())
max_strike.on_value_change(lambda _: update_filters())
def on_strategy_change(event) -> None:
selected_strategy["value"] = event.value # type: ignore[assignment]
def on_strategy_change(event: Any) -> None:
selected_strategy["value"] = event.value
render_selection()
strategy_select.on_value_change(on_strategy_change)
sync_status()
render_selection()
render_chain()