79 lines
2.9 KiB
Python
79 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from decimal import Decimal
|
|
from typing import Protocol
|
|
|
|
from app.domain.units import decimal_from_float
|
|
from app.models.portfolio import PortfolioConfig
|
|
|
|
|
|
class _SaveStatusConfig(Protocol):
|
|
entry_basis_mode: str
|
|
gold_value: float | None
|
|
entry_price: float | None
|
|
gold_ounces: float | None
|
|
current_ltv: float
|
|
margin_call_price: object
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SaveStatusSnapshot:
|
|
entry_basis_mode: str
|
|
gold_value: Decimal
|
|
entry_price: Decimal
|
|
gold_ounces: Decimal
|
|
current_ltv: Decimal
|
|
margin_call_price: Decimal
|
|
|
|
|
|
def _decimal_from_boundary_value(value: object, *, field_name: str) -> Decimal:
|
|
if value is None:
|
|
raise ValueError(f"{field_name} must be present")
|
|
if isinstance(value, bool):
|
|
raise TypeError(f"{field_name} must be numeric, got bool")
|
|
if isinstance(value, int):
|
|
parsed = float(value)
|
|
elif isinstance(value, float):
|
|
parsed = value
|
|
elif isinstance(value, str):
|
|
stripped = value.strip()
|
|
if not stripped:
|
|
raise ValueError(f"{field_name} must be present")
|
|
try:
|
|
parsed = float(stripped)
|
|
except ValueError as exc:
|
|
raise ValueError(f"{field_name} must be numeric, got {value!r}") from exc
|
|
else:
|
|
raise TypeError(f"{field_name} must be numeric, got {type(value)!r}")
|
|
return decimal_from_float(float(parsed))
|
|
|
|
|
|
def _normalize_save_status_snapshot(config: PortfolioConfig | _SaveStatusConfig) -> SaveStatusSnapshot:
|
|
margin_call_price = config.margin_call_price
|
|
resolved_margin_call_price = margin_call_price() if callable(margin_call_price) else margin_call_price
|
|
return SaveStatusSnapshot(
|
|
entry_basis_mode=config.entry_basis_mode,
|
|
gold_value=_decimal_from_boundary_value(config.gold_value, field_name="config.gold_value"),
|
|
entry_price=_decimal_from_boundary_value(config.entry_price, field_name="config.entry_price"),
|
|
gold_ounces=_decimal_from_boundary_value(config.gold_ounces, field_name="config.gold_ounces"),
|
|
current_ltv=_decimal_from_boundary_value(config.current_ltv, field_name="config.current_ltv"),
|
|
margin_call_price=_decimal_from_boundary_value(
|
|
resolved_margin_call_price,
|
|
field_name="config.margin_call_price",
|
|
),
|
|
)
|
|
|
|
|
|
def margin_call_price_value(config: PortfolioConfig | _SaveStatusConfig) -> float:
|
|
return float(_normalize_save_status_snapshot(config).margin_call_price)
|
|
|
|
|
|
def save_status_text(config: PortfolioConfig | _SaveStatusConfig) -> str:
|
|
snapshot = _normalize_save_status_snapshot(config)
|
|
return (
|
|
f"Saved: basis={snapshot.entry_basis_mode}, start=${float(snapshot.gold_value):,.0f}, "
|
|
f"entry=${float(snapshot.entry_price):,.2f}/oz, weight={float(snapshot.gold_ounces):,.2f} oz, "
|
|
f"LTV={float(snapshot.current_ltv):.1%}, trigger=${float(snapshot.margin_call_price):,.2f}/oz"
|
|
)
|