119 lines
4.0 KiB
Python
119 lines
4.0 KiB
Python
"""Position cost calculations for premium, spread, and storage costs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from app.models.position import Position
|
|
|
|
|
|
def calculate_effective_entry(
|
|
entry_price: Decimal,
|
|
purchase_premium: Decimal | None = None,
|
|
) -> Decimal:
|
|
"""Calculate effective entry cost including dealer premium.
|
|
|
|
Args:
|
|
entry_price: Spot price at entry (per unit)
|
|
purchase_premium: Dealer markup over spot as percentage (e.g., 0.04 for 4%)
|
|
|
|
Returns:
|
|
Effective entry cost per unit
|
|
"""
|
|
if purchase_premium is None or purchase_premium == 0:
|
|
return entry_price
|
|
return entry_price * (Decimal("1") + purchase_premium)
|
|
|
|
|
|
def calculate_effective_exit(
|
|
current_spot: Decimal,
|
|
bid_ask_spread: Decimal | None = None,
|
|
) -> Decimal:
|
|
"""Calculate effective exit value after bid/ask spread.
|
|
|
|
Args:
|
|
current_spot: Current spot price (per unit)
|
|
bid_ask_spread: Expected sale discount below spot as percentage (e.g., 0.03 for 3%)
|
|
|
|
Returns:
|
|
Effective exit value per unit
|
|
"""
|
|
if bid_ask_spread is None or bid_ask_spread == 0:
|
|
return current_spot
|
|
return current_spot * (Decimal("1") - bid_ask_spread)
|
|
|
|
|
|
def calculate_true_pnl(
|
|
position: Position,
|
|
current_spot: Decimal,
|
|
) -> dict[str, Any]:
|
|
"""Calculate true P&L accounting for premium and spread.
|
|
|
|
Args:
|
|
position: Position to calculate P&L for
|
|
current_spot: Current spot price per unit
|
|
|
|
Returns:
|
|
Dict with paper_pnl, realized_pnl, effective_entry, effective_exit, entry_value, exit_value
|
|
"""
|
|
# Effective entry cost (includes premium)
|
|
effective_entry = calculate_effective_entry(position.entry_price, position.purchase_premium)
|
|
|
|
# Effective exit value (after spread)
|
|
effective_exit = calculate_effective_exit(current_spot, position.bid_ask_spread)
|
|
|
|
# Paper P&L (without premium/spread)
|
|
paper_pnl = (current_spot - position.entry_price) * position.quantity
|
|
|
|
# True P&L (with premium/spread)
|
|
true_pnl = (effective_exit - effective_entry) * position.quantity
|
|
|
|
# Entry and exit values
|
|
entry_value = position.entry_price * position.quantity
|
|
exit_value = current_spot * position.quantity
|
|
|
|
return {
|
|
"paper_pnl": float(paper_pnl),
|
|
"true_pnl": float(true_pnl),
|
|
"effective_entry": float(effective_entry),
|
|
"effective_exit": float(effective_exit),
|
|
"entry_value": float(entry_value),
|
|
"exit_value": float(exit_value),
|
|
"premium_impact": float((position.purchase_premium or 0) * entry_value),
|
|
"spread_impact": float((position.bid_ask_spread or 0) * exit_value),
|
|
}
|
|
|
|
|
|
def get_default_premium_for_product(underlying: str, product_type: str = "default") -> Decimal | None:
|
|
"""Get default premium/spread for common gold products.
|
|
|
|
Args:
|
|
underlying: Underlying instrument ("GLD", "GC=F", "XAU")
|
|
product_type: Product type ("default", "coin_1oz", "bar_1kg", "allocated")
|
|
|
|
Returns:
|
|
Tuple of (purchase_premium, bid_ask_spread) or None if not applicable
|
|
"""
|
|
# GLD/GLDM: ETF is liquid, minimal spread
|
|
if underlying in ("GLD", "GLDM"):
|
|
# ETF spread is minimal, premium is 0
|
|
return Decimal("0"), Decimal("0.001") # 0% premium, 0.1% spread
|
|
|
|
# GC=F: Futures roll costs are handled separately (GCF-001)
|
|
if underlying == "GC=F":
|
|
return None, None
|
|
|
|
# XAU: Physical gold
|
|
if underlying == "XAU":
|
|
defaults = {
|
|
"default": (Decimal("0.04"), Decimal("0.03")), # 4% premium, 3% spread
|
|
"coin_1oz": (Decimal("0.04"), Decimal("0.03")), # 1oz coins: 4% premium, 3% spread
|
|
"bar_1kg": (Decimal("0.015"), Decimal("0.015")), # 1kg bars: 1.5% premium, 1.5% spread
|
|
"allocated": (Decimal("0.001"), Decimal("0.003")), # Allocated: 0.1% premium, 0.3% spread
|
|
}
|
|
return defaults.get(product_type, defaults["default"])
|
|
|
|
# Unknown underlying
|
|
return None, None
|