feat(CORE-002): add GLD share quote conversion seam

This commit is contained in:
Bu5hm4nn
2026-03-25 14:52:48 +01:00
parent 1a2dfaff01
commit f0d7ab5748
10 changed files with 425 additions and 34 deletions

96
app/domain/instruments.py Normal file
View File

@@ -0,0 +1,96 @@
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from app.domain.backtesting_math import AssetQuantity, PricePerAsset
from app.domain.units import BaseCurrency, PricePerWeight, Weight, WeightUnit
@dataclass(frozen=True, slots=True)
class InstrumentMetadata:
symbol: str
quote_currency: BaseCurrency | str
weight_per_share: Weight
def __post_init__(self) -> None:
normalized_symbol = str(self.symbol).strip().upper()
if not normalized_symbol:
raise ValueError("Instrument symbol is required")
object.__setattr__(self, "symbol", normalized_symbol)
object.__setattr__(self, "quote_currency", BaseCurrency(self.quote_currency))
object.__setattr__(self, "weight_per_share", self.weight_per_share)
def assert_symbol(self, symbol: str) -> InstrumentMetadata:
normalized = str(symbol).strip().upper()
if self.symbol != normalized:
raise ValueError(f"Instrument symbol mismatch: {self.symbol} != {normalized}")
return self
def assert_currency(self, currency: BaseCurrency | str) -> InstrumentMetadata:
normalized = BaseCurrency(currency)
if self.quote_currency is not normalized:
raise ValueError(f"Instrument currency mismatch: {self.quote_currency} != {normalized}")
return self
def price_per_weight_from_asset_price(
self,
price: PricePerAsset,
*,
per_unit: WeightUnit = WeightUnit.OUNCE_TROY,
) -> PricePerWeight:
self.assert_symbol(price.symbol)
self.assert_currency(price.currency)
weight_per_share = self.weight_per_share.to_unit(per_unit)
if weight_per_share.amount <= 0:
raise ValueError("Instrument weight_per_share must be positive")
return PricePerWeight(
amount=price.amount / weight_per_share.amount,
currency=price.currency,
per_unit=per_unit,
)
def weight_from_asset_quantity(self, quantity: AssetQuantity) -> Weight:
self.assert_symbol(quantity.symbol)
return Weight(amount=quantity.amount * self.weight_per_share.amount, unit=self.weight_per_share.unit)
def asset_quantity_from_weight(self, weight: Weight) -> AssetQuantity:
normalized_weight = weight.to_unit(self.weight_per_share.unit)
if self.weight_per_share.amount <= 0:
raise ValueError("Instrument weight_per_share must be positive")
return AssetQuantity(amount=normalized_weight.amount / self.weight_per_share.amount, symbol=self.symbol)
_GLD = InstrumentMetadata(
symbol="GLD",
quote_currency=BaseCurrency.USD,
weight_per_share=Weight(amount=Decimal("0.1"), unit=WeightUnit.OUNCE_TROY),
)
_INSTRUMENTS: dict[str, InstrumentMetadata] = {
_GLD.symbol: _GLD,
}
def instrument_metadata(symbol: str) -> InstrumentMetadata:
normalized = str(symbol).strip().upper()
metadata = _INSTRUMENTS.get(normalized)
if metadata is None:
raise ValueError(f"Unsupported instrument metadata: {normalized or symbol!r}")
return metadata
def price_per_weight_from_asset_price(
price: PricePerAsset,
*,
per_unit: WeightUnit = WeightUnit.OUNCE_TROY,
) -> PricePerWeight:
return instrument_metadata(price.symbol).price_per_weight_from_asset_price(price, per_unit=per_unit)
def weight_from_asset_quantity(quantity: AssetQuantity) -> Weight:
return instrument_metadata(quantity.symbol).weight_from_asset_quantity(quantity)
def asset_quantity_from_weight(symbol: str, weight: Weight) -> AssetQuantity:
return instrument_metadata(symbol).asset_quantity_from_weight(weight)