Files
vault-dash/app/models/ltv_history.py
Bu5hm4nn 887565be74 fix(types): resolve all mypy type errors (CORE-003)
- Fix return type annotation for get_default_premium_for_product
- Add type narrowing for Weight|Money union using _as_money helper
- Add isinstance checks before float() calls for object types
- Add type guard for Decimal.exponent comparison
- Use _unit_typed and _currency_typed properties for type narrowing
- Cast option_type to OptionType Literal after validation
- Fix provider type hierarchy in backtesting services
- Add types-requests to dev dependencies
- Remove '|| true' from CI type-check job

All 36 mypy errors resolved across 15 files.
2026-03-30 00:05:09 +02:00

199 lines
8.0 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any
class LtvHistoryLoadError(RuntimeError):
def __init__(self, history_path: Path, message: str) -> None:
super().__init__(message)
self.history_path = history_path
@dataclass(frozen=True)
class LtvSnapshot:
snapshot_date: str
captured_at: str
ltv_ratio: Decimal
margin_threshold: Decimal
loan_amount: Decimal
collateral_value: Decimal
spot_price: Decimal
source: str
def __post_init__(self) -> None:
for field_name in ("snapshot_date", "captured_at", "source"):
value = getattr(self, field_name)
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{field_name} must be a non-empty string")
date.fromisoformat(self.snapshot_date)
datetime.fromisoformat(self.captured_at.replace("Z", "+00:00"))
for field_name in (
"ltv_ratio",
"margin_threshold",
"loan_amount",
"collateral_value",
"spot_price",
):
value = getattr(self, field_name)
if not isinstance(value, Decimal) or not value.is_finite():
raise TypeError(f"{field_name} must be a finite Decimal")
if self.ltv_ratio < 0:
raise ValueError("ltv_ratio must be zero or greater")
if not Decimal("0") < self.margin_threshold < Decimal("1"):
raise ValueError("margin_threshold must be between 0 and 1")
if self.loan_amount < 0:
raise ValueError("loan_amount must be zero or greater")
if self.collateral_value <= 0:
raise ValueError("collateral_value must be positive")
if self.spot_price <= 0:
raise ValueError("spot_price must be positive")
def to_dict(self) -> dict[str, Any]:
return {
"snapshot_date": self.snapshot_date,
"captured_at": self.captured_at,
"ltv_ratio": _structured_ratio_payload(self.ltv_ratio),
"margin_threshold": _structured_ratio_payload(self.margin_threshold),
"loan_amount": _structured_money_payload(self.loan_amount),
"collateral_value": _structured_money_payload(self.collateral_value),
"spot_price": _structured_price_payload(self.spot_price),
"source": self.source,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "LtvSnapshot":
return cls(
snapshot_date=_require_non_empty_string(data, "snapshot_date"),
captured_at=_require_non_empty_string(data, "captured_at"),
ltv_ratio=_parse_ratio_payload(data.get("ltv_ratio"), field_name="ltv_ratio"),
margin_threshold=_parse_ratio_payload(data.get("margin_threshold"), field_name="margin_threshold"),
loan_amount=_parse_money_payload(data.get("loan_amount"), field_name="loan_amount"),
collateral_value=_parse_money_payload(data.get("collateral_value"), field_name="collateral_value"),
spot_price=_parse_price_payload(data.get("spot_price"), field_name="spot_price"),
source=_require_non_empty_string(data, "source"),
)
class LtvHistoryRepository:
def __init__(self, base_path: Path | str = Path("data/workspaces")) -> None:
self.base_path = Path(base_path)
self.base_path.mkdir(parents=True, exist_ok=True)
def load(self, workspace_id: str) -> list[LtvSnapshot]:
history_path = self.history_path(workspace_id)
if not history_path.exists():
return []
try:
payload = json.loads(history_path.read_text())
except json.JSONDecodeError as exc:
raise LtvHistoryLoadError(history_path, f"LTV history is not valid JSON: {exc}") from exc
except OSError as exc:
raise LtvHistoryLoadError(history_path, f"LTV history could not be read: {exc}") from exc
if not isinstance(payload, list):
raise LtvHistoryLoadError(history_path, "LTV history payload must be a list")
snapshots: list[LtvSnapshot] = []
for index, item in enumerate(payload):
if not isinstance(item, dict):
raise LtvHistoryLoadError(history_path, f"LTV history entry {index} must be an object")
try:
snapshots.append(LtvSnapshot.from_dict(item))
except (TypeError, ValueError, KeyError) as exc:
raise LtvHistoryLoadError(history_path, f"LTV history entry {index} is invalid: {exc}") from exc
return snapshots
def save(self, workspace_id: str, snapshots: list[LtvSnapshot]) -> None:
history_path = self.history_path(workspace_id)
history_path.parent.mkdir(parents=True, exist_ok=True)
history_path.write_text(json.dumps([snapshot.to_dict() for snapshot in snapshots], indent=2))
def history_path(self, workspace_id: str) -> Path:
return self.base_path / workspace_id / "ltv_history.json"
def _require_non_empty_string(data: dict[str, Any], field_name: str) -> str:
value = data.get(field_name)
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{field_name} must be a non-empty string")
return value
def _decimal_text(value: Decimal) -> str:
if value == value.to_integral():
return str(value.quantize(Decimal("1")))
normalized = value.normalize()
exponent = normalized.as_tuple().exponent
if isinstance(exponent, int) and exponent < 0:
return format(normalized, "f")
return str(normalized)
def _parse_decimal_payload(
payload: object,
*,
field_name: str,
expected_tag_key: str,
expected_tag_value: str,
expected_currency: str | None = None,
expected_per_weight_unit: str | None = None,
) -> Decimal:
if not isinstance(payload, dict):
raise TypeError(f"{field_name} must be an object")
if payload.get(expected_tag_key) != expected_tag_value:
raise ValueError(f"{field_name} must declare {expected_tag_key}={expected_tag_value!r}")
if expected_currency is not None and payload.get("currency") != expected_currency:
raise ValueError(f"{field_name} must declare currency={expected_currency!r}")
if expected_per_weight_unit is not None and payload.get("per_weight_unit") != expected_per_weight_unit:
raise ValueError(f"{field_name} must declare per_weight_unit={expected_per_weight_unit!r}")
raw_value = payload.get("value")
if not isinstance(raw_value, str) or not raw_value.strip():
raise ValueError(f"{field_name}.value must be a non-empty string")
try:
value = Decimal(raw_value)
except InvalidOperation as exc:
raise ValueError(f"{field_name}.value must be numeric") from exc
if not value.is_finite():
raise ValueError(f"{field_name}.value must be finite")
return value
def _parse_ratio_payload(payload: object, *, field_name: str) -> Decimal:
return _parse_decimal_payload(payload, field_name=field_name, expected_tag_key="unit", expected_tag_value="ratio")
def _parse_money_payload(payload: object, *, field_name: str) -> Decimal:
return _parse_decimal_payload(
payload,
field_name=field_name,
expected_tag_key="currency",
expected_tag_value="USD",
expected_currency="USD",
)
def _parse_price_payload(payload: object, *, field_name: str) -> Decimal:
return _parse_decimal_payload(
payload,
field_name=field_name,
expected_tag_key="currency",
expected_tag_value="USD",
expected_currency="USD",
expected_per_weight_unit="ozt",
)
def _structured_ratio_payload(value: Decimal) -> dict[str, str]:
return {"value": str(value), "unit": "ratio"}
def _structured_money_payload(value: Decimal) -> dict[str, str]:
return {"value": _decimal_text(value), "currency": "USD"}
def _structured_price_payload(value: Decimal) -> dict[str, str]:
return {"value": _decimal_text(value), "currency": "USD", "per_weight_unit": "ozt"}