feat: Phase 1 — IV Rank, Term Structure, Skew, Options Flow (Sprint 1.1/1.2/1.3)

Backend:
- iv_engine.py: ATM IV, term structure (30/60/90/180j), put/call skew,
  options flow (P/C OI ratio, unusual strikes, gamma bias), proxy map for futures→ETFs
- database.py: iv_history table + save_iv_snapshot, get_iv_rank_percentile, get_iv_history
- routers/options_vol.py: /api/options-vol/ endpoints (snapshot, batch, watchlist, history)
- auto_cycle.py: inject IV context string into scoring prompt (step 3.5)
- ai_analyzer.py: score_patterns_with_context accepts iv_context param
- main.py: register options_vol router

Frontend:
- pages/OptionsLab.tsx: full IV dashboard (watchlist by IVR, term structure, skew, flow, sparkline)
- pages/JournalDeBord.tsx: IvRankCell component + IV Rank column per trade
- hooks/useApi.ts: useIvSnapshot, useIvWatchlist, useIvBatch, useIvHistory, useIvForTrade

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-17 16:29:33 +02:00
parent 07c1a74704
commit 9a6b6f70b1
12 changed files with 3886 additions and 329 deletions

View File

@@ -284,9 +284,21 @@ def init_db():
trades_analyzed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)""")
c.execute("""CREATE TABLE IF NOT EXISTS iv_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL,
recorded_date TEXT NOT NULL,
iv_current REAL,
iv_30d REAL,
iv_60d REAL,
iv_90d REAL,
created_at TEXT DEFAULT (datetime('now'))
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category, status)")
c.execute("CREATE INDEX IF NOT EXISTS idx_rs_version ON reasoning_state(version DESC)")
c.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_iv_history_ticker_date ON iv_history(ticker, recorded_date)")
except Exception:
pass
@@ -1501,3 +1513,63 @@ def delete_kb_entry(entry_id: int) -> bool:
conn.commit()
conn.close()
return cur.rowcount > 0
# ── IV History ────────────────────────────────────────────────────────────────
def save_iv_snapshot(ticker: str, recorded_date: str, iv_current: float,
iv_30d=None, iv_60d=None, iv_90d=None) -> None:
conn = get_conn()
conn.execute(
"""INSERT OR REPLACE INTO iv_history
(ticker, recorded_date, iv_current, iv_30d, iv_60d, iv_90d)
VALUES (?, ?, ?, ?, ?, ?)""",
(ticker.upper(), recorded_date, iv_current, iv_30d, iv_60d, iv_90d),
)
conn.commit()
conn.close()
def get_iv_rank_percentile(ticker: str, current_iv: float, days: int = 252) -> Dict[str, Any]:
conn = get_conn()
rows = conn.execute(
"""SELECT iv_current FROM iv_history
WHERE ticker=? AND iv_current IS NOT NULL AND iv_current > 0
ORDER BY recorded_date DESC LIMIT ?""",
(ticker.upper(), days),
).fetchall()
conn.close()
if not rows:
return {"iv_rank": None, "iv_percentile": None, "history_days": 0}
hist = [r["iv_current"] for r in rows]
iv_min = min(hist)
iv_max = max(hist)
iv_rank = (
round((current_iv - iv_min) / (iv_max - iv_min) * 100, 1)
if iv_max > iv_min else 50.0
)
iv_percentile = round(sum(1 for v in hist if v < current_iv) / len(hist) * 100, 1)
return {
"iv_rank": iv_rank,
"iv_percentile": iv_percentile,
"history_days": len(hist),
"iv_min_52w": round(iv_min * 100, 1),
"iv_max_52w": round(iv_max * 100, 1),
}
def get_iv_history(ticker: str, days: int = 90) -> List[Dict]:
conn = get_conn()
rows = conn.execute(
"""SELECT recorded_date, iv_current, iv_30d, iv_60d, iv_90d
FROM iv_history WHERE ticker=? AND iv_current IS NOT NULL
ORDER BY recorded_date DESC LIMIT ?""",
(ticker.upper(), days),
).fetchall()
conn.close()
return [dict(r) for r in rows]