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 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"
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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}"),
|
||||
|
||||
Reference in New Issue
Block a user