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

@@ -231,6 +231,25 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
summary["patterns_added"] = added_count
# ── Step 3.5: Collect IV context ─────────────────────────────────────
iv_context = ""
try:
from services.iv_engine import get_iv_context_for_prompt, IV_WATCHLIST
# Collect underlyings from current trade journal + default watchlist
from services.database import get_mtm_trades_with_traces
_mtm = get_mtm_trades_with_traces(days=90)
_trade_tickers = list({
(t.get("underlying") or "").upper()
for t in _mtm.get("all_trades", [])
if t.get("underlying")
})
_iv_tickers = (_trade_tickers + IV_WATCHLIST[:6])[:10]
iv_context = get_iv_context_for_prompt(_iv_tickers)
if iv_context:
logger.info(f"[Cycle {run_id[:16]}] IV context collected for {len(_iv_tickers)} tickers")
except Exception as _e:
logger.warning(f"[Cycle] IV context collection failed (non-blocking): {_e}")
# ── Step 4: Score ALL patterns ────────────────────────────────────────
# Verify all patterns have IDs before scoring (guard against stale data)
patterns_with_id = [p for p in existing if p.get("id")]
@@ -251,6 +270,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
category_filter=None,
macro_regime=macro_regime,
portfolio_lessons=portfolio_lessons,
iv_context=iv_context,
)
scored_with_id = [s for s in scored if s.get("pattern_id")]
scored_without_id = [s for s in scored if not s.get("pattern_id")]