feat(PRICING-002): add GLD/GC=F basis display on overview

This commit is contained in:
Bu5hm4nn
2026-03-28 09:18:26 +01:00
parent 894d88f72f
commit 9d06313480
3 changed files with 380 additions and 1 deletions

View File

@@ -152,6 +152,13 @@ async def overview_page(workspace_id: str) -> None:
source=overview_source,
updated_at=overview_updated_at,
)
# Fetch basis data for GLD/GC=F comparison
try:
basis_data = await data_service.get_basis_data()
except Exception:
logger.exception("Failed to fetch basis data")
basis_data = None
configured_gold_value = float(config.gold_value or 0.0)
portfolio["cash_buffer"] = max(float(portfolio["gold_value"]) - configured_gold_value, 0.0) + _DEFAULT_CASH_BUFFER
portfolio["hedge_budget"] = float(config.monthly_budget)
@@ -208,6 +215,78 @@ async def overview_page(workspace_id: str) -> None:
)
with left_pane:
# GLD/GC=F Basis Card
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 gap-3"):
ui.label("GLD/GC=F Basis").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
if basis_data:
basis_badge_class = {
"green": "rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
"yellow": "rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
"red": "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",
}.get(
basis_data["basis_status"],
"rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700",
)
ui.label(f"{basis_data['basis_label']} ({basis_data['basis_bps']:+.1f} bps)").classes(
basis_badge_class
)
if basis_data:
with ui.grid(columns=2).classes("w-full gap-4 mt-4"):
# GLD Implied Spot
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("GLD Implied Spot").classes(
"text-sm font-medium text-slate-500 dark:text-slate-400"
)
ui.label(f"${basis_data['gld_implied_spot']:,.2f}/oz").classes(
"text-2xl font-bold text-slate-900 dark:text-slate-50"
)
ui.label(
f"GLD ${basis_data['gld_price']:.2f} ÷ {basis_data['gld_ounces_per_share']:.4f} oz/share"
).classes("text-xs text-slate-500 dark:text-slate-400")
# GC=F Adjusted
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("GC=F Adjusted").classes("text-sm font-medium text-slate-500 dark:text-slate-400")
ui.label(f"${basis_data['gc_f_adjusted']:,.2f}/oz").classes(
"text-2xl font-bold text-slate-900 dark:text-slate-50"
)
ui.label(
f"GC=F ${basis_data['gc_f_price']:.2f} - ${basis_data['contango_estimate']:.0f} contango"
).classes("text-xs text-slate-500 dark:text-slate-400")
# Basis explanation and after-hours notice
with ui.row().classes("w-full items-start gap-2 mt-4"):
ui.icon("info", size="xs").classes("text-slate-400 mt-0.5")
ui.label(
"Basis shows the premium/discount between GLD-implied gold and futures-adjusted spot. "
"Green < 25 bps (normal), Yellow 25-50 bps (elevated), Red > 50 bps (unusual)."
).classes("text-xs text-slate-500 dark:text-slate-400")
if basis_data["after_hours"]:
with ui.row().classes("w-full items-start gap-2 mt-2"):
ui.icon("schedule", size="xs").classes("text-amber-500 mt-0.5")
ui.label(
f"{basis_data['after_hours_note']} · GLD: {_format_timestamp(basis_data['gld_updated_at'])} · "
f"GC=F: {_format_timestamp(basis_data['gc_f_updated_at'])}"
).classes("text-xs text-amber-700 dark:text-amber-300")
# Warning for elevated basis
if basis_data["basis_status"] == "red":
ui.label(
f"⚠️ Elevated basis detected: {basis_data['basis_bps']:+.1f} bps. "
"This may indicate after-hours pricing gaps, physical stress, or arbitrage disruption."
).classes("text-sm font-medium text-rose-700 dark:text-rose-300 mt-3")
else:
ui.label("Basis data temporarily unavailable").classes("text-sm text-slate-500 dark:text-slate-400")
with ui.card().classes(
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
):