From 78eda311f8727b350d34d35d0fd1d1de2113f529 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Tue, 21 Jul 2026 17:23:55 +0200 Subject: [PATCH] feat: option lab --- backend/routers/instruments_watchlist.py | 22 +++++- backend/services/data_fetcher.py | 54 ++++++++++++++ backend/services/database.py | 33 ++++++++- frontend/src/hooks/useApi.ts | 14 ++++ frontend/src/pages/Config.tsx | 59 +++++++++++++-- frontend/src/pages/Dashboard.tsx | 91 +++++++++++++++--------- frontend/src/pages/OptionsLab.tsx | 84 +++++++++++----------- 7 files changed, 275 insertions(+), 82 deletions(-) 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 ? ( + + ) : ( + + ) + } + + 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) => )} + + {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} - +
+ + +
))} 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

-
{/* 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. + */}
) }