fix(workspaces): seed new defaults from live quote

This commit is contained in:
Bu5hm4nn
2026-03-25 19:48:58 +01:00
parent 782e8f692e
commit aae67dfd9b
5 changed files with 77 additions and 17 deletions

View File

@@ -159,13 +159,11 @@ def strategy_benefit_per_unit(strategy: Mapping[str, Any], *, current_spot: floa
return round(float(benefit), 2)
def resolve_portfolio_spot_from_quote(
config: PortfolioConfig,
def resolve_collateral_spot_from_quote(
quote: Mapping[str, object],
*,
fallback_symbol: str | None = None,
) -> tuple[float, str, str]:
configured_price = float(config.entry_price or 0.0)
) -> tuple[float, str, str] | None:
quote_price = _safe_quote_price(quote.get("price"))
quote_source = str(quote.get("source", "unknown"))
quote_updated_at = str(quote.get("updated_at", ""))
@@ -173,12 +171,12 @@ def resolve_portfolio_spot_from_quote(
quote_unit = str(quote.get("quote_unit", "")).strip().lower()
if quote_price <= 0 or not quote_symbol or quote_unit != "share":
return configured_price, "configured_entry_price", ""
return None
try:
metadata = instrument_metadata(quote_symbol)
except ValueError:
return configured_price, "configured_entry_price", ""
return None
converted_spot = metadata.price_per_weight_from_asset_price(
PricePerAsset(amount=decimal_from_float(quote_price), currency=BaseCurrency.USD, symbol=quote_symbol),
@@ -187,6 +185,19 @@ def resolve_portfolio_spot_from_quote(
return _decimal_to_float(converted_spot.amount), quote_source, quote_updated_at
def resolve_portfolio_spot_from_quote(
config: PortfolioConfig,
quote: Mapping[str, object],
*,
fallback_symbol: str | None = None,
) -> tuple[float, str, str]:
resolved = resolve_collateral_spot_from_quote(quote, fallback_symbol=fallback_symbol)
if resolved is not None:
return resolved
configured_price = float(config.entry_price or 0.0)
return configured_price, "configured_entry_price", ""
def portfolio_snapshot_from_config(
config: PortfolioConfig | None = None,
*,

View File

@@ -17,11 +17,13 @@ from nicegui import ui # type: ignore[attr-defined]
import app.pages # noqa: F401
from app.api.routes import router as api_router
from app.domain.portfolio_math import resolve_collateral_spot_from_quote
from app.models.portfolio import build_default_portfolio_config
from app.models.workspace import WORKSPACE_COOKIE, get_workspace_repository
from app.services import turnstile as turnstile_service
from app.services.cache import CacheService
from app.services.data_service import DataService
from app.services.runtime import set_data_service
from app.services.runtime import get_data_service, set_data_service
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"))
logger = logging.getLogger(__name__)
@@ -169,7 +171,18 @@ async def bootstrap_workspace(
):
return RedirectResponse(url="/?captcha_error=1", status_code=303)
workspace_id = get_workspace_repository().create_workspace_id()
repo = get_workspace_repository()
config = build_default_portfolio_config()
try:
data_service = get_data_service()
quote = await data_service.get_quote(data_service.default_symbol)
resolved_spot = resolve_collateral_spot_from_quote(quote, fallback_symbol=data_service.default_symbol)
if resolved_spot is not None:
config = build_default_portfolio_config(entry_price=resolved_spot[0])
except Exception as exc:
logger.warning("Falling back to static default workspace seed: %s", exc)
workspace_id = repo.create_workspace_id(config=config)
response = RedirectResponse(url=f"/{workspace_id}", status_code=303)
response.set_cookie(
key=WORKSPACE_COOKIE,

View File

@@ -16,6 +16,17 @@ _DEFAULT_GOLD_OUNCES = 100.0
_LEGACY_DEFAULT_GOLD_OUNCES = 1_000.0
def build_default_portfolio_config(*, entry_price: float | None = None) -> "PortfolioConfig":
resolved_entry_price = float(entry_price) if entry_price is not None else _DEFAULT_ENTRY_PRICE
gold_value = resolved_entry_price * _DEFAULT_GOLD_OUNCES
return PortfolioConfig(
gold_value=gold_value,
entry_price=resolved_entry_price,
gold_ounces=_DEFAULT_GOLD_OUNCES,
entry_basis_mode="value_price",
)
@dataclass(frozen=True)
class LombardPortfolio:
"""Lombard loan portfolio backed by physical gold."""
@@ -122,8 +133,9 @@ class PortfolioConfig:
raise ValueError("Gold weight must be positive")
if self.gold_value is None and self.gold_ounces is None:
self.gold_value = _DEFAULT_GOLD_VALUE
self.gold_ounces = self.gold_value / self.entry_price
default = build_default_portfolio_config(entry_price=self.entry_price)
self.gold_value = default.gold_value
self.gold_ounces = default.gold_ounces
return
if self.gold_value is None and self.gold_ounces is not None:

View File

@@ -4,7 +4,7 @@ import re
from pathlib import Path
from uuid import uuid4
from app.models.portfolio import PortfolioConfig, PortfolioRepository
from app.models.portfolio import PortfolioConfig, PortfolioRepository, build_default_portfolio_config
WORKSPACE_COOKIE = "workspace_id"
_WORKSPACE_ID_RE = re.compile(
@@ -35,17 +35,22 @@ class WorkspaceRepository:
return False
return True
def create_workspace(self, workspace_id: str | None = None) -> PortfolioConfig:
def create_workspace(
self,
workspace_id: str | None = None,
*,
config: PortfolioConfig | None = None,
) -> PortfolioConfig:
resolved_workspace_id = workspace_id or str(uuid4())
if not self.is_valid_workspace_id(resolved_workspace_id):
raise ValueError("workspace_id must be a UUID4 string")
config = PortfolioConfig()
self.save_portfolio_config(resolved_workspace_id, config)
return config
created_config = config or build_default_portfolio_config()
self.save_portfolio_config(resolved_workspace_id, created_config)
return created_config
def create_workspace_id(self) -> str:
def create_workspace_id(self, *, config: PortfolioConfig | None = None) -> str:
workspace_id = str(uuid4())
self.create_workspace(workspace_id)
self.create_workspace(workspace_id, config=config)
return workspace_id
def load_portfolio_config(self, workspace_id: str) -> PortfolioConfig: