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 fastapi import Request
from fastapi.responses import RedirectResponse
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
@@ -47,16 +50,45 @@ def _chart_options(result: BacktestPageRunResult) -> dict:
@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()
template_options = service.template_options("GLD")
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
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(
"Backtests",
"Run a thin read-only historical scenario over the BT-001 synthetic engine and inspect the daily path.",
"backtests",
workspace_id=workspace_id,
):
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
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(
"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")
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")
template_select = ui.select(select_options, value=default_template_slug, label="Template").classes(
"w-full"
)
units_input = ui.number("Underlying units", value=1000.0, min=0.0001, step=1).classes("w-full")
loan_input = ui.number("Loan amount", value=68000.0, 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")
units_input = ui.number("Underlying units", value=default_units, min=0.0001, step=1).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=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(
"text-sm text-slate-500 dark:text-slate-400"
)

View File

@@ -6,6 +6,7 @@ from typing import Any
from nicegui import ui
from app.models.portfolio import PortfolioConfig
from app.services.strategy_templates import StrategyTemplateService
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 [
("overview", f"/{workspace_id}", "Overview"),
("hedge", "/hedge", "Hedge Analysis"),
("hedge", f"/{workspace_id}/hedge", "Hedge Analysis"),
("options", "/options", "Options Chain"),
("backtests", "/backtests", "Backtests"),
("event-comparison", "/event-comparison", "Event Comparison"),
("backtests", f"/{workspace_id}/backtests", "Backtests"),
("event-comparison", f"/{workspace_id}/event-comparison", "Event Comparison"),
("settings", f"/{workspace_id}/settings", "Settings"),
]
@@ -35,22 +36,33 @@ def demo_spot_price() -> float:
return 215.0
def portfolio_snapshot() -> dict[str, float]:
gold_units = 1_000.0
spot = demo_spot_price()
gold_value = gold_units * spot
loan_amount = 145_000.0
margin_call_ltv = 0.75
def portfolio_snapshot(config: PortfolioConfig | None = None) -> dict[str, float]:
if config is None:
gold_units = 1_000.0
spot = demo_spot_price()
gold_value = gold_units * spot
loan_amount = 145_000.0
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 {
"gold_value": gold_value,
"loan_amount": loan_amount,
"ltv_ratio": loan_amount / gold_value,
"net_equity": gold_value - loan_amount,
"spot_price": spot,
"gold_units": gold_units,
"margin_call_ltv": margin_call_ltv,
"margin_call_price": loan_amount / (margin_call_ltv * gold_units),
"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
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(
(item for item in strategy_catalog() if item["name"] == strategy_name),
strategy_catalog()[0],
)
portfolio = portfolio_snapshot()
portfolio = portfolio or portfolio_snapshot()
spot = float(portfolio["spot_price"])
underlying_units = portfolio["gold_value"] / spot
loan_amount = float(portfolio["loan_amount"])
@@ -209,6 +226,21 @@ def dashboard_page(title: str, subtitle: str, current: str, workspace_id: str |
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:
return {
"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 fastapi import Request
from fastapi.responses import RedirectResponse
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
@@ -25,21 +28,59 @@ def _chart_options(dates: tuple[str, ...], series: tuple[dict[str, object], ...]
@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()
preset_options = service.preset_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}
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}
with dashboard_page(
"Event Comparison",
"Thin BT-003A read-only UI over EventComparisonService with deterministic seeded GLD presets.",
"event-comparison",
workspace_id=workspace_id,
):
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
with ui.card().classes(
@@ -49,6 +90,10 @@ def event_comparison_page() -> None:
ui.label(
"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")
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_options,
value=default_preset_slug,
@@ -60,12 +105,18 @@ def event_comparison_page() -> None:
label="Strategy templates",
multiple=True,
).classes("w-full")
ui.label("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")
loan_input = ui.number("Loan amount", value=68000.0, 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")
ui.label(
"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=default_units, min=0.0001, step=1).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=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")
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")
@@ -154,7 +205,10 @@ def event_comparison_page() -> None:
"Anchor date",
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}"),
("Loan amount", f"${scenario.initial_portfolio.loan_amount:,.0f}"),
("Margin call LTV", f"{scenario.initial_portfolio.margin_call_ltv:.1%}"),
@@ -219,16 +273,16 @@ def event_comparison_page() -> 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("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(
"Baseline series shows the unhedged collateral value path for the same seeded event window."
).classes("text-sm text-slate-500 dark:text-slate-400")
ui.echart(
_chart_options(
chart_model.dates,
tuple(
{"name": item.name, "values": list(item.values)} for item in chart_model.series
),
tuple({"name": item.name, "values": list(item.values)} for item in chart_model.series),
)
).classes("h-96 w-full")

View File

@@ -1,11 +1,15 @@
from __future__ import annotations
from fastapi import Request
from fastapi.responses import RedirectResponse
from nicegui import ui
from app.models.workspace import WORKSPACE_COOKIE, get_workspace_repository
from app.pages.common import (
dashboard_page,
demo_spot_price,
portfolio_snapshot,
render_workspace_recovery,
strategy_catalog,
strategy_metrics,
)
@@ -32,6 +36,7 @@ def _cost_benefit_options(metrics: dict) -> dict:
}
def _waterfall_options(metrics: dict) -> dict:
steps = metrics["waterfall_steps"]
values: list[dict[str, object]] = []
@@ -53,7 +58,28 @@ def _waterfall_options(metrics: dict) -> dict:
@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()
strategy_map = {strategy["label"]: strategy["name"] for strategy in strategies}
selected = {"strategy": strategies[0]["name"], "label": strategies[0]["label"], "scenario_pct": 0}
@@ -62,6 +88,7 @@ def hedge_page() -> None:
"Hedge Analysis",
"Compare hedge structures across scenarios, visualize cost-benefit tradeoffs, and inspect net equity impacts.",
"hedge",
workspace_id=workspace_id,
):
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
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")
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: ${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"
)
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(
"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")
with charts_row:
cost_chart = ui.echart(
_cost_benefit_options(strategy_metrics(selected["strategy"], selected["scenario_pct"]))
).classes(
initial_metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"], portfolio=portfolio)
cost_chart = ui.echart(_cost_benefit_options(initial_metrics)).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"
)
waterfall_chart = ui.echart(
_waterfall_options(strategy_metrics(selected["strategy"], selected["scenario_pct"]))
).classes(
waterfall_chart = ui.echart(_waterfall_options(initial_metrics)).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"
)
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"]
portfolio = portfolio_snapshot()
starting_weight = portfolio["gold_value"] / portfolio["spot_price"]
summary.clear()
with summary:
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 = [
("Start value", f"${portfolio['gold_value']:,.0f}"),
("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}"),
("Hedge cost", f"${strategy['estimated_cost']:,.2f}/oz"),
("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
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(
("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)
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=Scenario Form").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
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=Comparison Form").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_label("Entry price ($/oz)").fill("4400")
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.fill("12345")
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)
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("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)
assert "RuntimeError" not in settings_text
assert "Server error" not in settings_text
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)
overview_text = page.locator("body").inner_text(timeout=15000)
assert "Hedge Analysis" in overview_text
@@ -121,9 +144,14 @@ def test_homepage_and_options_page_render() -> None:
assert "Backtests" in overview_text
assert "Event Comparison" 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 "$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_page = second_context.new_page()
@@ -139,6 +167,7 @@ def test_homepage_and_options_page_render() -> None:
second_context.close()
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=Strategy selector").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 price" in hedge_text
assert "Weight" in hedge_text
assert "$215,000" in hedge_text
assert "$215.00/oz" in hedge_text
assert "1,000 oz" in hedge_text
assert "Loan amount" in hedge_text
assert "Monthly hedge budget" 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 "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)
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:
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 "Settings" 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"