From 3b4e035819da7c90778ec32822bce3cfb32d7276 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Thu, 18 Jun 2026 11:55:57 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20IV=20rank=20fallback=20to=20history=20wh?= =?UTF-8?q?en=20live=20options=20fetch=20fails=20+=20Entr=C3=A9e=20date=20?= =?UTF-8?q?from=20cycle=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iv_engine/options_vol: when get_atm_iv() returns None (yfinance chain unavailable), fall back to most recent iv_history row so IV Rank is always computable from bootstrapped data; live vs history source tagged as iv_source field - Dashboard: build mtmMap from tradeMtmData.trades (trade_entry_prices, cycle auto-log) keyed by pattern_id; getAddedInfo() falls back to mtmMap so Entrée/Durée columns populate automatically after each AI cycle without manual portfolio add - OptionsLab: show '~' prefix and 'IV estimée' label when IV comes from history fallback; fix near-invisible text-slate-700 on 'Sans historique' section header Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/options_vol.py | 16 +++++++++++++--- backend/services/iv_engine.py | 14 ++++++++++++-- frontend/src/pages/Dashboard.tsx | 14 ++++++++++++++ frontend/src/pages/OptionsLab.tsx | 8 ++++---- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/backend/routers/options_vol.py b/backend/routers/options_vol.py index ac1293e..b4503a7 100644 --- a/backend/routers/options_vol.py +++ b/backend/routers/options_vol.py @@ -69,7 +69,7 @@ def get_iv_watchlist(): Returns a summary list sorted by IV Rank descending. """ from services.iv_engine import IV_WATCHLIST, get_atm_iv, _resolve_ticker - from services.database import get_iv_rank_percentile, save_iv_snapshot + from services.database import get_iv_rank_percentile, save_iv_snapshot, get_iv_history from datetime import date results = [] @@ -81,12 +81,21 @@ def get_iv_watchlist(): 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 - proxy = _resolve_ticker(ticker) - save_iv_snapshot(proxy, today, iv) + if live_iv: + save_iv_snapshot(proxy, today, iv) rank_data = get_iv_rank_percentile(proxy, iv) item = { @@ -95,6 +104,7 @@ def get_iv_watchlist(): "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 diff --git a/backend/services/iv_engine.py b/backend/services/iv_engine.py index ebf8502..9be3818 100644 --- a/backend/services/iv_engine.py +++ b/backend/services/iv_engine.py @@ -396,10 +396,19 @@ def get_full_iv_snapshot(ticker: str) -> Dict[str, Any]: skew = get_skew(ticker, target_days=30) flow = get_options_flow(ticker) + live_iv = iv_current is not None + # Fallback: use most recent historical IV when live options fetch fails + if iv_current is None: + from services.database import get_iv_history + recent = get_iv_history(proxy, days=5) + if recent: + iv_current = recent[0]["iv_current"] + rank_data: Dict[str, Any] = {} if iv_current: - # Save to history first, then calculate rank - save_iv_snapshot(proxy, today, iv_current, term.get("iv_30d"), term.get("iv_60d"), term.get("iv_90d")) + if live_iv: + # Only persist to history when we have a fresh live IV + save_iv_snapshot(proxy, today, iv_current, term.get("iv_30d"), term.get("iv_60d"), term.get("iv_90d")) rank_data = get_iv_rank_percentile(proxy, iv_current) return { @@ -415,6 +424,7 @@ def get_full_iv_snapshot(ticker: str) -> Dict[str, Any]: "skew": skew, "options_flow": flow, "fetched_at": datetime.utcnow().isoformat(), + "iv_source": "live" if live_iv else ("history" if iv_current else "none"), } diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index c325c55..26051c1 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -651,6 +651,19 @@ export default function Dashboard() { return map }, [positions]) + // Fallback map from cycle-auto-logged trades (trade_entry_prices), keyed by pattern_id + const mtmMap = useMemo(() => { + const map: Record = {} + for (const t of ((tradeMtmData as any)?.trades ?? [])) { + const pid = t.pattern_id as string + if (!pid) continue + const entry_date = (t.entry_date as string) ?? '' + const expiry_days = (t.horizon_days as number) ?? undefined + if (!map[pid] || entry_date > map[pid].entry_date) map[pid] = { entry_date, expiry_days } + } + return map + }, [tradeMtmData]) + const macroInfo = useMemo(() => { if (!macroData?.scenarios) return null const sc = macroData.scenarios @@ -778,6 +791,7 @@ export default function Dashboard() { return ( addedMap[`trigger:${trigger}:${strategy}`] ?? addedMap[`ticker:${underly}:${strategy}`] ?? + mtmMap[item.patternId] ?? null ) } diff --git a/frontend/src/pages/OptionsLab.tsx b/frontend/src/pages/OptionsLab.tsx index 9bceb21..627c224 100644 --- a/frontend/src/pages/OptionsLab.tsx +++ b/frontend/src/pages/OptionsLab.tsx @@ -200,9 +200,9 @@ function WatchlistRow({ item }: { item: any }) {
- {item.iv_current_pct != null ? `${item.iv_current_pct}%` : '—'} + {item.iv_current_pct != null ? `${item.iv_source === 'history' ? '~' : ''}${item.iv_current_pct}%` : '—'} -
IV actuelle
+
{item.iv_source === 'history' ? 'IV estimée' : 'IV actuelle'}
@@ -229,7 +229,7 @@ function WatchlistRow({ item }: { item: any }) {
)} {item.history_days > 0 && ( -
{item.history_days}j historique
+
{item.history_days}j historique
)} @@ -384,7 +384,7 @@ export default function OptionsLab() { )} {noData.length > 0 && (
-
Sans historique ({noData.length})
+
Sans historique ({noData.length})
{noData.map(item => )}
)}