feat(DISPLAY-002): GLD mode shows real share prices
This commit is contained in:
@@ -5,7 +5,6 @@ import logging
|
||||
from fastapi.responses import RedirectResponse
|
||||
from nicegui import ui
|
||||
|
||||
from app.domain.conversions import collateral_to_display_units, format_display_quantity, get_display_mode_label
|
||||
from app.domain.portfolio_math import resolve_portfolio_spot_from_quote
|
||||
from app.models.workspace import get_workspace_repository
|
||||
from app.pages.common import (
|
||||
@@ -114,33 +113,33 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
"scenario_pct": 0,
|
||||
}
|
||||
|
||||
display_mode = portfolio.get("display_mode", "XAU")
|
||||
|
||||
if display_mode == "GLD":
|
||||
spot_unit = "/share"
|
||||
spot_desc = "GLD share price"
|
||||
else:
|
||||
spot_unit = "/oz"
|
||||
spot_desc = "converted collateral spot"
|
||||
|
||||
if quote_source == "configured_entry_price":
|
||||
spot_label = f"Current spot reference: ${portfolio['spot_price']:,.2f}/oz (configured entry price)"
|
||||
spot_label = f"Current spot reference: ${portfolio['spot_price']:,.2f}{spot_unit} (configured entry price)"
|
||||
else:
|
||||
spot_label = (
|
||||
f"Current spot reference: ${portfolio['spot_price']:,.2f}/oz (converted collateral spot via {quote_source})"
|
||||
f"Current spot reference: ${portfolio['spot_price']:,.2f}{spot_unit} ({spot_desc} via {quote_source})"
|
||||
)
|
||||
updated_label = f"Quote timestamp: {quote_updated_at}" if quote_updated_at else "Quote timestamp: unavailable"
|
||||
|
||||
# Get underlying and display mode for display
|
||||
# Get underlying for display
|
||||
underlying = "GLD"
|
||||
display_mode = "GLD"
|
||||
display_label = "GLD Shares"
|
||||
if workspace_id:
|
||||
try:
|
||||
repo = get_workspace_repository()
|
||||
config = repo.load_portfolio_config(workspace_id)
|
||||
underlying = config.underlying or "GLD"
|
||||
display_mode = config.display_mode or "GLD"
|
||||
display_label = get_display_mode_label(display_mode)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Compute display unit values
|
||||
collateral_qty, collateral_unit = collateral_to_display_units(
|
||||
float(portfolio.get("gold_units", 0.0)), display_mode
|
||||
)
|
||||
|
||||
with dashboard_page(
|
||||
"Hedge Analysis",
|
||||
f"Compare hedge structures across scenarios, visualize cost-benefit tradeoffs, and inspect net equity impacts for {underlying}.",
|
||||
@@ -148,10 +147,7 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
workspace_id=workspace_id,
|
||||
):
|
||||
with ui.row().classes("w-full items-center justify-between gap-4 max-md:flex-col max-md:items-start"):
|
||||
ui.label(
|
||||
f"Active underlying: {underlying} · Display mode: {display_label} · "
|
||||
f"Collateral: {format_display_quantity(collateral_qty, collateral_unit)} {collateral_unit}"
|
||||
).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
ui.label(f"Active underlying: {underlying}").classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
|
||||
left_pane, right_pane = split_page_panes(
|
||||
left_testid="hedge-left-pane",
|
||||
@@ -255,6 +251,17 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
def render_summary() -> None:
|
||||
metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"], portfolio=portfolio)
|
||||
strategy = metrics["strategy"]
|
||||
|
||||
# Display mode-aware labels
|
||||
if display_mode == "GLD":
|
||||
weight_unit = "shares"
|
||||
price_unit = "/share"
|
||||
hedge_cost_unit = "/share"
|
||||
else:
|
||||
weight_unit = "oz"
|
||||
price_unit = "/oz"
|
||||
hedge_cost_unit = "/oz"
|
||||
|
||||
summary.clear()
|
||||
with summary:
|
||||
ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
@@ -262,15 +269,11 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
"text-sm text-slate-500 dark:text-slate-400"
|
||||
)
|
||||
ui.label(strategy["description"]).classes("text-sm text-slate-600 dark:text-slate-300")
|
||||
# Compute display unit values for summary
|
||||
collateral_qty, collateral_unit = collateral_to_display_units(
|
||||
float(portfolio.get("gold_units", 0.0)), display_mode
|
||||
)
|
||||
with ui.grid(columns=1).classes("w-full gap-4 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2"):
|
||||
cards = [
|
||||
("Start value", f"${portfolio['gold_value']:,.0f}"),
|
||||
("Start price", f"${portfolio['spot_price']:,.2f}/oz"),
|
||||
("Weight", f"{format_display_quantity(collateral_qty, collateral_unit)} {collateral_unit}"),
|
||||
("Start price", f"${portfolio['spot_price']:,.2f}{price_unit}"),
|
||||
("Weight", f"{portfolio['gold_units']:,.0f} {weight_unit}"),
|
||||
("Loan amount", f"${portfolio['loan_amount']:,.0f}"),
|
||||
("Margin call LTV", f"{portfolio['margin_call_ltv']:.1%}"),
|
||||
("Monthly hedge budget", f"${portfolio['hedge_budget']:,.0f}"),
|
||||
@@ -287,8 +290,8 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
with ui.grid(columns=2).classes("w-full gap-4 max-md:grid-cols-1"):
|
||||
result_cards = [
|
||||
("Scenario spot", f"${metrics['scenario_price']:,.2f}"),
|
||||
("Hedge cost", f"${float(strategy.get('estimated_cost', 0.0)):,.2f}/oz"),
|
||||
("Scenario spot", f"${metrics['scenario_price']:,.2f}{price_unit}"),
|
||||
("Hedge cost", f"${float(strategy.get('estimated_cost', 0.0)):,.2f}{hedge_cost_unit}"),
|
||||
("Unhedged equity", f"${metrics['unhedged_equity']:,.0f}"),
|
||||
("Hedged equity", f"${metrics['hedged_equity']:,.0f}"),
|
||||
("Net hedge benefit", f"${metrics['hedged_equity'] - metrics['unhedged_equity']:,.0f}"),
|
||||
|
||||
@@ -8,7 +8,6 @@ from fastapi.responses import RedirectResponse
|
||||
from nicegui import ui
|
||||
|
||||
from app.components import PortfolioOverview
|
||||
from app.domain.conversions import collateral_to_display_units, format_display_quantity, get_display_mode_label
|
||||
from app.domain.portfolio_math import resolve_portfolio_spot_from_quote
|
||||
from app.models.ltv_history import LtvHistoryRepository
|
||||
from app.models.workspace import WORKSPACE_COOKIE, get_workspace_repository
|
||||
@@ -184,27 +183,38 @@ async def overview_page(workspace_id: str) -> None:
|
||||
ltv_chart_models = ()
|
||||
ltv_history_csv = ""
|
||||
ltv_history_notice = "Historical LTV is temporarily unavailable due to a storage error."
|
||||
display_mode = portfolio.get("display_mode", "XAU")
|
||||
|
||||
if portfolio["quote_source"] == "configured_entry_price":
|
||||
quote_status = "Live quote source: configured entry price fallback · Last updated Unavailable"
|
||||
if display_mode == "GLD":
|
||||
quote_status = "Live quote source: configured entry price fallback (GLD shares) · Last updated Unavailable"
|
||||
else:
|
||||
quote_status = "Live quote source: configured entry price fallback · Last updated Unavailable"
|
||||
else:
|
||||
quote_status = (
|
||||
f"Live quote source: {portfolio['quote_source']} · "
|
||||
f"GLD share quote converted to ozt-equivalent spot · "
|
||||
f"Last updated {_format_timestamp(str(portfolio['quote_updated_at']))}"
|
||||
if display_mode == "GLD":
|
||||
quote_status = (
|
||||
f"Live quote source: {portfolio['quote_source']} (GLD share price) · "
|
||||
f"Last updated {_format_timestamp(str(portfolio['quote_updated_at']))}"
|
||||
)
|
||||
else:
|
||||
quote_status = (
|
||||
f"Live quote source: {portfolio['quote_source']} · "
|
||||
f"GLD share quote converted to ozt-equivalent spot · "
|
||||
f"Last updated {_format_timestamp(str(portfolio['quote_updated_at']))}"
|
||||
)
|
||||
|
||||
if display_mode == "GLD":
|
||||
spot_caption = (
|
||||
f"{symbol} share price via {portfolio['quote_source']}"
|
||||
if portfolio["quote_source"] != "configured_entry_price"
|
||||
else "Configured GLD share entry price"
|
||||
)
|
||||
else:
|
||||
spot_caption = (
|
||||
f"{symbol} share quote converted to USD/ozt via {portfolio['quote_source']}"
|
||||
if portfolio["quote_source"] != "configured_entry_price"
|
||||
else "Configured entry price fallback in USD/ozt"
|
||||
)
|
||||
|
||||
spot_caption = (
|
||||
f"{symbol} share quote converted to USD/ozt via {portfolio['quote_source']}"
|
||||
if portfolio["quote_source"] != "configured_entry_price"
|
||||
else "Configured entry price fallback in USD/ozt"
|
||||
)
|
||||
|
||||
# Compute display mode values
|
||||
display_mode = config.display_mode or "GLD"
|
||||
display_label = get_display_mode_label(display_mode)
|
||||
collateral_display_qty, collateral_display_unit = collateral_to_display_units(
|
||||
float(config.gold_ounces or 0.0), display_mode
|
||||
)
|
||||
|
||||
with dashboard_page(
|
||||
"Overview",
|
||||
@@ -215,9 +225,7 @@ async def overview_page(workspace_id: str) -> None:
|
||||
with ui.row().classes("w-full items-center justify-between gap-4 max-md:flex-col max-md:items-start"):
|
||||
ui.label(quote_status).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
ui.label(
|
||||
f"Active underlying: {underlying} · Display mode: {display_label} · "
|
||||
f"Collateral: {format_display_quantity(collateral_display_qty, collateral_display_unit)} {collateral_display_unit} · "
|
||||
f"Loan ${config.loan_amount:,.0f}"
|
||||
f"Active underlying: {underlying} · Configured collateral baseline: ${config.gold_value:,.0f} · Loan ${config.loan_amount:,.0f}"
|
||||
).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
|
||||
left_pane, right_pane = split_page_panes(
|
||||
@@ -321,25 +329,27 @@ async def overview_page(workspace_id: str) -> None:
|
||||
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
):
|
||||
ui.label("Portfolio Snapshot").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
|
||||
# Display mode-aware labels
|
||||
if display_mode == "GLD":
|
||||
spot_label = "GLD Share Price"
|
||||
spot_unit = "/share"
|
||||
margin_label = "Margin Call Share Price"
|
||||
else:
|
||||
spot_label = "Collateral Spot Price"
|
||||
spot_unit = "/oz"
|
||||
margin_label = "Margin Call Price"
|
||||
|
||||
with ui.grid(columns=1).classes("w-full gap-4 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2"):
|
||||
# Compute display unit values for snapshot
|
||||
collateral_qty, collateral_unit = collateral_to_display_units(
|
||||
float(portfolio["gold_units"]), display_mode
|
||||
)
|
||||
summary_cards = [
|
||||
(
|
||||
"Collateral Spot Price",
|
||||
f"${portfolio['spot_price']:,.2f}/oz",
|
||||
spot_label,
|
||||
f"${portfolio['spot_price']:,.2f}{spot_unit}",
|
||||
spot_caption,
|
||||
),
|
||||
(
|
||||
"Collateral Amount",
|
||||
f"{format_display_quantity(collateral_qty, collateral_unit)} {collateral_unit}",
|
||||
f"Gold collateral in {display_label.lower()}",
|
||||
),
|
||||
(
|
||||
"Margin Call Price",
|
||||
f"${portfolio['margin_call_price']:,.2f}/oz",
|
||||
margin_label,
|
||||
f"${portfolio['margin_call_price']:,.2f}",
|
||||
"Implied trigger level from persisted portfolio settings",
|
||||
),
|
||||
(
|
||||
|
||||
@@ -250,6 +250,21 @@ def settings_page(workspace_id: str) -> None:
|
||||
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("Display Mode").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
ui.label(
|
||||
"Choose how to view your portfolio: GLD shares (financial instrument view) or physical gold ounces."
|
||||
).classes("text-sm text-slate-500 dark:text-slate-400 mb-3")
|
||||
display_mode = ui.select(
|
||||
{
|
||||
"GLD": "GLD Shares (show share prices directly)",
|
||||
"XAU": "Physical Gold (oz) (convert to gold ounces)",
|
||||
},
|
||||
value=config.display_mode,
|
||||
label="Display mode",
|
||||
).classes("w-full")
|
||||
|
||||
ui.separator().classes("my-4")
|
||||
|
||||
ui.label("Data Sources").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
underlying = ui.select(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user