fix: restore workspace nav and correct overview spot fallback
This commit is contained in:
@@ -23,6 +23,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"),
|
||||||
|
("options", "/options", "Options Chain"),
|
||||||
|
("backtests", "/backtests", "Backtests"),
|
||||||
|
("event-comparison", "/event-comparison", "Event Comparison"),
|
||||||
("settings", f"/{workspace_id}/settings", "Settings"),
|
("settings", f"/{workspace_id}/settings", "Settings"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,22 @@ from app.services.runtime import get_data_service
|
|||||||
_DEFAULT_CASH_BUFFER = 18_500.0
|
_DEFAULT_CASH_BUFFER = 18_500.0
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_overview_spot(config, quote: dict[str, object]) -> tuple[float, str, str]:
|
||||||
|
configured_price = float(config.entry_price or 0.0)
|
||||||
|
quote_price = float(quote.get("price", configured_price or 0.0) or 0.0)
|
||||||
|
quote_source = str(quote.get("source", "unknown"))
|
||||||
|
quote_updated_at = str(quote.get("updated_at", ""))
|
||||||
|
|
||||||
|
if configured_price > 0 and quote_price > 0:
|
||||||
|
ratio = max(configured_price / quote_price, quote_price / configured_price)
|
||||||
|
if ratio > 1.5:
|
||||||
|
return configured_price, "configured_entry_price", ""
|
||||||
|
|
||||||
|
if quote_price > 0:
|
||||||
|
return quote_price, quote_source, quote_updated_at
|
||||||
|
return configured_price, "configured_entry_price", ""
|
||||||
|
|
||||||
|
|
||||||
def _format_timestamp(value: str | None) -> str:
|
def _format_timestamp(value: str | None) -> str:
|
||||||
if not value:
|
if not value:
|
||||||
return "Unavailable"
|
return "Unavailable"
|
||||||
@@ -96,16 +112,20 @@ async def overview_page(workspace_id: str) -> None:
|
|||||||
data_service = get_data_service()
|
data_service = get_data_service()
|
||||||
symbol = data_service.default_symbol
|
symbol = data_service.default_symbol
|
||||||
quote = await data_service.get_quote(symbol)
|
quote = await data_service.get_quote(symbol)
|
||||||
|
overview_spot_price, overview_source, overview_updated_at = _resolve_overview_spot(config, quote)
|
||||||
portfolio = build_portfolio_alert_context(
|
portfolio = build_portfolio_alert_context(
|
||||||
config,
|
config,
|
||||||
spot_price=float(quote.get("price", float(config.entry_price or 0.0))),
|
spot_price=overview_spot_price,
|
||||||
source=str(quote.get("source", "unknown")),
|
source=overview_source,
|
||||||
updated_at=str(quote.get("updated_at", "")),
|
updated_at=overview_updated_at,
|
||||||
)
|
)
|
||||||
configured_gold_value = float(config.gold_value or 0.0)
|
configured_gold_value = float(config.gold_value or 0.0)
|
||||||
portfolio["cash_buffer"] = max(float(portfolio["gold_value"]) - configured_gold_value, 0.0) + _DEFAULT_CASH_BUFFER
|
portfolio["cash_buffer"] = max(float(portfolio["gold_value"]) - configured_gold_value, 0.0) + _DEFAULT_CASH_BUFFER
|
||||||
portfolio["hedge_budget"] = float(config.monthly_budget)
|
portfolio["hedge_budget"] = float(config.monthly_budget)
|
||||||
alert_status = AlertService().evaluate(config, portfolio)
|
alert_status = AlertService().evaluate(config, portfolio)
|
||||||
|
if portfolio["quote_source"] == "configured_entry_price":
|
||||||
|
quote_status = "Live quote source: configured entry price fallback · Last updated Unavailable"
|
||||||
|
else:
|
||||||
quote_status = (
|
quote_status = (
|
||||||
f"Live quote source: {portfolio['quote_source']} · "
|
f"Live quote source: {portfolio['quote_source']} · "
|
||||||
f"Last updated {_format_timestamp(str(portfolio['quote_updated_at']))}"
|
f"Last updated {_format_timestamp(str(portfolio['quote_updated_at']))}"
|
||||||
|
|||||||
@@ -97,17 +97,34 @@ def test_homepage_and_options_page_render() -> None:
|
|||||||
expect(page.locator("text=Settings").first).to_be_visible(timeout=15000)
|
expect(page.locator("text=Settings").first).to_be_visible(timeout=15000)
|
||||||
expect(page.locator("text=Collateral entry basis").first).to_be_visible(timeout=15000)
|
expect(page.locator("text=Collateral entry basis").first).to_be_visible(timeout=15000)
|
||||||
expect(page.locator("text=Entry price ($/oz)").first).to_be_visible(timeout=15000)
|
expect(page.locator("text=Entry price ($/oz)").first).to_be_visible(timeout=15000)
|
||||||
|
|
||||||
|
page.get_by_label("Collateral entry basis").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("Gold weight (oz)").fill("220")
|
||||||
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()
|
||||||
expect(page.locator("text=Settings saved successfully").first).to_be_visible(timeout=15000)
|
expect(page.locator("text=Settings saved successfully").first).to_be_visible(timeout=15000)
|
||||||
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")
|
||||||
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(workspace_url, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
overview_text = page.locator("body").inner_text(timeout=15000)
|
||||||
|
assert "Hedge Analysis" in overview_text
|
||||||
|
assert "Options Chain" in overview_text
|
||||||
|
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 "$968,000.00" in overview_text
|
||||||
|
assert "$823,000.00" in overview_text
|
||||||
|
|
||||||
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()
|
||||||
second_page.goto(BASE_URL, wait_until="domcontentloaded", timeout=30000)
|
second_page.goto(BASE_URL, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
|||||||
26
tests/test_overview_workspace.py
Normal file
26
tests/test_overview_workspace.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.models.portfolio import PortfolioConfig
|
||||||
|
from app.pages.overview import _resolve_overview_spot
|
||||||
|
from app.services.alerts import build_portfolio_alert_context
|
||||||
|
|
||||||
|
|
||||||
|
def test_overview_falls_back_to_configured_entry_price_when_live_quote_units_do_not_match() -> None:
|
||||||
|
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
||||||
|
|
||||||
|
spot_price, source, updated_at = _resolve_overview_spot(
|
||||||
|
config,
|
||||||
|
{"price": 404.19, "source": "yfinance", "updated_at": "2026-03-24T00:00:00+00:00"},
|
||||||
|
)
|
||||||
|
portfolio = build_portfolio_alert_context(
|
||||||
|
config,
|
||||||
|
spot_price=spot_price,
|
||||||
|
source=source,
|
||||||
|
updated_at=updated_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert spot_price == 4400.0
|
||||||
|
assert source == "configured_entry_price"
|
||||||
|
assert portfolio["gold_value"] == 968000.0
|
||||||
|
assert portfolio["net_equity"] == 823000.0
|
||||||
|
assert round(float(portfolio["margin_call_price"]), 2) == 878.79
|
||||||
Reference in New Issue
Block a user