feat(CORE-002B): roll out hedge quote unit conversion

This commit is contained in:
Bu5hm4nn
2026-03-25 15:46:44 +01:00
parent f00b58bba0
commit 829c0b5da2
7 changed files with 223 additions and 30 deletions

View File

@@ -1,17 +1,22 @@
from __future__ import annotations
import logging
from fastapi.responses import RedirectResponse
from nicegui import ui
from app.domain.portfolio_math import resolve_portfolio_spot_from_quote
from app.models.workspace import get_workspace_repository
from app.pages.common import (
dashboard_page,
demo_spot_price,
portfolio_snapshot,
render_workspace_recovery,
strategy_catalog,
strategy_metrics,
)
from app.services.runtime import get_data_service
logger = logging.getLogger(__name__)
def _cost_benefit_options(metrics: dict) -> dict:
@@ -35,7 +40,6 @@ def _cost_benefit_options(metrics: dict) -> dict:
}
def _waterfall_options(metrics: dict) -> dict:
steps = metrics["waterfall_steps"]
values: list[dict[str, object]] = []
@@ -57,22 +61,49 @@ def _waterfall_options(metrics: dict) -> dict:
@ui.page("/{workspace_id}/hedge")
def workspace_hedge_page(workspace_id: str) -> None:
async def workspace_hedge_page(workspace_id: str) -> None:
repo = get_workspace_repository()
if not repo.workspace_exists(workspace_id):
return RedirectResponse(url="/", status_code=307)
_render_hedge_page(workspace_id=workspace_id)
await _render_hedge_page(workspace_id=workspace_id)
def _render_hedge_page(workspace_id: str | None = None) -> None:
async def _resolve_hedge_spot(workspace_id: str | None = None) -> tuple[dict[str, float], str, str]:
"""Resolve hedge page spot price using the same quote-unit seam as overview."""
repo = get_workspace_repository()
config = repo.load_portfolio_config(workspace_id) if workspace_id else None
portfolio = portfolio_snapshot(config)
if config is None:
return {"spot_price": demo_spot_price()}, "demo", ""
try:
data_service = get_data_service()
quote = await data_service.get_quote(data_service.default_symbol)
spot, source, updated_at = resolve_portfolio_spot_from_quote(
config, quote, fallback_symbol=data_service.default_symbol
)
portfolio = portfolio_snapshot(config, runtime_spot_price=spot)
return portfolio, source, updated_at
except Exception as exc:
logger.warning("Falling back to configured hedge spot for workspace %s: %s", workspace_id, exc)
portfolio = portfolio_snapshot(config)
return portfolio, "configured_entry_price", ""
async def _render_hedge_page(workspace_id: str | None = None) -> None:
repo = get_workspace_repository()
config = repo.load_portfolio_config(workspace_id) if workspace_id else None
portfolio, quote_source, quote_updated_at = await _resolve_hedge_spot(workspace_id)
strategies = strategy_catalog()
strategy_map = {strategy["label"]: strategy["name"] for strategy in strategies}
selected = {"strategy": strategies[0]["name"], "label": strategies[0]["label"], "scenario_pct": 0}
if quote_source == "configured_entry_price":
spot_label = f"Current spot reference: ${portfolio['spot_price']:,.2f}/oz (configured entry price)"
else:
spot_label = (
f"Current spot reference: ${portfolio['spot_price']:,.2f}/oz (converted collateral spot via {quote_source})"
)
with dashboard_page(
"Hedge Analysis",
"Compare hedge structures across scenarios, visualize cost-benefit tradeoffs, and inspect net equity impacts.",
@@ -87,9 +118,7 @@ def _render_hedge_page(workspace_id: str | None = None) -> None:
selector = ui.select(strategy_map, value=selected["label"], label="Strategy selector").classes("w-full")
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")
ui.label(f"Current spot reference: ${portfolio['spot_price']:,.2f}").classes(
"text-sm text-slate-500 dark:text-slate-400"
)
ui.label(spot_label).classes("text-sm text-slate-500 dark:text-slate-400")
if workspace_id:
ui.label(f"Workspace route: /{workspace_id}/hedge").classes(
"text-xs text-slate-500 dark:text-slate-400"