Files
vault-dash/app/services/position_costs.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

122 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"
) -> tuple[Decimal | None, 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