feat(DATA-002): add live GLD options chain data via yfinance
This commit is contained in:
@@ -5,16 +5,22 @@ from typing import Any
|
||||
from nicegui import ui
|
||||
|
||||
from app.components import GreeksTable
|
||||
from app.pages.common import dashboard_page, option_chain, strategy_catalog
|
||||
from app.pages.common import dashboard_page, strategy_catalog
|
||||
from app.services.runtime import get_data_service
|
||||
|
||||
|
||||
@ui.page("/options")
|
||||
def options_page() -> None:
|
||||
chain = option_chain()
|
||||
expiries = sorted({row["expiry"] for row in chain})
|
||||
strike_values = sorted({row["strike"] for row in chain})
|
||||
selected_expiry = {"value": expiries[0]}
|
||||
strike_range = {"min": strike_values[0], "max": strike_values[-1]}
|
||||
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})
|
||||
|
||||
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,
|
||||
}
|
||||
selected_strategy = {"value": strategy_catalog()[0]["label"]}
|
||||
chosen_contracts: list[dict[str, Any]] = []
|
||||
|
||||
@@ -32,15 +38,15 @@ def options_page() -> None:
|
||||
min_strike = ui.number(
|
||||
"Min strike",
|
||||
value=strike_range["min"],
|
||||
min=strike_values[0],
|
||||
max=strike_values[-1],
|
||||
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],
|
||||
max=strike_values[-1],
|
||||
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")
|
||||
strategy_select = ui.select(
|
||||
@@ -49,6 +55,15 @@ def options_page() -> None:
|
||||
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"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
@@ -56,12 +71,14 @@ def options_page() -> None:
|
||||
chain_table = ui.html("").classes("w-full")
|
||||
greeks = GreeksTable([])
|
||||
|
||||
def filtered_rows() -> list[dict]:
|
||||
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"] <= row["strike"] <= strike_range["max"]
|
||||
and strike_range["min"] <= float(row["strike"]) <= strike_range["max"]
|
||||
]
|
||||
|
||||
def render_selection() -> None:
|
||||
@@ -76,10 +93,10 @@ def options_page() -> None:
|
||||
return
|
||||
for contract in chosen_contracts[-3:]:
|
||||
ui.label(
|
||||
f"{contract['symbol']} · premium ${contract['premium']:.2f} · Δ {contract['delta']:+.3f}"
|
||||
f"{contract['symbol']} · premium ${float(contract['premium']):.2f} · IV {float(contract.get('impliedVolatility', 0.0)):.1%}"
|
||||
).classes("text-sm text-slate-600 dark:text-slate-300")
|
||||
|
||||
def add_to_strategy(contract: dict) -> None:
|
||||
def add_to_strategy(contract: dict[str, Any]) -> None:
|
||||
chosen_contracts.append(contract)
|
||||
render_selection()
|
||||
greeks.set_options(chosen_contracts[-6:])
|
||||
@@ -100,6 +117,8 @@ def options_page() -> None:
|
||||
<th class='px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300'>Type</th>
|
||||
<th class='px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300'>Strike</th>
|
||||
<th class='px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300'>Bid / Ask</th>
|
||||
<th class='px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300'>Last</th>
|
||||
<th class='px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300'>IV</th>
|
||||
<th class='px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300'>Greeks</th>
|
||||
<th class='px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300'>Action</th>
|
||||
</tr>
|
||||
@@ -110,16 +129,18 @@ def options_page() -> None:
|
||||
<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>
|
||||
<td class='px-4 py-3 text-slate-600 dark:text-slate-300'>${row['strike']:.2f}</td>
|
||||
<td class='px-4 py-3 text-slate-600 dark:text-slate-300'>${row['bid']:.2f} / ${row['ask']:.2f}</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-slate-600 dark:text-slate-300'>${float(row['strike']):.2f}</td>
|
||||
<td class='px-4 py-3 text-slate-600 dark:text-slate-300'>${float(row['bid']):.2f} / ${float(row['ask']):.2f}</td>
|
||||
<td class='px-4 py-3 text-slate-600 dark:text-slate-300'>${float(row.get('lastPrice', row.get('premium', 0.0))):.2f}</td>
|
||||
<td class='px-4 py-3 text-slate-600 dark:text-slate-300'>{float(row.get('impliedVolatility', 0.0)):.1%}</td>
|
||||
<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)
|
||||
+ (
|
||||
""
|
||||
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>"
|
||||
else "<tr><td colspan='8' class='px-4 py-6 text-center text-slate-500 dark:text-slate-400'>No contracts match the current filter.</td></tr>"
|
||||
)
|
||||
+ """
|
||||
</tbody>
|
||||
@@ -136,7 +157,7 @@ def options_page() -> None:
|
||||
with ui.row().classes("w-full gap-2 max-sm:flex-col"):
|
||||
for row in rows[:6]:
|
||||
ui.button(
|
||||
f"Add {row['type'].upper()} {row['strike']:.0f}",
|
||||
f"Add {row['type'].upper()} {float(row['strike']):.0f}",
|
||||
on_click=lambda _, contract=row: add_to_strategy(contract),
|
||||
).props("outline color=primary")
|
||||
greeks.set_options(rows[:6])
|
||||
@@ -147,8 +168,8 @@ def options_page() -> None:
|
||||
|
||||
def update_filters() -> None:
|
||||
selected_expiry["value"] = expiry_select.value
|
||||
strike_range["min"] = float(min_strike.value)
|
||||
strike_range["max"] = float(max_strike.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"],
|
||||
@@ -161,6 +182,7 @@ 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())
|
||||
|
||||
def on_strategy_change(event) -> None:
|
||||
selected_strategy["value"] = event.value # type: ignore[assignment]
|
||||
render_selection()
|
||||
|
||||
Reference in New Issue
Block a user