feat: cockpit
This commit is contained in:
@@ -63,7 +63,8 @@ def mark_to_market(pos: Dict[str, Any]) -> Dict[str, Any]:
|
||||
days_to_expiry = T * 365
|
||||
|
||||
r = 0.05
|
||||
chain, surface = resolve_saxo_chain(underlying, target_days=max(int(days_to_expiry), 1))
|
||||
chain, surface = resolve_saxo_chain(underlying, target_days=max(int(days_to_expiry), 1),
|
||||
sanity_reference=pos.get("entry_underlying_price"))
|
||||
|
||||
# yfinance fallback inputs — only actually fetched if no usable Saxo chain, so a
|
||||
# Saxo-linked instrument never pays for a yfinance round-trip it doesn't need.
|
||||
@@ -276,13 +277,18 @@ def add_pos(req: AddPositionRequest):
|
||||
|
||||
# Underlying price — Saxo option chain's own spot first (real, and consistent with
|
||||
# whatever prices the legs below), yfinance only if this underlying has no
|
||||
# saxo_option_symbol link at all (Config -> Instruments Watchlist -> "Option").
|
||||
chain, surface = resolve_saxo_chain(normalized, target_days=data.get("expiry_days", 90))
|
||||
# saxo_option_symbol link at all (Config -> Instruments Watchlist -> "Option"). The
|
||||
# yfinance quote is fetched unconditionally (cheap, one-time at creation) to also
|
||||
# serve as resolve_saxo_chain's sanity_reference — guards against a Saxo chain whose
|
||||
# spot is on a different scale (observed on COMEX copper, ~100x too large).
|
||||
q = get_quote(normalized)
|
||||
yf_spot = q.get("price") if q else None
|
||||
chain, surface = resolve_saxo_chain(normalized, target_days=data.get("expiry_days", 90),
|
||||
sanity_reference=yf_spot)
|
||||
S = chain["spot"] if chain else None
|
||||
sigma = None
|
||||
if S is None:
|
||||
q = get_quote(normalized)
|
||||
S = q.get("price") if q else None
|
||||
S = yf_spot
|
||||
if S is not None:
|
||||
sigma = compute_historical_iv(req.underlying)
|
||||
if not S:
|
||||
|
||||
@@ -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