feat: cockpit
This commit is contained in:
@@ -25,10 +25,23 @@ from typing import Any, Dict, Optional, Tuple
|
||||
from services.options_pricer import black_scholes
|
||||
|
||||
|
||||
def resolve_saxo_chain(underlying: str, target_days: int) -> Tuple[Optional[Dict[str, Any]], Optional[Any]]:
|
||||
def resolve_saxo_chain(
|
||||
underlying: str, target_days: int, sanity_reference: Optional[float] = None,
|
||||
) -> Tuple[Optional[Dict[str, Any]], Optional[Any]]:
|
||||
"""Returns (chain_slice, Surface) for this underlying's linked Saxo option chain, or
|
||||
(None, None) if it isn't linked, or the chain can't be built right now (Saxo down, no
|
||||
snapshot yet, entitlement gap, etc.) — callers fall back to yfinance pricing in that case."""
|
||||
snapshot yet, entitlement gap, etc.) — callers fall back to yfinance pricing in that case.
|
||||
|
||||
`sanity_reference` (typically the position's entry_underlying_price, or a fresh
|
||||
yfinance quote at creation time) guards against a chain whose "spot" is on a different
|
||||
scale than the rest of the app expects. Saxo's option-chain snapshot has no dedicated
|
||||
underlying-quote field (see saxo_client.snapshot_options_chain's MidStrikePrice-as-spot
|
||||
comment), so for at least one real instrument (COMEX copper, HG=F) it has been observed
|
||||
coming back ~100x too large — silently corrupting every downstream Black-Scholes reprice
|
||||
for that position (current spot showing $649 against a real ~$6.3/lb). A >5x or <0.2x
|
||||
deviation from a known-good reference is never a real intraday/short-term move for the
|
||||
instruments this app trades, so it's treated as a mis-scaled/wrong quote rather than a
|
||||
genuine price — safer to fall back to yfinance than to trust an unverifiable number."""
|
||||
from services.database import get_saxo_option_symbol_for_ticker
|
||||
saxo_symbol = get_saxo_option_symbol_for_ticker(underlying)
|
||||
if not saxo_symbol:
|
||||
@@ -37,9 +50,20 @@ def resolve_saxo_chain(underlying: str, target_days: int) -> Tuple[Optional[Dict
|
||||
from services.option_chain import get_chain_slice
|
||||
from services.vol_surface import Surface
|
||||
chain = get_chain_slice(saxo_symbol, target_days=max(target_days, 1))
|
||||
if not chain.get("spot"):
|
||||
spot = chain.get("spot")
|
||||
if not spot:
|
||||
return None, None
|
||||
surface = Surface(chain["spot"], chain["expiries"])
|
||||
if sanity_reference and sanity_reference > 0:
|
||||
ratio = spot / sanity_reference
|
||||
if ratio > 5 or ratio < 0.2:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(
|
||||
f"[portfolio_pricing] Saxo chain spot for '{underlying}' ({spot}) is "
|
||||
f"{ratio:.1f}x the reference ({sanity_reference}) — treating as a "
|
||||
f"mis-scaled/unusable quote, falling back to yfinance."
|
||||
)
|
||||
return None, None
|
||||
surface = Surface(spot, chain["expiries"])
|
||||
return chain, surface
|
||||
except Exception:
|
||||
return None, None
|
||||
@@ -106,7 +130,8 @@ def compute_payoff(pos: Dict[str, Any], n_points: int = 61, range_pct: float = 0
|
||||
days_remaining = max(0, pos.get("expiry_days", 90) - (date.today() - entry).days)
|
||||
|
||||
r = 0.05
|
||||
chain, surface = resolve_saxo_chain(underlying, target_days=max(days_remaining, 1))
|
||||
chain, surface = resolve_saxo_chain(underlying, target_days=max(days_remaining, 1),
|
||||
sanity_reference=pos.get("entry_underlying_price"))
|
||||
fallback_spot = pos.get("entry_underlying_price") or 100.0
|
||||
fallback_sigma = 0.20
|
||||
if chain is None:
|
||||
|
||||
@@ -138,7 +138,8 @@ def _resolve_position_market(pos: Dict[str, Any]):
|
||||
entry = datetime.strptime(pos["entry_date"][:10], "%Y-%m-%d").date()
|
||||
days_remaining = max(0, pos.get("expiry_days", 90) - (date.today() - entry).days)
|
||||
|
||||
chain, surface = resolve_saxo_chain(underlying, target_days=max(days_remaining, 1))
|
||||
chain, surface = resolve_saxo_chain(underlying, target_days=max(days_remaining, 1),
|
||||
sanity_reference=pos.get("entry_underlying_price"))
|
||||
fallback_spot = pos.get("entry_underlying_price") or 100.0
|
||||
fallback_sigma = 0.20
|
||||
if chain is None:
|
||||
|
||||
Reference in New Issue
Block a user