"""Display mode conversion utilities for GLD/XAU views. This module handles conversion between GLD share prices and physical gold prices based on the user's display mode preference. Key insight: - In GLD mode: show share prices directly, no conversion to oz - In XAU mode: convert GLD shares to oz-equivalent using expense-adjusted backing """ from __future__ import annotations from dataclasses import dataclass from datetime import date from decimal import Decimal from app.domain.instruments import gld_ounces_per_share from app.models.position import Position @dataclass(frozen=True) class DisplayContext: """Context for display mode conversions. Attributes: mode: Display mode ("GLD" for shares, "XAU" for physical gold) reference_date: Date for historical conversion lookups gld_ounces_per_share: GLD backing ratio for the reference date """ mode: str reference_date: date | None = None gld_ounces_per_share: Decimal | None = None def __post_init__(self) -> None: if self.mode not in {"GLD", "XAU"}: raise ValueError(f"Invalid display mode: {self.mode!r}") @classmethod def create(cls, mode: str, reference_date: date | None = None) -> "DisplayContext": """Create a display context with computed GLD backing ratio.""" gld_backing = None if mode == "XAU" and reference_date is not None: gld_backing = gld_ounces_per_share(reference_date) return cls(mode=mode, reference_date=reference_date, gld_ounces_per_share=gld_backing) def is_gld_mode(display_mode: str) -> bool: """Check if display mode is GLD (share view).""" return display_mode == "GLD" def is_xau_mode(display_mode: str) -> bool: """Check if display mode is XAU (physical gold view).""" return display_mode == "XAU" def convert_position_to_display( position: Position, display_mode: str, reference_date: date | None = None, ) -> tuple[Decimal, str, Decimal]: """Convert a position to display units based on display mode. Args: position: Position to convert display_mode: "GLD" for shares, "XAU" for physical gold reference_date: Date for historical conversion (for GLD->XAU) Returns: Tuple of (display_quantity, display_unit, display_entry_price) Examples: >>> # GLD position in GLD mode: show as-is >>> from datetime import date >>> from decimal import Decimal >>> from app.models.position import create_position >>> pos = create_position( ... underlying="GLD", ... quantity=Decimal("100"), ... unit="shares", ... entry_price=Decimal("400"), ... entry_date=date.today(), ... ) >>> qty, unit, price = convert_position_to_display(pos, "GLD") >>> qty, unit, price (Decimal('100'), 'shares', Decimal('400')) >>> # GLD position in XAU mode: convert to oz >>> qty, unit, price = convert_position_to_display(pos, "XAU", date.today()) >>> # qty will be shares * oz_per_share """ if display_mode == "GLD": # GLD mode: show shares directly if position.underlying == "GLD": return position.quantity, position.unit, position.entry_price # Non-GLD positions in GLD mode: would need conversion (not implemented yet) return position.quantity, position.unit, position.entry_price elif display_mode == "XAU": # XAU mode: convert to physical gold ounces if position.underlying == "GLD": # Convert GLD shares to oz using expense-adjusted backing backing = gld_ounces_per_share(reference_date or position.entry_date) display_qty = position.quantity * backing display_price = position.entry_price / backing # Price per oz return display_qty, "oz", display_price # XAU positions already in oz return position.quantity, position.unit, position.entry_price else: raise ValueError(f"Unsupported display mode: {display_mode!r}") def convert_price_to_display( price: Decimal, from_unit: str, to_mode: str, reference_date: date | None = None, ) -> tuple[Decimal, str]: """Convert a price to display mode units. Args: price: Price value to convert from_unit: Source unit ("shares" or "oz") to_mode: Target display mode ("GLD" or "XAU") reference_date: Date for historical conversion Returns: Tuple of (converted_price, display_unit) """ if to_mode == "GLD": if from_unit == "shares": return price, "shares" elif from_unit == "oz": # Convert oz price to share price backing = gld_ounces_per_share(reference_date or date.today()) return price * backing, "shares" elif to_mode == "XAU": if from_unit == "oz": return price, "oz" elif from_unit == "shares": # Convert share price to oz price backing = gld_ounces_per_share(reference_date or date.today()) return price / backing, "oz" raise ValueError(f"Unsupported conversion: {from_unit} -> {to_mode}") def convert_quantity_to_display( quantity: Decimal, from_unit: str, to_mode: str, reference_date: date | None = None, ) -> tuple[Decimal, str]: """Convert a quantity to display mode units. Args: quantity: Quantity value to convert from_unit: Source unit ("shares" or "oz") to_mode: Target display mode ("GLD" or "XAU") reference_date: Date for historical conversion Returns: Tuple of (converted_quantity, display_unit) """ if to_mode == "GLD": if from_unit == "shares": return quantity, "shares" elif from_unit == "oz": # Convert oz to shares (inverse of backing) backing = gld_ounces_per_share(reference_date or date.today()) return quantity / backing, "shares" elif to_mode == "XAU": if from_unit == "oz": return quantity, "oz" elif from_unit == "shares": # Convert shares to oz using backing backing = gld_ounces_per_share(reference_date or date.today()) return quantity * backing, "oz" raise ValueError(f"Unsupported conversion: {from_unit} -> {to_mode}") def get_display_unit_label(underlying: str, display_mode: str) -> str: """Get the display unit label for a position based on display mode. Args: underlying: Position underlying symbol display_mode: Display mode ("GLD" or "XAU") Returns: Unit label string ("shares", "oz", etc.) """ if underlying == "GLD": if display_mode == "GLD": return "shares" else: # XAU mode return "oz" elif underlying in ("XAU", "GC=F"): return "oz" if display_mode == "XAU" else "oz" # Physical gold always in oz return "units" def calculate_position_value_in_display_mode( quantity: Decimal, unit: str, current_price: Decimal, price_unit: str, display_mode: str, reference_date: date | None = None, ) -> Decimal: """Calculate position value in display mode. Args: quantity: Position quantity unit: Position unit current_price: Current market price price_unit: Price unit ("shares" or "oz") display_mode: Display mode ("GLD" or "XAU") reference_date: Date for conversion Returns: Position value in USD """ if display_mode == "GLD" and unit == "shares": # GLD mode: shares × share_price return quantity * current_price elif display_mode == "XAU" and unit == "oz": # XAU mode: oz × oz_price return quantity * current_price elif display_mode == "GLD" and unit == "oz": # Convert oz to shares, then calculate backing = gld_ounces_per_share(reference_date or date.today()) shares = quantity / backing share_price = current_price * backing return shares * share_price elif display_mode == "XAU" and unit == "shares": # Convert shares to oz, then calculate backing = gld_ounces_per_share(reference_date or date.today()) oz = quantity * backing oz_price = current_price / backing return oz * oz_price # Fallback: direct multiplication return quantity * current_price def calculate_pnl_in_display_mode( quantity: Decimal, unit: str, entry_price: Decimal, current_price: Decimal, display_mode: str, reference_date: date | None = None, ) -> Decimal: """Calculate P&L in display mode. Args: quantity: Position quantity unit: Position unit entry_price: Entry price per unit current_price: Current price per unit display_mode: Display mode ("GLD" or "XAU") reference_date: Date for conversion Returns: P&L in USD """ if display_mode == "GLD" and unit == "shares": # GLD mode: (current_share_price - entry_share_price) × shares return (current_price - entry_price) * quantity elif display_mode == "XAU" and unit == "oz": # XAU mode: (current_oz_price - entry_oz_price) × oz return (current_price - entry_price) * quantity elif display_mode == "GLD" and unit == "oz": # Convert to share basis backing = gld_ounces_per_share(reference_date or date.today()) shares = quantity * backing # oz → shares (wait, this is wrong) # Actually: if we have oz, we need to convert to shares # shares = oz / backing shares = quantity / backing share_entry = entry_price / backing share_current = current_price / backing return (share_current - share_entry) * shares elif display_mode == "XAU" and unit == "shares": # Convert to oz basis backing = gld_ounces_per_share(reference_date or date.today()) oz = quantity * backing oz_entry = entry_price * backing oz_current = current_price * backing return (oz_current - oz_entry) * oz # Fallback return (current_price - entry_price) * quantity