feat: cockpit

This commit is contained in:
OpenSquared
2026-07-27 08:29:55 +02:00
parent a08e8e1b11
commit ce09159bfb
4 changed files with 48 additions and 12 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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: