fix: bootstrap IV history with 1y realized vol to unblock IV Rank from 50
IV Rank was stuck at 50 for all tickers because the formula returns 50.0 when iv_max == iv_min (not enough historical snapshots). Added: - bootstrap_iv_history(): downloads 1y of closes per ticker, computes 30d rolling realized vol (annualized), saves each day to iv_history - POST /api/options-vol/bootstrap-history endpoint (runs in background) - OptionsLab: auto-detect when IV Rank needs bootstrapping and show an amber banner with one-click "Initialiser historique" button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useIvWatchlist, useIvSnapshot, useIvHistory } from '../hooks/useApi'
|
||||
import { Activity, TrendingUp, TrendingDown, Minus, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { Activity, TrendingUp, TrendingDown, Minus, RefreshCw, ChevronDown, ChevronUp, Database } from 'lucide-react'
|
||||
import { api } from '../hooks/useApi'
|
||||
import clsx from 'clsx'
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -249,6 +250,8 @@ function WatchlistRow({ item }: { item: any }) {
|
||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||
export default function OptionsLab() {
|
||||
const { data, isLoading, refetch, isFetching } = useIvWatchlist()
|
||||
const [bootstrapping, setBootstrapping] = useState(false)
|
||||
const [bootstrapMsg, setBootstrapMsg] = useState<string | null>(null)
|
||||
const items: any[] = data?.items || []
|
||||
|
||||
const sellVol = items.filter(i => (i.iv_rank ?? 50) >= 80)
|
||||
@@ -256,8 +259,44 @@ export default function OptionsLab() {
|
||||
const neutral = items.filter(i => i.iv_rank != null && i.iv_rank >= 20 && i.iv_rank < 80)
|
||||
const noData = items.filter(i => i.iv_rank == null)
|
||||
|
||||
const handleBootstrap = async () => {
|
||||
setBootstrapping(true)
|
||||
setBootstrapMsg(null)
|
||||
try {
|
||||
await api.post('/options-vol/bootstrap-history')
|
||||
setBootstrapMsg('Bootstrap lancé (~60s) — actualise dans 1 minute pour voir les IV Rank mis à jour.')
|
||||
setTimeout(() => refetch(), 70_000)
|
||||
} catch {
|
||||
setBootstrapMsg('Erreur lors du bootstrap.')
|
||||
} finally {
|
||||
setBootstrapping(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Show bootstrap banner if most items have no meaningful IV Rank (stuck at 50 or null)
|
||||
const needsBootstrap = items.length > 0 && items.filter(i => i.iv_rank == null || i.iv_rank === 50).length > items.length * 0.6
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-5">
|
||||
{needsBootstrap && !bootstrapMsg && (
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-3 rounded border border-amber-700/40 bg-amber-900/10 text-xs text-amber-300">
|
||||
<span>⚠️ Historique IV insuffisant — IV Rank bloqué à 50%. Bootstrappe 1 an de données réalisées pour calibrer le Rank.</span>
|
||||
<button
|
||||
onClick={handleBootstrap}
|
||||
disabled={bootstrapping}
|
||||
className="flex items-center gap-1.5 text-xs bg-amber-700/30 hover:bg-amber-700/50 border border-amber-600/50 text-amber-200 px-3 py-1.5 rounded transition-all whitespace-nowrap disabled:opacity-50"
|
||||
>
|
||||
<Database className={clsx('w-3.5 h-3.5', bootstrapping && 'animate-pulse')} />
|
||||
{bootstrapping ? 'Chargement...' : 'Initialiser historique'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{bootstrapMsg && (
|
||||
<div className="px-4 py-3 rounded border border-blue-700/40 bg-blue-900/10 text-xs text-blue-300">
|
||||
ℹ️ {bootstrapMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user