diff --git a/backend/routers/instruments_watchlist.py b/backend/routers/instruments_watchlist.py
index e200dc4..8647e1a 100644
--- a/backend/routers/instruments_watchlist.py
+++ b/backend/routers/instruments_watchlist.py
@@ -26,14 +26,16 @@ def list_watchlist():
@router.get("/quotes")
def watchlist_quotes():
from services.database import get_instruments_watchlist
- from services.data_fetcher import get_quote
+ from services.data_fetcher import get_quote_with_volatility
items = []
for row in get_instruments_watchlist():
- q = get_quote(row["ticker"]) or {}
+ q = get_quote_with_volatility(row["ticker"]) or {}
items.append({
**row,
"price": q.get("price"),
"change_pct": q.get("change_pct"),
+ "volatility_pct": q.get("volatility_pct"),
+ "volatility_change_pct": q.get("volatility_change_pct"),
})
return {"items": items}
@@ -78,3 +80,19 @@ def reorder(body: ReorderBody):
from services.database import reorder_instruments_watchlist
reorder_instruments_watchlist(body.tickers)
return {"ok": True}
+
+
+class SaxoLinkBody(BaseModel):
+ saxo_symbol: str | None = None
+
+
+@router.put("/{ticker}/saxo-link")
+def set_saxo_link(ticker: str, body: SaxoLinkBody):
+ """Link this tracked instrument to a Saxo watchlist symbol (or pass null to unlink)
+ so Options Lab's Saxo section shows broker data for it. Adds the symbol to
+ services.saxo_scheduler's watchlist automatically if it wasn't already there."""
+ from services.database import set_instrument_watchlist_saxo_symbol
+ ok = set_instrument_watchlist_saxo_symbol(ticker, body.saxo_symbol)
+ if not ok:
+ raise HTTPException(404, f"'{ticker}' is not in the instruments watchlist")
+ return {"ticker": ticker.strip().upper(), "saxo_symbol": (body.saxo_symbol or "").strip().upper() or None}
diff --git a/backend/services/data_fetcher.py b/backend/services/data_fetcher.py
index 07d6b6b..2052255 100644
--- a/backend/services/data_fetcher.py
+++ b/backend/services/data_fetcher.py
@@ -132,6 +132,60 @@ def get_quote(symbol: str) -> Optional[Dict[str, Any]]:
return {"symbol": symbol, "price": None, "error": "no data"}
+def get_quote_with_volatility(symbol: str, vol_window: int = 20) -> Optional[Dict[str, Any]]:
+ """Like get_quote(), plus a realized volatility overlay: annualized %, rolling
+ `vol_window`-day stddev of log returns — same formula as the Instrument Analysis
+ chart's volatility overlay (services.instrument_service). Needs more history than
+ get_quote()'s 5d/1mo window, so it's kept as a separate function rather than slowing
+ down get_quote()'s many other callers that don't need volatility."""
+ import numpy as np
+
+ for period in ("3mo", "6mo"):
+ try:
+ ticker = yf.Ticker(symbol)
+ hist = ticker.history(period=period, interval="1d", auto_adjust=True)
+ if hist.empty:
+ continue
+ hist = hist.dropna(subset=["Close"])
+ if len(hist) < vol_window + 2:
+ continue
+
+ close = hist["Close"]
+ price = float(close.iloc[-1])
+ last_date = hist.index[-1].date()
+ prior_rows = hist[hist.index.date < last_date]
+ prev = float(prior_rows["Close"].iloc[-1]) if not prior_rows.empty else price
+ change = price - prev
+ change_pct = (change / prev * 100) if prev else 0
+
+ log_ret = np.log(close / close.shift(1))
+ vol_series = (log_ret.rolling(vol_window).std() * np.sqrt(252) * 100).dropna()
+ if vol_series.empty:
+ continue
+ volatility_pct = float(vol_series.iloc[-1])
+
+ # D-1 vol: same explicit date-comparison approach as the price above, not
+ # just "the point before last" (guards the same near-24h double-row case).
+ volatility_change_pct = None
+ prior_vol = vol_series[vol_series.index.date < last_date]
+ if not prior_vol.empty:
+ volatility_change_pct = round(volatility_pct - float(prior_vol.iloc[-1]), 2)
+
+ return {
+ "symbol": symbol,
+ "price": round(price, 4),
+ "change": round(change, 4),
+ "change_pct": round(change_pct, 2),
+ "volatility_pct": round(volatility_pct, 2),
+ "volatility_change_pct": volatility_change_pct,
+ "volume": int(hist["Volume"].iloc[-1]) if "Volume" in hist.columns else 0,
+ "timestamp": datetime.utcnow().isoformat(),
+ }
+ except Exception:
+ continue
+ return {"symbol": symbol, "price": None, "error": "no data"}
+
+
def get_all_quotes() -> Dict[str, List[Dict[str, Any]]]:
result = {}
for asset_class, assets in WATCHLIST.items():
diff --git a/backend/services/database.py b/backend/services/database.py
index 685bbc6..30a5655 100644
--- a/backend/services/database.py
+++ b/backend/services/database.py
@@ -183,6 +183,10 @@ def init_db():
sort_order INTEGER DEFAULT 0,
added_at TEXT DEFAULT (datetime('now'))
)""",
+ # Optional link to a Saxo watchlist symbol (services.saxo_scheduler) — lets Options
+ # Lab's Saxo section show only broker data for instruments the user actually tracks
+ # here, instead of an independently-managed Saxo symbol list.
+ "ALTER TABLE instruments_watchlist ADD COLUMN saxo_symbol TEXT",
# Wavelets — saved optimization/simulation runs (ported from InstrumentSimulator's
# WaveletOptimizationRun: form/results are free-form JSON blobs, not modeled relationally)
"""CREATE TABLE IF NOT EXISTS wavelet_simulations (
@@ -3270,13 +3274,40 @@ def remove_market_custom_ticker(ticker: str) -> bool:
def get_instruments_watchlist() -> List[Dict]:
conn = get_conn()
rows = conn.execute(
- "SELECT ticker, name, asset_class, sort_order, added_at "
+ "SELECT ticker, name, asset_class, sort_order, added_at, saxo_symbol "
"FROM instruments_watchlist ORDER BY sort_order ASC, added_at ASC"
).fetchall()
conn.close()
return [dict(r) for r in rows]
+def set_instrument_watchlist_saxo_symbol(ticker: str, saxo_symbol: Optional[str]) -> bool:
+ """Link (or unlink, if saxo_symbol is None/empty) a tracked instrument to a Saxo
+ watchlist symbol. Linking also adds that symbol to services.saxo_scheduler's own
+ watchlist if it isn't there yet, so it actually starts getting snapshotted. Unlinking
+ does NOT remove it from the Saxo watchlist — it may still be wanted directly, or by
+ another linked instrument; remove it from Config -> Saxo if it's truly no longer needed."""
+ from services.saxo_scheduler import get_watchlist, set_watchlist
+
+ ticker = ticker.upper()
+ saxo_symbol = (saxo_symbol or "").strip().upper() or None
+
+ conn = get_conn()
+ row = conn.execute("SELECT ticker FROM instruments_watchlist WHERE ticker = ?", (ticker,)).fetchone()
+ if row is None:
+ conn.close()
+ return False
+ conn.execute("UPDATE instruments_watchlist SET saxo_symbol = ? WHERE ticker = ?", (saxo_symbol, ticker))
+ conn.commit()
+ conn.close()
+
+ if saxo_symbol:
+ current = get_watchlist()
+ if saxo_symbol not in current:
+ set_watchlist(current + [saxo_symbol])
+ return True
+
+
def add_instrument_watchlist(ticker: str, name: str = "", asset_class: str = "unknown") -> bool:
conn = get_conn()
try:
diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts
index 3f350a4..201b131 100644
--- a/frontend/src/hooks/useApi.ts
+++ b/frontend/src/hooks/useApi.ts
@@ -151,6 +151,20 @@ export const useRemoveWatchlistInstrument = () => {
})
}
+export const useSetWatchlistSaxoLink = () => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: ({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: string | null }) =>
+ api.put(`/watchlist/${encodeURIComponent(ticker)}/saxo-link`, { saxo_symbol: saxoSymbol }).then(r => r.data),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['instruments-watchlist'] })
+ qc.invalidateQueries({ queryKey: ['instruments-watchlist-quotes'] })
+ qc.invalidateQueries({ queryKey: ['saxo-iv-watchlist'] })
+ qc.invalidateQueries({ queryKey: ['saxo-watchlist'] })
+ },
+ })
+}
+
// Latest cycle report — shares the 'cycle-report-latest' queryKey with RapportIA.tsx
export const useLatestCycleReport = () =>
useQuery({
diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx
index e52cfb7..804d769 100644
--- a/frontend/src/pages/Config.tsx
+++ b/frontend/src/pages/Config.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
-import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useCycleStepCatalog, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, validateTicker, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, type CycleStepDef } from '../hooks/useApi'
+import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useCycleStepCatalog, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, useSetWatchlistSaxoLink, validateTicker, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, type CycleStepDef } from '../hooks/useApi'
import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign, TrendingUp, ShieldAlert, DatabaseBackup, Radar, Link2, Unlink, Camera, ShieldCheck, ExternalLink } from 'lucide-react'
import clsx from 'clsx'
@@ -331,6 +331,54 @@ function RiskProfilesCard() {
)
}
+// Inline Saxo-symbol link editor for one watchlist row — lets Options Lab's Saxo section
+// show only broker data for instruments actually tracked here, instead of relying on an
+// independently-managed Saxo watchlist. Linking auto-adds the symbol to the Saxo
+// scheduler's watchlist (Config -> Saxo below) so it starts getting snapshotted.
+function SaxoLinkPicker({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: string | null }) {
+ const [editing, setEditing] = useState(false)
+ const [value, setValue] = useState(saxoSymbol ?? '')
+ const { mutate: setLink, isPending } = useSetWatchlistSaxoLink()
+ const { data: catalogMatches } = useSaxoCatalog(undefined, value.length >= 2 ? value : undefined)
+ const datalistId = `saxo-symbols-${ticker}`
+
+ const save = () => {
+ const sym = value.trim().toUpperCase() || null
+ setLink({ ticker, saxoSymbol: sym }, { onSuccess: () => setEditing(false) })
+ }
+
+ if (!editing) {
+ return saxoSymbol ? (
+ setEditing(true)} className="flex items-center gap-1 text-[9px] text-emerald-400 hover:text-emerald-300 bg-emerald-900/20 border border-emerald-700/30 px-1.5 py-0.5 rounded">
+ {saxoSymbol}
+
+ ) : (
+ setEditing(true)} className="flex items-center gap-1 text-[9px] text-slate-500 hover:text-slate-300 border border-slate-700/40 px-1.5 py-0.5 rounded">
+ Link Saxo
+
+ )
+ }
+
+ return (
+
+ setValue(e.target.value)}
+ onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') setEditing(false) }}
+ onBlur={save}
+ placeholder="Saxo symbol"
+ className="bg-dark-800 border border-slate-700/40 rounded px-1.5 py-0.5 text-[9px] text-white w-24 focus:outline-none focus:border-blue-500/50"
+ />
+
+ {(catalogMatches ?? []).map((c: any) => {c.description} )}
+
+ {isPending && … }
+
+ )
+}
+
function WatchlistCard() {
const { data: items, isLoading } = useInstrumentsWatchlist()
const { mutateAsync: addTicker, isPending: adding } = useAddWatchlistInstrument()
@@ -399,9 +447,12 @@ function WatchlistCard() {
{item.name}
{item.asset_class}
- removeTicker(item.ticker)} className="text-slate-600 hover:text-red-400 transition-colors">
-
-
+
+
+ removeTicker(item.ticker)} className="text-slate-600 hover:text-red-400 transition-colors">
+
+
+
))}
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index 17b0b75..171540c 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -5,10 +5,10 @@ import {
useEcoCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
useTradeMtm, useRiskDashboard, useGeoNews,
useSimPortfolioRisk, useCycleStatus, useClosedTrades,
- useInstrumentsWatchlist, useInstrumentsWatchlistQuotes, useIvBatch, useLatestCycleReport,
+ useInstrumentsWatchlist, useInstrumentsWatchlistQuotes, useSaxoIvWatchlist, useLatestCycleReport,
useWaveletWatchlistSignals,
} from '../hooks/useApi'
-import { Clock, Globe, ShieldAlert, ArrowUpRight, Newspaper, Waves } from 'lucide-react'
+import { Clock, Globe, ShieldAlert, ArrowUpRight, Newspaper, Waves, Link2 } from 'lucide-react'
import { Link } from 'react-router-dom'
import clsx from 'clsx'
import type { Quote } from '../types'
@@ -122,16 +122,18 @@ function EcoEventRow({ ev, showActual }: { ev: any; showActual: boolean }) {
{CURRENCY_FLAGS[ev.currency] ?? ev.currency}
-
{ev.event_name}
- {showActual && ev.actual_value ? (
-
- {ev.actual_value}
- {ev.forecast_value && /{ev.forecast_value} }
-
- ) : ev.forecast_value ? (
-
F {ev.forecast_value}
- ) : null}
- {ev.event_time &&
{ev.event_time} }
+
{ev.event_name}
+
+ {showActual && ev.actual_value ? (
+
+ {ev.actual_value}
+ {ev.forecast_value && /{ev.forecast_value} }
+
+ ) : ev.forecast_value ? (
+ F {ev.forecast_value}
+ ) : null}
+ {ev.event_time && {ev.event_time} }
+
)
}
@@ -154,8 +156,7 @@ export default function Dashboard() {
const { data: watchlistQuotesData } = useInstrumentsWatchlistQuotes()
const { data: latestCycleReportData } = useLatestCycleReport()
const { data: waveletSignalsData } = useWaveletWatchlistSignals()
- const watchlistTickers: string[] = (watchlistItems ?? []).map((w: any) => w.ticker)
- const { data: ivBatchData } = useIvBatch(watchlistTickers.join(','))
+ const { data: saxoIvWatchlistData } = useSaxoIvWatchlist()
// Historical PnL curve + latest VaR snapshot
const { data: pnlHistoryData } = useQuery({
@@ -272,6 +273,11 @@ export default function Dashboard() {
})
}, [watchlistQuotesData])
+ const watchlistAsOf = useMemo(() => {
+ const items: any[] = (watchlistQuotesData as any)?.items ?? []
+ return fmtAsOf(items.map((it: any) => it.timestamp).filter(Boolean).sort().pop())
+ }, [watchlistQuotesData])
+
// FF economic events — today (with actual/forecast as they release) + rest of the
// current week (Mon-Sun), grouped by date. Past events beyond today are dropped —
// the point of this card is "am I up to date", not a historical log.
@@ -412,9 +418,12 @@ export default function Dashboard() {
📡 Watchlist Radar
-
- Manage
-
+
+ {watchlistAsOf &&
MAJ {watchlistAsOf} }
+
+ Manage
+
+
{watchlistRadarData.length > 0 ? (
<>
@@ -435,6 +444,14 @@ export default function Dashboard() {
= 0 ? 'text-emerald-400' : 'text-red-400')}>
{it.change_pct != null ? `${it.change_pct >= 0 ? '+' : ''}${it.change_pct.toFixed(2)}%` : '—'}
+
+ {it.volatility_pct != null ? `σ${it.volatility_pct.toFixed(1)}%` : '—'}
+ {it.volatility_change_pct != null && Math.abs(it.volatility_change_pct) >= 0.1 && (
+ 0 ? 'text-orange-400' : 'text-blue-400'}>
+ {' '}{it.volatility_change_pct >= 0 ? '+' : ''}{it.volatility_change_pct.toFixed(1)}
+
+ )}
+
))}
@@ -501,7 +518,12 @@ export default function Dashboard() {
style={row1Height ? { height: row1Height } : undefined}>
🌐 Macro Regime
-
+
+ {fmtAsOf((macroData as any)?.fetched_at) && (
+
MAJ {fmtAsOf((macroData as any)?.fetched_at)}
+ )}
+
+
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
@@ -945,41 +967,42 @@ export default function Dashboard() {
)
})()}
- {/* Options Lab — IV highlights scoped to the watchlist */}
+ {/* Options Lab — Saxo-only IV highlights, scoped to watchlist instruments that have
+ a Saxo symbol linked (Config -> Instruments Watchlist). Deliberately not the
+ yfinance-based IV batch — matches Options Lab itself, which now only shows Saxo. */}
{(() => {
const watchlist: any[] = watchlistItems ?? []
- const snapshots: Record
= (ivBatchData as any)?.snapshots ?? {}
- const highlights = watchlist
- .map((w: any) => snapshots[w.ticker])
+ const linked = watchlist.filter((w: any) => w.saxo_symbol)
+ const bySaxoSymbol: Record = {}
+ for (const item of ((saxoIvWatchlistData as any)?.items ?? [])) bySaxoSymbol[item.ticker] = item
+ const highlights = linked
+ .map((w: any) => bySaxoSymbol[w.saxo_symbol] ? { ...bySaxoSymbol[w.saxo_symbol], watchlistTicker: w.ticker } : null)
.filter(Boolean)
.sort((a: any, b: any) => Math.abs((b.iv_rank ?? 50) - 50) - Math.abs((a.iv_rank ?? 50) - 50))
- const asOf = fmtAsOf(highlights.map((h: any) => h.fetched_at).filter(Boolean).sort().pop())
return (
🧪 Options Lab
-
- {asOf &&
MAJ {asOf} }
-
+
+ Saxo
- {watchlist.length === 0 ? (
+ {linked.length === 0 ? (
- Add instruments to your watchlist (Config) to see IV highlights
+ Link a watchlist instrument to a Saxo symbol (Config → Instruments Watchlist) to see IV highlights here
) : highlights.length > 0 ? (
{highlights.map((h: any) => {
const rank = h.iv_rank
- const skewPct = h.skew?.skew_pct
const ivCur = h.iv_current_pct
const ivChg = h.iv_change_1d_pct
return (
{rank != null && rank > 80 ? '🔴' : rank != null && rank < 20 ? '🟢' : '⚪'}
- {h.ticker}
+ {h.watchlistTicker}
IVR {rank != null ? rank.toFixed(0) : '—'}
{ivCur != null && · IV {ivCur.toFixed(1)}% }
{ivChg != null && Math.abs(ivChg) >= 0.1 && (
@@ -987,10 +1010,8 @@ export default function Dashboard() {
{ivChg >= 0 ? '+' : ''}{ivChg.toFixed(1)}pt
)}
- {skewPct != null && Math.abs(skewPct) > 3 && (
- 0 ? 'text-orange-400' : 'text-blue-400')}>
- skew {skewPct >= 0 ? '+' : ''}{skewPct.toFixed(1)}
-
+ {h.history_days > 0 && (
+ {h.history_days}d
)}
@@ -998,7 +1019,7 @@ export default function Dashboard() {
})}
) : (
-
Loading IV data…
+
Waiting for Saxo snapshots to accumulate…
)}
)
diff --git a/frontend/src/pages/OptionsLab.tsx b/frontend/src/pages/OptionsLab.tsx
index e8b3c0a..4642d09 100644
--- a/frontend/src/pages/OptionsLab.tsx
+++ b/frontend/src/pages/OptionsLab.tsx
@@ -530,37 +530,44 @@ function WatchlistManager() {
// ── Main page ─────────────────────────────────────────────────────────────────
export default function OptionsLab() {
- const { data, isLoading, refetch, isFetching } = useIvWatchlist()
+ // yfinance-based IV watchlist — disabled per user request (2026-07-21): Options Lab
+ // should only show Saxo broker data now, linked from Config -> Instruments Watchlist.
+ // Kept commented out rather than deleted in case yfinance coverage is wanted again
+ // later (e.g. for symbols with no Saxo link).
+ //
+ // const { data, isLoading, refetch, isFetching } = useIvWatchlist()
+ // const [bootstrapping, setBootstrapping] = useState(false)
+ // const [bootstrapMsg, setBootstrapMsg] = useState
(null)
+ // const items: any[] = data?.items || []
+ //
+ // const sellVol = items.filter(i => (i.iv_rank ?? 50) >= 80)
+ // const buyVol = items.filter(i => i.iv_rank != null && i.iv_rank < 20)
+ // 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 started (~60s) — refresh in 1 minute to see updated IV Ranks.')
+ // setTimeout(() => refetch(), 70_000)
+ // } catch {
+ // setBootstrapMsg('Error during 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
+
const { data: saxoData, isLoading: saxoLoading, refetch: refetchSaxo, isFetching: saxoFetching } = useSaxoIvWatchlist()
- const [bootstrapping, setBootstrapping] = useState(false)
- const [bootstrapMsg, setBootstrapMsg] = useState(null)
- const items: any[] = data?.items || []
const saxoItems: any[] = saxoData?.items || []
- const sellVol = items.filter(i => (i.iv_rank ?? 50) >= 80)
- const buyVol = items.filter(i => i.iv_rank != null && i.iv_rank < 20)
- 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 started (~60s) — refresh in 1 minute to see updated IV Ranks.')
- setTimeout(() => refetch(), 70_000)
- } catch {
- setBootstrapMsg('Error during 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 (
+ {/* yfinance bootstrap banner — disabled along with the yfinance section below.
{needsBootstrap && !bootstrapMsg && (
⚠️ Insufficient IV history — IV Rank stuck at 50%. Bootstrap 1 year of realized data to calibrate the Rank.
@@ -579,6 +586,7 @@ export default function OptionsLab() {
ℹ️ {bootstrapMsg}
)}
+ */}
@@ -586,17 +594,9 @@ export default function OptionsLab() {
Options Lab
- IV Rank · IV Percentile · Term Structure · Skew · Options Flow
+ Saxo broker data · IV Rank · Term Structure · Skew
-
refetch()}
- disabled={isFetching}
- className="flex items-center gap-1.5 text-xs border border-slate-600 text-slate-400 hover:text-slate-200 hover:border-slate-500 px-3 py-1.5 rounded transition-all disabled:opacity-50"
- >
-
- {isFetching ? 'Loading...' : 'Refresh'}
-
{/* Légende */}
@@ -622,6 +622,7 @@ export default function OptionsLab() {
Skew + = puts more expensive than calls (institutional bearish bias)
+ {/* yfinance IV watchlist rows — disabled along with the hooks/state above.
{isLoading ? (
{[...Array(6)].map((_, i) =>
)}
@@ -673,12 +674,13 @@ export default function OptionsLab() {
)}
)}
+ */}
- {/* ── Saxo section — deliberately separate from the yfinance watchlist above.
- Symbols come from the Saxo watchlist (Config → Saxo), not from IV Watchlist
- Manager below — everything here is computed purely from our own accumulated
- saxo_option_snapshots, never blended with yfinance IV. ── */}
-
+ {/* ── Saxo section — the only IV data source now. Symbols come from the Saxo
+ watchlist (Config → Saxo), auto-populated by linking a tracked instrument to a
+ Saxo symbol (Config → Instruments Watchlist). Computed purely from our own
+ accumulated saxo_option_snapshots, never blended with yfinance IV. ── */}
+
@@ -713,7 +715,9 @@ export default function OptionsLab() {
)}
+ {/* yfinance IV Watchlist Manager — disabled along with the section above.
+ */}
)
}