feat(DISPLAY-002): GLD mode shows real share prices
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any, Mapping
|
||||
|
||||
from app.domain.backtesting_math import PricePerAsset
|
||||
from app.domain.instruments import instrument_metadata
|
||||
from app.domain.conversions import is_gld_mode
|
||||
from app.domain.instruments import gld_ounces_per_share, instrument_metadata
|
||||
from app.domain.units import BaseCurrency, Money, PricePerWeight, Weight, WeightUnit, decimal_from_float
|
||||
from app.models.portfolio import PortfolioConfig
|
||||
|
||||
@@ -191,11 +193,40 @@ def resolve_portfolio_spot_from_quote(
|
||||
*,
|
||||
fallback_symbol: str | None = None,
|
||||
) -> tuple[float, str, str]:
|
||||
"""Resolve spot price from quote based on display mode.
|
||||
|
||||
In GLD display mode: return GLD share price directly (no conversion)
|
||||
In XAU display mode: convert GLD share price to oz-equivalent using expense-adjusted backing
|
||||
|
||||
Args:
|
||||
config: Portfolio configuration with display_mode setting
|
||||
quote: Quote data from data service
|
||||
fallback_symbol: Fallback symbol if quote lacks symbol
|
||||
|
||||
Returns:
|
||||
Tuple of (spot_price, source, updated_at)
|
||||
"""
|
||||
display_mode = getattr(config, "display_mode", "XAU")
|
||||
|
||||
# Try to resolve from quote first
|
||||
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", ""
|
||||
|
||||
if resolved is None:
|
||||
# No valid quote, use configured entry price
|
||||
configured_price = float(config.entry_price or 0.0)
|
||||
return configured_price, "configured_entry_price", ""
|
||||
|
||||
spot_price, source, updated_at = resolved
|
||||
|
||||
# In GLD mode, return share price directly (no conversion to oz)
|
||||
if is_gld_mode(display_mode):
|
||||
# For GLD mode, we want the share price, not the converted oz price
|
||||
quote_price = _safe_quote_price(quote.get("price"))
|
||||
if quote_price > 0:
|
||||
return quote_price, source, updated_at
|
||||
|
||||
# XAU mode: use the converted oz-equivalent price (already done in resolve_collateral_spot_from_quote)
|
||||
return spot_price, source, updated_at
|
||||
|
||||
|
||||
def portfolio_snapshot_from_config(
|
||||
@@ -203,16 +234,62 @@ def portfolio_snapshot_from_config(
|
||||
*,
|
||||
runtime_spot_price: float | None = None,
|
||||
) -> dict[str, float]:
|
||||
"""Build portfolio snapshot with display-mode-aware calculations.
|
||||
|
||||
In GLD mode:
|
||||
- gold_units: shares (not oz)
|
||||
- spot_price: GLD share price
|
||||
- gold_value: shares × share_price
|
||||
|
||||
In XAU mode:
|
||||
- gold_units: oz
|
||||
- spot_price: USD/oz
|
||||
- gold_value: oz × oz_price
|
||||
"""
|
||||
if config is None:
|
||||
gold_weight = Weight(amount=Decimal("1000"), unit=WeightUnit.OUNCE_TROY)
|
||||
spot = PricePerWeight(amount=Decimal("215"), currency=BaseCurrency.USD, per_unit=WeightUnit.OUNCE_TROY)
|
||||
loan_amount = Money(amount=Decimal("145000"), currency=BaseCurrency.USD)
|
||||
margin_call_ltv = Decimal("0.75")
|
||||
hedge_budget = Money(amount=Decimal("8000"), currency=BaseCurrency.USD)
|
||||
display_mode = "XAU"
|
||||
else:
|
||||
gold_weight = _gold_weight(float(config.gold_ounces or 0.0))
|
||||
display_mode = getattr(config, "display_mode", "XAU")
|
||||
resolved_spot = runtime_spot_price if runtime_spot_price is not None else float(config.entry_price or 0.0)
|
||||
spot = _spot_price(resolved_spot)
|
||||
|
||||
if is_gld_mode(display_mode):
|
||||
# GLD mode: work with shares directly
|
||||
# Use positions if available, otherwise fall back to legacy gold_ounces as shares
|
||||
if config.positions:
|
||||
# Sum GLD position quantities in shares
|
||||
total_shares = Decimal("0")
|
||||
for pos in config.positions:
|
||||
if pos.underlying == "GLD" and pos.unit == "shares":
|
||||
total_shares += pos.quantity
|
||||
elif pos.underlying == "GLD" and pos.unit == "oz":
|
||||
# Convert oz to shares using current backing
|
||||
backing = gld_ounces_per_share(date.today())
|
||||
total_shares += pos.quantity / backing
|
||||
else:
|
||||
# Non-GLD positions: treat as oz and convert to shares
|
||||
backing = gld_ounces_per_share(date.today())
|
||||
total_shares += pos.quantity / backing
|
||||
|
||||
gold_weight = Weight(amount=total_shares, unit=WeightUnit.OUNCE_TROY) # Store shares in weight for now
|
||||
else:
|
||||
# Legacy: treat gold_ounces as oz, convert to shares
|
||||
backing = gld_ounces_per_share(date.today())
|
||||
shares = Decimal(str(config.gold_ounces or 0.0)) / backing
|
||||
gold_weight = Weight(amount=shares, unit=WeightUnit.OUNCE_TROY)
|
||||
|
||||
spot = PricePerWeight(
|
||||
amount=decimal_from_float(resolved_spot), currency=BaseCurrency.USD, per_unit=WeightUnit.OUNCE_TROY
|
||||
)
|
||||
else:
|
||||
# XAU mode: work with oz
|
||||
gold_weight = _gold_weight(float(config.gold_ounces or 0.0))
|
||||
spot = _spot_price(resolved_spot)
|
||||
|
||||
loan_amount = Money(amount=decimal_from_float(float(config.loan_amount)), currency=BaseCurrency.USD)
|
||||
margin_call_ltv = decimal_from_float(float(config.margin_threshold))
|
||||
hedge_budget = Money(amount=decimal_from_float(float(config.monthly_budget)), currency=BaseCurrency.USD)
|
||||
@@ -233,6 +310,7 @@ def portfolio_snapshot_from_config(
|
||||
"margin_call_price": _decimal_to_float(margin_call_price),
|
||||
"cash_buffer": _DEFAULT_CASH_BUFFER,
|
||||
"hedge_budget": _money_to_float(hedge_budget),
|
||||
"display_mode": display_mode,
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +321,18 @@ def build_alert_context(
|
||||
source: str,
|
||||
updated_at: str,
|
||||
) -> dict[str, float | str]:
|
||||
gold_weight = _gold_weight(float(config.gold_ounces or 0.0))
|
||||
"""Build alert context with display-mode-aware calculations."""
|
||||
display_mode = getattr(config, "display_mode", "XAU")
|
||||
|
||||
if is_gld_mode(display_mode):
|
||||
# GLD mode: work with shares
|
||||
backing = gld_ounces_per_share(date.today())
|
||||
shares = Decimal(str(config.gold_ounces or 0.0)) / backing
|
||||
gold_weight = Weight(amount=shares, unit=WeightUnit.OUNCE_TROY)
|
||||
else:
|
||||
# XAU mode: work with oz
|
||||
gold_weight = _gold_weight(float(config.gold_ounces or 0.0))
|
||||
|
||||
live_spot = _spot_price(spot_price)
|
||||
gold_value = gold_weight * live_spot
|
||||
loan_amount = Money(amount=decimal_from_float(float(config.loan_amount)), currency=BaseCurrency.USD)
|
||||
@@ -263,6 +352,7 @@ def build_alert_context(
|
||||
"margin_call_price": _decimal_to_float(margin_call_price),
|
||||
"quote_source": source,
|
||||
"quote_updated_at": updated_at,
|
||||
"display_mode": display_mode,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user