fix: IV rank fallback to history when live options fetch fails + Entrée date from cycle logs
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@ def get_iv_watchlist():
|
|||||||
Returns a summary list sorted by IV Rank descending.
|
Returns a summary list sorted by IV Rank descending.
|
||||||
"""
|
"""
|
||||||
from services.iv_engine import IV_WATCHLIST, get_atm_iv, _resolve_ticker
|
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
|
from datetime import date
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
@@ -81,12 +81,21 @@ def get_iv_watchlist():
|
|||||||
results.append(_iv_cache[key]["data"])
|
results.append(_iv_cache[key]["data"])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
proxy = _resolve_ticker(ticker)
|
||||||
iv = get_atm_iv(ticker, 30)
|
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:
|
if not iv:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
proxy = _resolve_ticker(ticker)
|
if live_iv:
|
||||||
save_iv_snapshot(proxy, today, iv)
|
save_iv_snapshot(proxy, today, iv)
|
||||||
rank_data = get_iv_rank_percentile(proxy, iv)
|
rank_data = get_iv_rank_percentile(proxy, iv)
|
||||||
|
|
||||||
item = {
|
item = {
|
||||||
@@ -95,6 +104,7 @@ def get_iv_watchlist():
|
|||||||
"iv_rank": rank_data.get("iv_rank"),
|
"iv_rank": rank_data.get("iv_rank"),
|
||||||
"iv_percentile": rank_data.get("iv_percentile"),
|
"iv_percentile": rank_data.get("iv_percentile"),
|
||||||
"history_days": rank_data.get("history_days", 0),
|
"history_days": rank_data.get("history_days", 0),
|
||||||
|
"iv_source": "live" if live_iv else "history",
|
||||||
"signal": (
|
"signal": (
|
||||||
"sell_vol" if (rank_data.get("iv_rank") or 0) > 80
|
"sell_vol" if (rank_data.get("iv_rank") or 0) > 80
|
||||||
else "buy_vol" if (rank_data.get("iv_rank") or 100) < 20
|
else "buy_vol" if (rank_data.get("iv_rank") or 100) < 20
|
||||||
|
|||||||
@@ -396,10 +396,19 @@ def get_full_iv_snapshot(ticker: str) -> Dict[str, Any]:
|
|||||||
skew = get_skew(ticker, target_days=30)
|
skew = get_skew(ticker, target_days=30)
|
||||||
flow = get_options_flow(ticker)
|
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] = {}
|
rank_data: Dict[str, Any] = {}
|
||||||
if iv_current:
|
if iv_current:
|
||||||
# Save to history first, then calculate rank
|
if live_iv:
|
||||||
save_iv_snapshot(proxy, today, iv_current, term.get("iv_30d"), term.get("iv_60d"), term.get("iv_90d"))
|
# 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)
|
rank_data = get_iv_rank_percentile(proxy, iv_current)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -415,6 +424,7 @@ def get_full_iv_snapshot(ticker: str) -> Dict[str, Any]:
|
|||||||
"skew": skew,
|
"skew": skew,
|
||||||
"options_flow": flow,
|
"options_flow": flow,
|
||||||
"fetched_at": datetime.utcnow().isoformat(),
|
"fetched_at": datetime.utcnow().isoformat(),
|
||||||
|
"iv_source": "live" if live_iv else ("history" if iv_current else "none"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -651,6 +651,19 @@ export default function Dashboard() {
|
|||||||
return map
|
return map
|
||||||
}, [positions])
|
}, [positions])
|
||||||
|
|
||||||
|
// Fallback map from cycle-auto-logged trades (trade_entry_prices), keyed by pattern_id
|
||||||
|
const mtmMap = useMemo(() => {
|
||||||
|
const map: Record<string, { entry_date: string; expiry_days?: number }> = {}
|
||||||
|
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(() => {
|
const macroInfo = useMemo(() => {
|
||||||
if (!macroData?.scenarios) return null
|
if (!macroData?.scenarios) return null
|
||||||
const sc = macroData.scenarios
|
const sc = macroData.scenarios
|
||||||
@@ -778,6 +791,7 @@ export default function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
addedMap[`trigger:${trigger}:${strategy}`] ??
|
addedMap[`trigger:${trigger}:${strategy}`] ??
|
||||||
addedMap[`ticker:${underly}:${strategy}`] ??
|
addedMap[`ticker:${underly}:${strategy}`] ??
|
||||||
|
mtmMap[item.patternId] ??
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ function WatchlistRow({ item }: { item: any }) {
|
|||||||
|
|
||||||
<div className="w-16 shrink-0">
|
<div className="w-16 shrink-0">
|
||||||
<span className={clsx('text-sm font-bold font-mono', ivRankColor(item.iv_rank))}>
|
<span className={clsx('text-sm font-bold font-mono', ivRankColor(item.iv_rank))}>
|
||||||
{item.iv_current_pct != null ? `${item.iv_current_pct}%` : '—'}
|
{item.iv_current_pct != null ? `${item.iv_source === 'history' ? '~' : ''}${item.iv_current_pct}%` : '—'}
|
||||||
</span>
|
</span>
|
||||||
<div className="text-[8px] text-slate-600">IV actuelle</div>
|
<div className="text-[8px] text-slate-600">{item.iv_source === 'history' ? 'IV estimée' : 'IV actuelle'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 space-y-1">
|
<div className="flex-1 min-w-0 space-y-1">
|
||||||
@@ -229,7 +229,7 @@ function WatchlistRow({ item }: { item: any }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{item.history_days > 0 && (
|
{item.history_days > 0 && (
|
||||||
<div className="text-[8px] text-slate-700">{item.history_days}j historique</div>
|
<div className="text-[8px] text-slate-500">{item.history_days}j historique</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ export default function OptionsLab() {
|
|||||||
)}
|
)}
|
||||||
{noData.length > 0 && (
|
{noData.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-slate-700 uppercase tracking-wide mb-2">Sans historique ({noData.length})</div>
|
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">Sans historique ({noData.length})</div>
|
||||||
<div className="space-y-1.5">{noData.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
<div className="space-y-1.5">{noData.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user