feat(PORT-004C): seed workspace routes from portfolio settings

This commit is contained in:
Bu5hm4nn
2026-03-24 21:14:09 +01:00
parent 2cbe4f274d
commit 5ac66ea97b
8 changed files with 350 additions and 51 deletions

View File

@@ -2,9 +2,12 @@ from __future__ import annotations
from datetime import date, datetime from datetime import date, datetime
from fastapi import Request
from fastapi.responses import RedirectResponse
from nicegui import ui from nicegui import ui
from app.pages.common import dashboard_page from app.models.workspace import WORKSPACE_COOKIE, get_workspace_repository
from app.pages.common import dashboard_page, render_workspace_recovery
from app.services.backtesting.ui_service import BacktestPageRunResult, BacktestPageService from app.services.backtesting.ui_service import BacktestPageRunResult, BacktestPageService
@@ -47,16 +50,45 @@ def _chart_options(result: BacktestPageRunResult) -> dict:
@ui.page("/backtests") @ui.page("/backtests")
def backtests_page() -> None: def legacy_backtests_page(request: Request):
repo = get_workspace_repository()
workspace_id = request.cookies.get(WORKSPACE_COOKIE, "")
if workspace_id and repo.workspace_exists(workspace_id):
return RedirectResponse(url=f"/{workspace_id}/backtests", status_code=307)
_render_backtests_page()
@ui.page("/{workspace_id}/backtests")
def workspace_backtests_page(workspace_id: str) -> None:
repo = get_workspace_repository()
if not repo.workspace_exists(workspace_id):
render_workspace_recovery()
return
_render_backtests_page(workspace_id=workspace_id)
def _render_backtests_page(workspace_id: str | None = None) -> None:
service = BacktestPageService() service = BacktestPageService()
template_options = service.template_options("GLD") template_options = service.template_options("GLD")
select_options = {str(option["slug"]): str(option["label"]) for option in template_options} select_options = {str(option["slug"]): str(option["label"]) for option in template_options}
default_template_slug = str(template_options[0]["slug"]) if template_options else None default_template_slug = str(template_options[0]["slug"]) if template_options else None
repo = get_workspace_repository()
config = repo.load_portfolio_config(workspace_id) if workspace_id else None
default_entry_spot = service.derive_entry_spot("GLD", date(2024, 1, 2), date(2024, 1, 8))
default_units = (
float(config.gold_value or 0.0) / default_entry_spot
if config and config.gold_value is not None and default_entry_spot > 0
else 1000.0
)
default_loan = float(config.loan_amount) if config else 68000.0
default_margin_call_ltv = float(config.margin_threshold) if config else 0.75
with dashboard_page( with dashboard_page(
"Backtests", "Backtests",
"Run a thin read-only historical scenario over the BT-001 synthetic engine and inspect the daily path.", "Run a thin read-only historical scenario over the BT-001 synthetic engine and inspect the daily path.",
"backtests", "backtests",
workspace_id=workspace_id,
): ):
with ui.row().classes("w-full gap-6 max-lg:flex-col"): with ui.row().classes("w-full gap-6 max-lg:flex-col"):
with ui.card().classes( with ui.card().classes(
@@ -69,15 +101,25 @@ def backtests_page() -> None:
ui.label("BT-001A currently supports GLD only for this thin read-only page.").classes( ui.label("BT-001A currently supports GLD only for this thin read-only page.").classes(
"text-sm text-slate-500 dark:text-slate-400" "text-sm text-slate-500 dark:text-slate-400"
) )
if workspace_id:
ui.label("Workspace defaults seed underlying units, loan amount, and margin threshold.").classes(
"text-sm text-slate-500 dark:text-slate-400"
)
symbol_input = ui.input("Symbol", value="GLD").props("readonly").classes("w-full") symbol_input = ui.input("Symbol", value="GLD").props("readonly").classes("w-full")
start_input = ui.input("Start date", value="2024-01-02").classes("w-full") start_input = ui.input("Start date", value="2024-01-02").classes("w-full")
end_input = ui.input("End date", value="2024-01-08").classes("w-full") end_input = ui.input("End date", value="2024-01-08").classes("w-full")
template_select = ui.select(select_options, value=default_template_slug, label="Template").classes( template_select = ui.select(select_options, value=default_template_slug, label="Template").classes(
"w-full" "w-full"
) )
units_input = ui.number("Underlying units", value=1000.0, min=0.0001, step=1).classes("w-full") units_input = ui.number("Underlying units", value=default_units, min=0.0001, step=1).classes("w-full")
loan_input = ui.number("Loan amount", value=68000.0, min=0, step=1000).classes("w-full") loan_input = ui.number("Loan amount", value=default_loan, min=0, step=1000).classes("w-full")
ltv_input = ui.number("Margin call LTV", value=0.75, min=0.01, max=0.99, step=0.01).classes("w-full") ltv_input = ui.number(
"Margin call LTV",
value=default_margin_call_ltv,
min=0.01,
max=0.99,
step=0.01,
).classes("w-full")
entry_spot_hint = ui.label("Entry spot will be auto-derived on run.").classes( entry_spot_hint = ui.label("Entry spot will be auto-derived on run.").classes(
"text-sm text-slate-500 dark:text-slate-400" "text-sm text-slate-500 dark:text-slate-400"
) )

View File

@@ -6,6 +6,7 @@ from typing import Any
from nicegui import ui from nicegui import ui
from app.models.portfolio import PortfolioConfig
from app.services.strategy_templates import StrategyTemplateService from app.services.strategy_templates import StrategyTemplateService
NAV_ITEMS: list[tuple[str, str, str]] = [ NAV_ITEMS: list[tuple[str, str, str]] = [
@@ -23,10 +24,10 @@ def nav_items(workspace_id: str | None = None) -> list[tuple[str, str, str]]:
return NAV_ITEMS return NAV_ITEMS
return [ return [
("overview", f"/{workspace_id}", "Overview"), ("overview", f"/{workspace_id}", "Overview"),
("hedge", "/hedge", "Hedge Analysis"), ("hedge", f"/{workspace_id}/hedge", "Hedge Analysis"),
("options", "/options", "Options Chain"), ("options", "/options", "Options Chain"),
("backtests", "/backtests", "Backtests"), ("backtests", f"/{workspace_id}/backtests", "Backtests"),
("event-comparison", "/event-comparison", "Event Comparison"), ("event-comparison", f"/{workspace_id}/event-comparison", "Event Comparison"),
("settings", f"/{workspace_id}/settings", "Settings"), ("settings", f"/{workspace_id}/settings", "Settings"),
] ]
@@ -35,22 +36,33 @@ def demo_spot_price() -> float:
return 215.0 return 215.0
def portfolio_snapshot() -> dict[str, float]: def portfolio_snapshot(config: PortfolioConfig | None = None) -> dict[str, float]:
if config is None:
gold_units = 1_000.0 gold_units = 1_000.0
spot = demo_spot_price() spot = demo_spot_price()
gold_value = gold_units * spot gold_value = gold_units * spot
loan_amount = 145_000.0 loan_amount = 145_000.0
margin_call_ltv = 0.75 margin_call_ltv = 0.75
hedge_budget = 8_000.0
else:
gold_units = float(config.gold_ounces or 0.0)
spot = float(config.entry_price or 0.0)
gold_value = float(config.gold_value or gold_units * spot)
loan_amount = float(config.loan_amount)
margin_call_ltv = float(config.margin_threshold)
hedge_budget = float(config.monthly_budget)
return { return {
"gold_value": gold_value, "gold_value": gold_value,
"loan_amount": loan_amount, "loan_amount": loan_amount,
"ltv_ratio": loan_amount / gold_value, "ltv_ratio": loan_amount / gold_value,
"net_equity": gold_value - loan_amount, "net_equity": gold_value - loan_amount,
"spot_price": spot, "spot_price": spot,
"gold_units": gold_units,
"margin_call_ltv": margin_call_ltv, "margin_call_ltv": margin_call_ltv,
"margin_call_price": loan_amount / (margin_call_ltv * gold_units), "margin_call_price": loan_amount / (margin_call_ltv * gold_units),
"cash_buffer": 18_500.0, "cash_buffer": 18_500.0,
"hedge_budget": 8_000.0, "hedge_budget": hedge_budget,
} }
@@ -122,12 +134,17 @@ def option_chain() -> list[dict[str, Any]]:
return rows return rows
def strategy_metrics(strategy_name: str, scenario_pct: int) -> dict[str, Any]: def strategy_metrics(
strategy_name: str,
scenario_pct: int,
*,
portfolio: dict[str, Any] | None = None,
) -> dict[str, Any]:
strategy = next( strategy = next(
(item for item in strategy_catalog() if item["name"] == strategy_name), (item for item in strategy_catalog() if item["name"] == strategy_name),
strategy_catalog()[0], strategy_catalog()[0],
) )
portfolio = portfolio_snapshot() portfolio = portfolio or portfolio_snapshot()
spot = float(portfolio["spot_price"]) spot = float(portfolio["spot_price"])
underlying_units = portfolio["gold_value"] / spot underlying_units = portfolio["gold_value"] / spot
loan_amount = float(portfolio["loan_amount"]) loan_amount = float(portfolio["loan_amount"])
@@ -209,6 +226,21 @@ def dashboard_page(title: str, subtitle: str, current: str, workspace_id: str |
yield container yield container
def render_workspace_recovery(title: str = "Workspace not found", message: str | None = None) -> None:
resolved_message = message or "The requested workspace is unavailable. Start a new workspace or return to the welcome page."
with ui.column().classes("mx-auto mt-24 w-full max-w-2xl gap-6 px-6 text-center"):
ui.icon("folder_off").classes("mx-auto text-6xl text-slate-400")
ui.label(title).classes("text-3xl font-bold text-slate-900 dark:text-slate-50")
ui.label(resolved_message).classes("text-base text-slate-500 dark:text-slate-400")
with ui.row().classes("mx-auto gap-3"):
ui.link("Get started", "/workspaces/bootstrap").classes(
"rounded-lg bg-slate-900 px-5 py-3 text-sm font-semibold text-white no-underline dark:bg-slate-100 dark:text-slate-900"
)
ui.link("Go to welcome page", "/").classes(
"rounded-lg border border-slate-300 px-5 py-3 text-sm font-semibold text-slate-700 no-underline dark:border-slate-700 dark:text-slate-200"
)
def recommendation_style(tone: str) -> str: def recommendation_style(tone: str) -> str:
return { return {
"positive": "border-emerald-200 bg-emerald-50 dark:border-emerald-900/60 dark:bg-emerald-950/30", "positive": "border-emerald-200 bg-emerald-50 dark:border-emerald-900/60 dark:bg-emerald-950/30",

View File

@@ -1,8 +1,11 @@
from __future__ import annotations from __future__ import annotations
from fastapi import Request
from fastapi.responses import RedirectResponse
from nicegui import ui from nicegui import ui
from app.pages.common import dashboard_page from app.models.workspace import WORKSPACE_COOKIE, get_workspace_repository
from app.pages.common import dashboard_page, render_workspace_recovery
from app.services.event_comparison_ui import EventComparisonPageService from app.services.event_comparison_ui import EventComparisonPageService
@@ -25,21 +28,59 @@ def _chart_options(dates: tuple[str, ...], series: tuple[dict[str, object], ...]
@ui.page("/event-comparison") @ui.page("/event-comparison")
def event_comparison_page() -> None: def legacy_event_comparison_page(request: Request):
repo = get_workspace_repository()
workspace_id = request.cookies.get(WORKSPACE_COOKIE, "")
if workspace_id and repo.workspace_exists(workspace_id):
return RedirectResponse(url=f"/{workspace_id}/event-comparison", status_code=307)
_render_event_comparison_page()
@ui.page("/{workspace_id}/event-comparison")
def workspace_event_comparison_page(workspace_id: str) -> None:
repo = get_workspace_repository()
if not repo.workspace_exists(workspace_id):
render_workspace_recovery()
return
_render_event_comparison_page(workspace_id=workspace_id)
def _render_event_comparison_page(workspace_id: str | None = None) -> None:
service = EventComparisonPageService() service = EventComparisonPageService()
preset_options = service.preset_options("GLD") preset_options = service.preset_options("GLD")
template_options = service.template_options("GLD") template_options = service.template_options("GLD")
repo = get_workspace_repository()
config = repo.load_portfolio_config(workspace_id) if workspace_id else None
default_preset_slug = str(preset_options[0]["slug"]) if preset_options else None
default_template_slugs = list(preset_options[0]["default_template_slugs"]) if preset_options else []
default_entry_spot = 100.0
if default_preset_slug is not None:
default_preview = service.preview_scenario(
preset_slug=default_preset_slug,
template_slugs=tuple(default_template_slugs),
underlying_units=1.0,
loan_amount=0.0,
margin_call_ltv=0.75,
)
default_entry_spot = default_preview.initial_portfolio.entry_spot
default_units = (
float(config.gold_value or 0.0) / default_entry_spot
if config and config.gold_value is not None and default_entry_spot > 0
else 1000.0
)
default_loan = float(config.loan_amount) if config else 68000.0
default_margin_call_ltv = float(config.margin_threshold) if config else 0.75
preset_select_options = {str(option["slug"]): str(option["label"]) for option in preset_options} preset_select_options = {str(option["slug"]): str(option["label"]) for option in preset_options}
template_select_options = {str(option["slug"]): str(option["label"]) for option in template_options} template_select_options = {str(option["slug"]): str(option["label"]) for option in template_options}
default_preset_slug = str(preset_options[0]["slug"]) if preset_options else None
default_template_slugs = list(preset_options[0]["default_template_slugs"]) if preset_options else []
preset_lookup = {str(option["slug"]): option for option in preset_options} preset_lookup = {str(option["slug"]): option for option in preset_options}
with dashboard_page( with dashboard_page(
"Event Comparison", "Event Comparison",
"Thin BT-003A read-only UI over EventComparisonService with deterministic seeded GLD presets.", "Thin BT-003A read-only UI over EventComparisonService with deterministic seeded GLD presets.",
"event-comparison", "event-comparison",
workspace_id=workspace_id,
): ):
with ui.row().classes("w-full gap-6 max-lg:flex-col"): with ui.row().classes("w-full gap-6 max-lg:flex-col"):
with ui.card().classes( with ui.card().classes(
@@ -49,6 +90,10 @@ def event_comparison_page() -> None:
ui.label( ui.label(
"Preset selection is deterministic and read-only in the sense that runs reuse seeded event windows and existing BT-003 ranking logic." "Preset selection is deterministic and read-only in the sense that runs reuse seeded event windows and existing BT-003 ranking logic."
).classes("text-sm text-slate-500 dark:text-slate-400") ).classes("text-sm text-slate-500 dark:text-slate-400")
if workspace_id:
ui.label("Workspace defaults seed underlying units, loan amount, and margin threshold.").classes(
"text-sm text-slate-500 dark:text-slate-400"
)
preset_select = ui.select( preset_select = ui.select(
preset_select_options, preset_select_options,
value=default_preset_slug, value=default_preset_slug,
@@ -60,12 +105,18 @@ def event_comparison_page() -> None:
label="Strategy templates", label="Strategy templates",
multiple=True, multiple=True,
).classes("w-full") ).classes("w-full")
ui.label("Changing the preset resets strategy templates to that preset's default comparison set.").classes( ui.label(
"text-xs text-slate-500 dark:text-slate-400" "Changing the preset resets strategy templates to that preset's default comparison set."
) ).classes("text-xs text-slate-500 dark:text-slate-400")
units_input = ui.number("Underlying units", value=1000.0, min=0.0001, step=1).classes("w-full") units_input = ui.number("Underlying units", value=default_units, min=0.0001, step=1).classes("w-full")
loan_input = ui.number("Loan amount", value=68000.0, min=0, step=1000).classes("w-full") loan_input = ui.number("Loan amount", value=default_loan, min=0, step=1000).classes("w-full")
ltv_input = ui.number("Margin call LTV", value=0.75, min=0.01, max=0.99, step=0.01).classes("w-full") ltv_input = ui.number(
"Margin call LTV",
value=default_margin_call_ltv,
min=0.01,
max=0.99,
step=0.01,
).classes("w-full")
metadata_label = ui.label("").classes("text-sm text-slate-500 dark:text-slate-400") metadata_label = ui.label("").classes("text-sm text-slate-500 dark:text-slate-400")
scenario_label = ui.label("").classes("text-sm text-slate-500 dark:text-slate-400") scenario_label = ui.label("").classes("text-sm text-slate-500 dark:text-slate-400")
validation_label = ui.label("").classes("text-sm text-rose-600 dark:text-rose-300") validation_label = ui.label("").classes("text-sm text-rose-600 dark:text-rose-300")
@@ -154,7 +205,10 @@ def event_comparison_page() -> None:
"Anchor date", "Anchor date",
preset.anchor_date.isoformat() if preset.anchor_date is not None else "None", preset.anchor_date.isoformat() if preset.anchor_date is not None else "None",
), ),
("Scenario dates used", f"{scenario.start_date.isoformat()}{scenario.end_date.isoformat()}"), (
"Scenario dates used",
f"{scenario.start_date.isoformat()}{scenario.end_date.isoformat()}",
),
("Underlying units", f"{scenario.initial_portfolio.underlying_units:,.0f}"), ("Underlying units", f"{scenario.initial_portfolio.underlying_units:,.0f}"),
("Loan amount", f"${scenario.initial_portfolio.loan_amount:,.0f}"), ("Loan amount", f"${scenario.initial_portfolio.loan_amount:,.0f}"),
("Margin call LTV", f"{scenario.initial_portfolio.margin_call_ltv:.1%}"), ("Margin call LTV", f"{scenario.initial_portfolio.margin_call_ltv:.1%}"),
@@ -219,16 +273,16 @@ def event_comparison_page() -> None:
with ui.card().classes( with ui.card().classes(
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
): ):
ui.label("Portfolio Value Paths").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") ui.label("Portfolio Value Paths").classes(
"text-lg font-semibold text-slate-900 dark:text-slate-100"
)
ui.label( ui.label(
"Baseline series shows the unhedged collateral value path for the same seeded event window." "Baseline series shows the unhedged collateral value path for the same seeded event window."
).classes("text-sm text-slate-500 dark:text-slate-400") ).classes("text-sm text-slate-500 dark:text-slate-400")
ui.echart( ui.echart(
_chart_options( _chart_options(
chart_model.dates, chart_model.dates,
tuple( tuple({"name": item.name, "values": list(item.values)} for item in chart_model.series),
{"name": item.name, "values": list(item.values)} for item in chart_model.series
),
) )
).classes("h-96 w-full") ).classes("h-96 w-full")

View File

@@ -1,11 +1,15 @@
from __future__ import annotations from __future__ import annotations
from fastapi import Request
from fastapi.responses import RedirectResponse
from nicegui import ui from nicegui import ui
from app.models.workspace import WORKSPACE_COOKIE, get_workspace_repository
from app.pages.common import ( from app.pages.common import (
dashboard_page, dashboard_page,
demo_spot_price, demo_spot_price,
portfolio_snapshot, portfolio_snapshot,
render_workspace_recovery,
strategy_catalog, strategy_catalog,
strategy_metrics, strategy_metrics,
) )
@@ -32,6 +36,7 @@ def _cost_benefit_options(metrics: dict) -> dict:
} }
def _waterfall_options(metrics: dict) -> dict: def _waterfall_options(metrics: dict) -> dict:
steps = metrics["waterfall_steps"] steps = metrics["waterfall_steps"]
values: list[dict[str, object]] = [] values: list[dict[str, object]] = []
@@ -53,7 +58,28 @@ def _waterfall_options(metrics: dict) -> dict:
@ui.page("/hedge") @ui.page("/hedge")
def hedge_page() -> None: def legacy_hedge_page(request: Request):
repo = get_workspace_repository()
workspace_id = request.cookies.get(WORKSPACE_COOKIE, "")
if workspace_id and repo.workspace_exists(workspace_id):
return RedirectResponse(url=f"/{workspace_id}/hedge", status_code=307)
_render_hedge_page()
@ui.page("/{workspace_id}/hedge")
def workspace_hedge_page(workspace_id: str) -> None:
repo = get_workspace_repository()
if not repo.workspace_exists(workspace_id):
render_workspace_recovery()
return
_render_hedge_page(workspace_id=workspace_id)
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 = portfolio_snapshot(config)
strategies = strategy_catalog() strategies = strategy_catalog()
strategy_map = {strategy["label"]: strategy["name"] for strategy in strategies} strategy_map = {strategy["label"]: strategy["name"] for strategy in strategies}
selected = {"strategy": strategies[0]["name"], "label": strategies[0]["label"], "scenario_pct": 0} selected = {"strategy": strategies[0]["name"], "label": strategies[0]["label"], "scenario_pct": 0}
@@ -62,6 +88,7 @@ def hedge_page() -> None:
"Hedge Analysis", "Hedge Analysis",
"Compare hedge structures across scenarios, visualize cost-benefit tradeoffs, and inspect net equity impacts.", "Compare hedge structures across scenarios, visualize cost-benefit tradeoffs, and inspect net equity impacts.",
"hedge", "hedge",
workspace_id=workspace_id,
): ):
with ui.row().classes("w-full gap-6 max-lg:flex-col"): with ui.row().classes("w-full gap-6 max-lg:flex-col"):
with ui.card().classes( with ui.card().classes(
@@ -71,9 +98,17 @@ def hedge_page() -> None:
selector = ui.select(strategy_map, value=selected["label"], label="Strategy selector").classes("w-full") 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_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") slider = ui.slider(min=-25, max=25, value=0, step=5).classes("w-full")
ui.label(f"Current spot reference: ${demo_spot_price():,.2f}").classes( ui.label(f"Current spot reference: ${portfolio['spot_price']:,.2f}").classes(
"text-sm text-slate-500 dark:text-slate-400" "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"
)
else:
ui.label(f"Demo spot reference: ${demo_spot_price():,.2f}").classes(
"text-xs text-slate-500 dark:text-slate-400"
)
summary = ui.card().classes( summary = ui.card().classes(
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
@@ -81,22 +116,17 @@ def hedge_page() -> None:
charts_row = ui.row().classes("w-full gap-6 max-lg:flex-col") charts_row = ui.row().classes("w-full gap-6 max-lg:flex-col")
with charts_row: with charts_row:
cost_chart = ui.echart( initial_metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"], portfolio=portfolio)
_cost_benefit_options(strategy_metrics(selected["strategy"], selected["scenario_pct"])) cost_chart = ui.echart(_cost_benefit_options(initial_metrics)).classes(
).classes(
"h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900" "h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900"
) )
waterfall_chart = ui.echart( waterfall_chart = ui.echart(_waterfall_options(initial_metrics)).classes(
_waterfall_options(strategy_metrics(selected["strategy"], selected["scenario_pct"]))
).classes(
"h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900" "h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900"
) )
def render_summary() -> None: def render_summary() -> None:
metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"]) metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"], portfolio=portfolio)
strategy = metrics["strategy"] strategy = metrics["strategy"]
portfolio = portfolio_snapshot()
starting_weight = portfolio["gold_value"] / portfolio["spot_price"]
summary.clear() summary.clear()
with summary: with summary:
ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
@@ -104,7 +134,10 @@ def hedge_page() -> None:
cards = [ cards = [
("Start value", f"${portfolio['gold_value']:,.0f}"), ("Start value", f"${portfolio['gold_value']:,.0f}"),
("Start price", f"${portfolio['spot_price']:,.2f}/oz"), ("Start price", f"${portfolio['spot_price']:,.2f}/oz"),
("Weight", f"{starting_weight:,.0f} oz"), ("Weight", f"{portfolio['gold_units']:,.0f} oz"),
("Loan amount", f"${portfolio['loan_amount']:,.0f}"),
("Margin call LTV", f"{portfolio['margin_call_ltv']:.1%}"),
("Monthly hedge budget", f"${portfolio['hedge_budget']:,.0f}"),
("Scenario spot", f"${metrics['scenario_price']:,.2f}"), ("Scenario spot", f"${metrics['scenario_price']:,.2f}"),
("Hedge cost", f"${strategy['estimated_cost']:,.2f}/oz"), ("Hedge cost", f"${strategy['estimated_cost']:,.2f}/oz"),
("Unhedged equity", f"${metrics['unhedged_equity']:,.0f}"), ("Unhedged equity", f"${metrics['unhedged_equity']:,.0f}"),

View File

@@ -54,6 +54,32 @@ def test_backtest_non_default_template_slug_runs_successfully() -> None:
assert result.run_result.template_results[0].template_slug == non_default_slug assert result.run_result.template_results[0].template_slug == non_default_slug
def test_backtest_page_service_keeps_fixture_history_while_using_caller_portfolio_inputs() -> None:
service = BacktestPageService()
result = service.run_read_only_scenario(
symbol="GLD",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 8),
template_slug="protective-put-atm-12m",
underlying_units=9680.0,
loan_amount=222000.0,
margin_call_ltv=0.80,
)
assert result.entry_spot == 100.0
assert result.scenario.initial_portfolio.underlying_units == 9680.0
assert result.scenario.initial_portfolio.loan_amount == 222000.0
assert result.scenario.initial_portfolio.margin_call_ltv == 0.80
assert [point.date.isoformat() for point in result.run_result.template_results[0].daily_path] == [
"2024-01-02",
"2024-01-03",
"2024-01-04",
"2024-01-05",
"2024-01-08",
]
@pytest.mark.parametrize( @pytest.mark.parametrize(
("kwargs", "message"), ("kwargs", "message"),
[ [

View File

@@ -36,6 +36,7 @@ def test_homepage_and_options_page_render() -> None:
expect(page.locator("text=Alert Status").first).to_be_visible(timeout=15000) expect(page.locator("text=Alert Status").first).to_be_visible(timeout=15000)
page.goto(f"{BASE_URL}/backtests", wait_until="networkidle", timeout=30000) page.goto(f"{BASE_URL}/backtests", wait_until="networkidle", timeout=30000)
page.wait_for_url(f"{workspace_url}/backtests", timeout=15000)
expect(page.locator("text=Backtests").first).to_be_visible(timeout=15000) expect(page.locator("text=Backtests").first).to_be_visible(timeout=15000)
expect(page.locator("text=Scenario Form").first).to_be_visible(timeout=15000) expect(page.locator("text=Scenario Form").first).to_be_visible(timeout=15000)
expect(page.locator("text=Scenario Summary").first).to_be_visible(timeout=15000) expect(page.locator("text=Scenario Summary").first).to_be_visible(timeout=15000)
@@ -58,6 +59,7 @@ def test_homepage_and_options_page_render() -> None:
assert "Server error" not in rerun_text assert "Server error" not in rerun_text
page.goto(f"{BASE_URL}/event-comparison", wait_until="networkidle", timeout=30000) page.goto(f"{BASE_URL}/event-comparison", wait_until="networkidle", timeout=30000)
page.wait_for_url(f"{workspace_url}/event-comparison", timeout=15000)
expect(page.locator("text=Event Comparison").first).to_be_visible(timeout=15000) expect(page.locator("text=Event Comparison").first).to_be_visible(timeout=15000)
expect(page.locator("text=Comparison Form").first).to_be_visible(timeout=15000) expect(page.locator("text=Comparison Form").first).to_be_visible(timeout=15000)
expect(page.locator("text=Ranked Results").first).to_be_visible(timeout=15000) expect(page.locator("text=Ranked Results").first).to_be_visible(timeout=15000)
@@ -102,6 +104,8 @@ def test_homepage_and_options_page_render() -> None:
page.get_by_text("Gold weight + entry price", exact=True).click() page.get_by_text("Gold weight + entry price", exact=True).click()
page.get_by_label("Entry price ($/oz)").fill("4400") page.get_by_label("Entry price ($/oz)").fill("4400")
page.get_by_label("Gold weight (oz)").fill("220") page.get_by_label("Gold weight (oz)").fill("220")
page.get_by_label("Loan amount ($)").fill("222000")
page.get_by_label("Margin call LTV threshold").fill("0.8")
budget_input = page.get_by_label("Monthly hedge budget ($)") budget_input = page.get_by_label("Monthly hedge budget ($)")
budget_input.fill("12345") budget_input.fill("12345")
page.get_by_role("button", name="Save settings").click() page.get_by_role("button", name="Save settings").click()
@@ -109,11 +113,30 @@ def test_homepage_and_options_page_render() -> None:
page.reload(wait_until="domcontentloaded", timeout=30000) page.reload(wait_until="domcontentloaded", timeout=30000)
expect(page.get_by_label("Monthly hedge budget ($)")).to_have_value("12345") expect(page.get_by_label("Monthly hedge budget ($)")).to_have_value("12345")
expect(page.get_by_label("Entry price ($/oz)")).to_have_value("4400") expect(page.get_by_label("Entry price ($/oz)")).to_have_value("4400")
expect(page.get_by_label("Loan amount ($)")).to_have_value("222000")
expect(page.get_by_label("Margin call LTV threshold")).to_have_value("0.8")
settings_text = page.locator("body").inner_text(timeout=15000) settings_text = page.locator("body").inner_text(timeout=15000)
assert "RuntimeError" not in settings_text assert "RuntimeError" not in settings_text
assert "Server error" not in settings_text assert "Server error" not in settings_text
page.screenshot(path=str(ARTIFACTS / "settings.png"), full_page=True) page.screenshot(path=str(ARTIFACTS / "settings.png"), full_page=True)
page.goto(f"{workspace_url}/backtests", wait_until="networkidle", timeout=30000)
expect(page.get_by_label("Underlying units")).to_have_value("9680")
expect(page.get_by_label("Loan amount")).to_have_value("222000")
expect(page.get_by_label("Margin call LTV")).to_have_value("0.8")
backtests_workspace_text = page.locator("body").inner_text(timeout=15000)
assert "Scenario Summary" in backtests_workspace_text
assert "$968,000" in backtests_workspace_text
page.goto(f"{workspace_url}/event-comparison", wait_until="networkidle", timeout=30000)
expect(page.get_by_label("Underlying units")).to_have_value("9680")
expect(page.get_by_label("Loan amount")).to_have_value("222000")
expect(page.get_by_label("Margin call LTV")).to_have_value("0.8")
event_workspace_text = page.locator("body").inner_text(timeout=15000)
assert "$222,000" in event_workspace_text
assert "9,680" in event_workspace_text
assert "80.0%" in event_workspace_text
page.goto(workspace_url, wait_until="domcontentloaded", timeout=30000) page.goto(workspace_url, wait_until="domcontentloaded", timeout=30000)
overview_text = page.locator("body").inner_text(timeout=15000) overview_text = page.locator("body").inner_text(timeout=15000)
assert "Hedge Analysis" in overview_text assert "Hedge Analysis" in overview_text
@@ -121,9 +144,14 @@ def test_homepage_and_options_page_render() -> None:
assert "Backtests" in overview_text assert "Backtests" in overview_text
assert "Event Comparison" in overview_text assert "Event Comparison" in overview_text
assert "Live quote source: configured entry price fallback" in overview_text assert "Live quote source: configured entry price fallback" in overview_text
assert "$878.79" in overview_text assert "$1,261.36" in overview_text
assert "$968,000.00" in overview_text assert "$968,000.00" in overview_text
assert "$823,000.00" in overview_text assert "$746,000.00" in overview_text
expect(page.get_by_role("link", name="Hedge Analysis")).to_have_attribute("href", f"/{workspace_id}/hedge")
expect(page.get_by_role("link", name="Backtests")).to_have_attribute("href", f"/{workspace_id}/backtests")
expect(page.get_by_role("link", name="Event Comparison")).to_have_attribute(
"href", f"/{workspace_id}/event-comparison"
)
second_context = browser.new_context(viewport={"width": 1440, "height": 1000}) second_context = browser.new_context(viewport={"width": 1440, "height": 1000})
second_page = second_context.new_page() second_page = second_context.new_page()
@@ -139,6 +167,7 @@ def test_homepage_and_options_page_render() -> None:
second_context.close() second_context.close()
page.goto(f"{BASE_URL}/hedge", wait_until="domcontentloaded", timeout=30000) page.goto(f"{BASE_URL}/hedge", wait_until="domcontentloaded", timeout=30000)
page.wait_for_url(f"{workspace_url}/hedge", timeout=15000)
expect(page.locator("text=Hedge Analysis").first).to_be_visible(timeout=15000) expect(page.locator("text=Hedge Analysis").first).to_be_visible(timeout=15000)
expect(page.locator("text=Strategy selector").first).to_be_visible(timeout=15000) expect(page.locator("text=Strategy selector").first).to_be_visible(timeout=15000)
expect(page.locator("text=Strategy Controls").first).to_be_visible(timeout=15000) expect(page.locator("text=Strategy Controls").first).to_be_visible(timeout=15000)
@@ -156,13 +185,16 @@ def test_homepage_and_options_page_render() -> None:
assert "Start value" in hedge_text assert "Start value" in hedge_text
assert "Start price" in hedge_text assert "Start price" in hedge_text
assert "Weight" in hedge_text assert "Weight" in hedge_text
assert "$215,000" in hedge_text assert "Loan amount" in hedge_text
assert "$215.00/oz" in hedge_text assert "Monthly hedge budget" in hedge_text
assert "1,000 oz" in hedge_text assert "$968,000" in hedge_text
assert "$4,400.00/oz" in hedge_text
assert "220 oz" in hedge_text
assert "$222,000" in hedge_text
assert "80.0%" in hedge_text
assert "$12,345" in hedge_text
assert "Unhedged equity" in hedge_text assert "Unhedged equity" in hedge_text
assert "Hedged equity" in hedge_text assert "Hedged equity" in hedge_text
assert "$27,000" in hedge_text
assert "$58,750" in hedge_text
page.screenshot(path=str(ARTIFACTS / "hedge.png"), full_page=True) page.screenshot(path=str(ARTIFACTS / "hedge.png"), full_page=True)
browser.close() browser.close()

View File

@@ -66,6 +66,25 @@ def test_event_comparison_page_service_exposes_seeded_preset_options() -> None:
) )
def test_event_comparison_page_service_keeps_fixture_window_while_using_caller_portfolio_inputs() -> None:
service = EventComparisonPageService()
report = service.run_read_only_comparison(
preset_slug="gld-jan-2024-selloff",
template_slugs=("protective-put-atm-12m",),
underlying_units=9680.0,
loan_amount=222000.0,
margin_call_ltv=0.80,
)
assert report.scenario.start_date.isoformat() == "2024-01-02"
assert report.scenario.end_date.isoformat() == "2024-01-08"
assert report.scenario.initial_portfolio.entry_spot == 100.0
assert report.scenario.initial_portfolio.underlying_units == 9680.0
assert report.scenario.initial_portfolio.loan_amount == 222000.0
assert report.scenario.initial_portfolio.margin_call_ltv == 0.80
def test_event_comparison_page_service_resets_template_selection_to_preset_defaults() -> None: def test_event_comparison_page_service_resets_template_selection_to_preset_defaults() -> None:
service = EventComparisonPageService() service = EventComparisonPageService()

View File

@@ -98,3 +98,64 @@ def test_workspace_settings_round_trip_uses_workspace_storage(tmp_path, monkeypa
assert response.status_code == 200 assert response.status_code == 200
assert "Settings" in response.text assert "Settings" in response.text
assert "9,999" in response.text or "9999" in response.text assert "9,999" in response.text or "9999" in response.text
def test_workspace_pages_use_workspace_scoped_navigation_links(tmp_path, monkeypatch) -> None:
repo = _install_workspace_repo(tmp_path, monkeypatch)
workspace_id = str(uuid4())
repo.create_workspace(workspace_id)
with TestClient(app) as client:
response = client.get(f"/{workspace_id}")
assert response.status_code == 200
assert f"/{workspace_id}/hedge" in response.text
assert f"/{workspace_id}/backtests" in response.text
assert f"/{workspace_id}/event-comparison" in response.text
assert f"/{workspace_id}/settings" in response.text
def test_workspace_routes_seed_page_defaults_from_workspace_portfolio_config(tmp_path, monkeypatch) -> None:
repo = _install_workspace_repo(tmp_path, monkeypatch)
workspace_id = str(uuid4())
config = repo.create_workspace(workspace_id)
config.entry_basis_mode = "weight"
config.entry_price = 4_400.0
config.gold_ounces = 220.0
config.gold_value = 968_000.0
config.loan_amount = 222_000.0
config.margin_threshold = 0.80
config.monthly_budget = 12_345.0
repo.save_portfolio_config(workspace_id, config)
with TestClient(app) as client:
hedge_response = client.get(f"/{workspace_id}/hedge")
backtests_response = client.get(f"/{workspace_id}/backtests")
event_response = client.get(f"/{workspace_id}/event-comparison")
redirect_response = client.get("/backtests", cookies={"workspace_id": workspace_id}, follow_redirects=False)
assert hedge_response.status_code == 200
assert "Monthly hedge budget" in hedge_response.text
assert "12,345" in hedge_response.text or "12345" in hedge_response.text
assert "968,000" in hedge_response.text or "968000" in hedge_response.text
assert "4,400.00/oz" in hedge_response.text or "4400.00/oz" in hedge_response.text
assert "220 oz" in hedge_response.text
assert "222,000" in hedge_response.text or "222000" in hedge_response.text
assert "80.0%" in hedge_response.text
assert backtests_response.status_code == 200
assert "Workspace defaults seed underlying units, loan amount, and margin threshold." in backtests_response.text
assert "9680" in backtests_response.text or "9,680" in backtests_response.text
assert "222000" in backtests_response.text or "222,000" in backtests_response.text
assert "0.8" in backtests_response.text or "80.0%" in backtests_response.text
assert event_response.status_code == 200
assert "Workspace defaults seed underlying units, loan amount, and margin threshold." in event_response.text
assert "Underlying units" in event_response.text
assert "Loan amount" in event_response.text
assert "222,000" in event_response.text or "222000" in event_response.text
assert "9,680" in event_response.text or "9680" in event_response.text
assert "80.0%" in event_response.text
assert redirect_response.status_code in {302, 303, 307}
assert redirect_response.headers["location"] == f"/{workspace_id}/backtests"