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:
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user