""" Options volatility endpoints — IV Rank, Term Structure, Skew, Options Flow. """ from fastapi import APIRouter, BackgroundTasks, HTTPException from typing import List, Optional import logging router = APIRouter(prefix="/api/options-vol", tags=["options-vol"]) logger = logging.getLogger(__name__) # Simple in-memory cache to avoid hammering yfinance on every page load _iv_cache: dict = {} _CACHE_TTL_SECONDS = 3600 # 1h def _is_stale(key: str) -> bool: import time entry = _iv_cache.get(key) if not entry: return True return (time.time() - entry["ts"]) > _CACHE_TTL_SECONDS def _set_cache(key: str, data: dict): import time _iv_cache[key] = {"data": data, "ts": time.time()} @router.get("/snapshot/{ticker}") def get_iv_snapshot(ticker: str, force: bool = False): """Full IV snapshot for one ticker: IV, Rank, Percentile, Term Structure, Skew, Flow.""" key = f"snapshot:{ticker.upper()}" if not force and not _is_stale(key): return _iv_cache[key]["data"] from services.iv_engine import get_full_iv_snapshot data = get_full_iv_snapshot(ticker) _set_cache(key, data) return data @router.get("/batch") def get_iv_batch(tickers: str = "SPY,QQQ,GLD,USO,UNG,XLE,TLT"): """ IV snapshot for multiple tickers (comma-separated). Returns a dict {ticker: snapshot}. Cached 1h per ticker. """ ticker_list = [t.strip().upper() for t in tickers.split(",") if t.strip()][:12] result = {} from services.iv_engine import get_full_iv_snapshot for ticker in ticker_list: key = f"snapshot:{ticker}" if not _is_stale(key): result[ticker] = _iv_cache[key]["data"] else: snap = get_full_iv_snapshot(ticker) _set_cache(key, snap) result[ticker] = snap return {"snapshots": result, "count": len(result)} @router.get("/watchlist") def get_iv_watchlist(): """ IV for the core IV watchlist (portfolio-relevant tickers). Returns a summary list sorted by IV Rank descending. """ from services.iv_engine import get_atm_iv, _resolve_ticker from services.database import get_iv_rank_percentile, save_iv_snapshot, get_iv_history, get_watchlist_tickers from datetime import date results = [] today = date.today().isoformat() for ticker in get_watchlist_tickers(): key = f"watchlist:{ticker}" if not _is_stale(key): results.append(_iv_cache[key]["data"]) continue proxy = _resolve_ticker(ticker) iv = get_atm_iv(ticker, 30) live_iv = iv is not None # Fallback: use most recent historical IV when live options chain fails if iv is None: recent = get_iv_history(proxy, days=5) if recent: iv = recent[0]["iv_current"] if not iv: continue if live_iv: save_iv_snapshot(proxy, today, iv) rank_data = get_iv_rank_percentile(proxy, iv) item = { "ticker": ticker, "iv_current_pct": round(iv * 100, 1), "iv_rank": rank_data.get("iv_rank"), "iv_percentile": rank_data.get("iv_percentile"), "history_days": rank_data.get("history_days", 0), "iv_source": "live" if live_iv else "history", "signal": ( "sell_vol" if (rank_data.get("iv_rank") or 0) > 80 else "buy_vol" if (rank_data.get("iv_rank") or 100) < 20 else "neutral" ), } _set_cache(key, item) results.append(item) results.sort(key=lambda x: -(x.get("iv_rank") or 0)) return {"items": results, "count": len(results)} @router.get("/history/{ticker}") def get_iv_history_endpoint(ticker: str, days: int = 90): """Historical IV for a ticker (used to render IV chart).""" from services.database import get_iv_history from services.iv_engine import _resolve_ticker proxy = _resolve_ticker(ticker) history = get_iv_history(proxy, days) return {"ticker": ticker, "proxy": proxy, "history": history, "count": len(history)} @router.get("/watchlist-tickers") def list_watchlist_tickers(): """List all watchlist tickers (active + inactive) with metadata.""" from services.database import get_watchlist_entries return {"tickers": get_watchlist_entries()} @router.post("/watchlist-tickers/{ticker}") def add_to_watchlist(ticker: str, background_tasks: BackgroundTasks): """Add a ticker to the IV watchlist and start background bootstrap.""" from services.database import add_watchlist_ticker from services.iv_engine import _resolve_ticker proxy = _resolve_ticker(ticker.upper()) is_new = add_watchlist_ticker(proxy, added_by="manual") if is_new: def _bootstrap(): from services.iv_engine import bootstrap_iv_history bootstrap_iv_history(tickers=[proxy], min_existing=0) logger.info(f"[Watchlist] Bootstrap done for new ticker {proxy}") background_tasks.add_task(_bootstrap) return {"ticker": proxy, "is_new": is_new, "bootstrap": is_new} @router.delete("/watchlist-tickers/{ticker}") def remove_from_watchlist(ticker: str): """Remove (soft-delete) a ticker from the watchlist.""" from services.database import remove_watchlist_ticker from services.iv_engine import _resolve_ticker proxy = _resolve_ticker(ticker.upper()) ok = remove_watchlist_ticker(proxy) return {"ticker": proxy, "removed": ok} @router.post("/refresh-watchlist") def refresh_watchlist(background_tasks: BackgroundTasks): """Trigger a background refresh of IV data for all watchlist tickers.""" def _refresh(): from services.iv_engine import get_full_iv_snapshot from services.database import get_watchlist_tickers tickers = get_watchlist_tickers() logger.info(f"[IVRefresh] Refreshing IV for {len(tickers)} tickers") for ticker in tickers: try: snap = get_full_iv_snapshot(ticker) key = f"snapshot:{ticker}" import time _iv_cache[key] = {"data": snap, "ts": time.time()} logger.debug(f"[IVRefresh] {ticker}: IV={snap.get('iv_current_pct')}% IVR={snap.get('iv_rank')}") except Exception as e: logger.warning(f"[IVRefresh] {ticker} failed: {e}") logger.info("[IVRefresh] Done") from services.database import get_watchlist_tickers count = len(get_watchlist_tickers()) background_tasks.add_task(_refresh) return {"status": "refresh started", "tickers": count} @router.post("/bootstrap-history") def bootstrap_iv_history_endpoint(background_tasks: BackgroundTasks, min_existing: int = 30): """Bootstrap iv_history with 1 year of realized vol for all watchlist tickers.""" def _run(): from services.iv_engine import bootstrap_iv_history from services.database import get_watchlist_tickers tickers = get_watchlist_tickers() logger.info(f"[IV Bootstrap] Starting for {len(tickers)} tickers") result = bootstrap_iv_history(tickers=tickers, min_existing=min_existing) total = sum(v.get("inserted", 0) for v in result.values() if isinstance(v, dict)) logger.info(f"[IV Bootstrap] Done — {total} rows inserted across {len(result)} tickers") background_tasks.add_task(_run) return {"status": "bootstrap started", "message": "Chargement historique 1 an réalisé vol — vérifier les logs dans ~60s"} @router.get("/for-trade/{underlying}") def get_iv_for_trade(underlying: str): """ IV snapshot specifically for a trade's underlying. Used by JournalDeBord to show the IV context at trade time. """ from services.iv_engine import get_full_iv_snapshot key = f"snapshot:{underlying.upper()}" if not _is_stale(key): return _iv_cache[key]["data"] data = get_full_iv_snapshot(underlying) _set_cache(key, data) return data