fix: bootstrap IV history with 1y realized vol to unblock IV Rank from 50

IV Rank was stuck at 50 for all tickers because the formula returns 50.0
when iv_max == iv_min (not enough historical snapshots). Added:
- bootstrap_iv_history(): downloads 1y of closes per ticker, computes
  30d rolling realized vol (annualized), saves each day to iv_history
- POST /api/options-vol/bootstrap-history endpoint (runs in background)
- OptionsLab: auto-detect when IV Rank needs bootstrapping and show
  an amber banner with one-click "Initialiser historique" button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-17 21:33:01 +02:00
parent 7e38fd6257
commit 97ba36454b
3 changed files with 110 additions and 1 deletions

View File

@@ -142,6 +142,24 @@ def refresh_watchlist(background_tasks: BackgroundTasks):
return {"status": "refresh started", "tickers": len(__import__("services.iv_engine", fromlist=["IV_WATCHLIST"]).IV_WATCHLIST)}
@router.post("/bootstrap-history")
def bootstrap_iv_history_endpoint(background_tasks: BackgroundTasks, min_existing: int = 30):
"""
Bootstrap iv_history with 1 year of realized volatility for all watchlist tickers.
Only fills missing dates — safe to call multiple times.
Runs in background (takes ~30-60s for 18 tickers).
"""
def _run():
from services.iv_engine import bootstrap_iv_history, IV_WATCHLIST
logger.info(f"[IV Bootstrap] Starting for {len(IV_WATCHLIST)} tickers")
result = bootstrap_iv_history(tickers=IV_WATCHLIST, 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):
"""

View File

@@ -418,6 +418,58 @@ def get_full_iv_snapshot(ticker: str) -> Dict[str, Any]:
}
def bootstrap_iv_history(tickers: Optional[List[str]] = None, days: int = 365, min_existing: int = 30) -> Dict[str, Any]:
"""
Bootstrap iv_history with 1 year of realized volatility (rolling 30d annualized).
Used when the system is new and has no meaningful IV Rank history.
Only fills dates not already present (safe to re-run).
Returns a summary dict {ticker: rows_inserted}.
"""
import math
from services.database import save_iv_snapshot, get_iv_history
targets = [_resolve_ticker(t) for t in (tickers or IV_WATCHLIST)]
results: Dict[str, Any] = {}
for ticker in targets:
existing = get_iv_history(ticker, days=days)
if len(existing) >= min_existing:
results[ticker] = {"skipped": True, "existing_rows": len(existing)}
continue
try:
t = yf.Ticker(ticker)
hist = t.history(period="1y", interval="1d")
if hist.empty or len(hist) < 32:
results[ticker] = {"error": "insufficient price history"}
continue
closes = hist["Close"].dropna()
log_returns = [math.log(closes.iloc[i] / closes.iloc[i - 1]) for i in range(1, len(closes))]
inserted = 0
for i in range(30, len(log_returns)):
window = log_returns[i - 30:i]
mean = sum(window) / 30
variance = sum((r - mean) ** 2 for r in window) / 29
realized_vol = math.sqrt(variance * 252)
snap_date = closes.index[i + 1].date().isoformat()
try:
save_iv_snapshot(ticker, snap_date, realized_vol, None, None, None)
inserted += 1
except Exception:
pass # UNIQUE constraint = date already exists, skip
results[ticker] = {"inserted": inserted}
logger.info(f"[IV Bootstrap] {ticker}: {inserted} rows inserted")
except Exception as e:
results[ticker] = {"error": str(e)}
logger.warning(f"[IV Bootstrap] {ticker}: {e}")
return results
def get_iv_context_for_prompt(tickers: List[str]) -> str:
"""
Build a compact IV context string to inject into GPT-4o scoring prompts.