feat(PORT-004C): seed workspace routes from portfolio settings
This commit is contained in:
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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}"),
|
||||||
|
|||||||
@@ -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"),
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user