fix(workspaces): seed new defaults from live quote
This commit is contained in:
@@ -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,
|
||||
*,
|
||||
|
||||
17
app/main.py
17
app/main.py
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user