feat: saxo price
This commit is contained in:
@@ -49,8 +49,8 @@ def watchlist_quotes():
|
|||||||
items = []
|
items = []
|
||||||
for row in get_instruments_watchlist():
|
for row in get_instruments_watchlist():
|
||||||
q = None
|
q = None
|
||||||
if row.get("saxo_symbol"):
|
if row.get("saxo_quote_symbol"):
|
||||||
q = _saxo_quote(row["saxo_symbol"])
|
q = _saxo_quote(row["saxo_quote_symbol"])
|
||||||
if q is None:
|
if q is None:
|
||||||
q = get_quote_with_volatility(row["ticker"]) or {}
|
q = get_quote_with_volatility(row["ticker"]) or {}
|
||||||
items.append({
|
items.append({
|
||||||
@@ -110,13 +110,27 @@ class SaxoLinkBody(BaseModel):
|
|||||||
saxo_symbol: str | None = None
|
saxo_symbol: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{ticker}/saxo-link")
|
@router.put("/{ticker}/saxo-option-link")
|
||||||
def set_saxo_link(ticker: str, body: SaxoLinkBody):
|
def set_saxo_option_link(ticker: str, body: SaxoLinkBody):
|
||||||
"""Link this tracked instrument to a Saxo watchlist symbol (or pass null to unlink)
|
"""Link this tracked instrument to the Saxo symbol whose OPTIONS CHAIN Options Lab
|
||||||
so Options Lab's Saxo section shows broker data for it. Adds the symbol to
|
should analyze for it (or pass null to unlink) — e.g. CL=F -> MCL:XCME. Adds the
|
||||||
services.saxo_scheduler's watchlist automatically if it wasn't already there."""
|
symbol to services.saxo_scheduler's watchlist automatically if it wasn't already
|
||||||
from services.database import set_instrument_watchlist_saxo_symbol
|
there, so that chain starts getting snapshotted."""
|
||||||
ok = set_instrument_watchlist_saxo_symbol(ticker, body.saxo_symbol)
|
from services.database import set_instrument_watchlist_saxo_option_symbol
|
||||||
|
ok = set_instrument_watchlist_saxo_option_symbol(ticker, body.saxo_symbol)
|
||||||
if not ok:
|
if not ok:
|
||||||
raise HTTPException(404, f"'{ticker}' is not in the instruments watchlist")
|
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}
|
return {"ticker": ticker.strip().upper(), "saxo_option_symbol": (body.saxo_symbol or "").strip().upper() or None}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{ticker}/saxo-quote-link")
|
||||||
|
def set_saxo_quote_link(ticker: str, body: SaxoLinkBody):
|
||||||
|
"""Link this tracked instrument to the Saxo symbol used to price it in the Cockpit
|
||||||
|
(or pass null to unlink) — an accurate broker spot/futures feed instead of
|
||||||
|
yfinance's unadjusted continuous-futures tickers. Independent of the options link
|
||||||
|
above; often a different Saxo instrument."""
|
||||||
|
from services.database import set_instrument_watchlist_saxo_quote_symbol
|
||||||
|
ok = set_instrument_watchlist_saxo_quote_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_quote_symbol": (body.saxo_symbol or "").strip().upper() or None}
|
||||||
|
|||||||
@@ -183,10 +183,21 @@ def init_db():
|
|||||||
sort_order INTEGER DEFAULT 0,
|
sort_order INTEGER DEFAULT 0,
|
||||||
added_at TEXT DEFAULT (datetime('now'))
|
added_at TEXT DEFAULT (datetime('now'))
|
||||||
)""",
|
)""",
|
||||||
# Optional link to a Saxo watchlist symbol (services.saxo_scheduler) — lets Options
|
# Two DISTINCT, independent Saxo links per tracked instrument — often different
|
||||||
# Lab's Saxo section show only broker data for instruments the user actually tracks
|
# Saxo instruments, not one shared symbol:
|
||||||
# here, instead of an independently-managed Saxo symbol list.
|
# saxo_option_symbol — which Saxo options chain Options Lab should analyze for
|
||||||
|
# this underlying (e.g. CL=F -> MCL:XCME, a micro futures option chain).
|
||||||
|
# Feeds services.saxo_scheduler's watchlist so that chain gets snapshotted.
|
||||||
|
# saxo_quote_symbol — which Saxo spot/futures instrument to price this
|
||||||
|
# instrument FROM in the Cockpit, instead of yfinance's unadjusted continuous-
|
||||||
|
# futures tickers (BZ=F, CL=F... see project_saxo_quote_source memory).
|
||||||
|
# `saxo_symbol` (original single field, 2026-07-21) is superseded by
|
||||||
|
# saxo_option_symbol below — kept as a column (not dropped) with its data copied
|
||||||
|
# forward, so links set before the split aren't lost.
|
||||||
"ALTER TABLE instruments_watchlist ADD COLUMN saxo_symbol TEXT",
|
"ALTER TABLE instruments_watchlist ADD COLUMN saxo_symbol TEXT",
|
||||||
|
"ALTER TABLE instruments_watchlist ADD COLUMN saxo_option_symbol TEXT",
|
||||||
|
"ALTER TABLE instruments_watchlist ADD COLUMN saxo_quote_symbol TEXT",
|
||||||
|
"UPDATE instruments_watchlist SET saxo_option_symbol = saxo_symbol WHERE saxo_option_symbol IS NULL AND saxo_symbol IS NOT NULL",
|
||||||
# Wavelets — saved optimization/simulation runs (ported from InstrumentSimulator's
|
# Wavelets — saved optimization/simulation runs (ported from InstrumentSimulator's
|
||||||
# WaveletOptimizationRun: form/results are free-form JSON blobs, not modeled relationally)
|
# WaveletOptimizationRun: form/results are free-form JSON blobs, not modeled relationally)
|
||||||
"""CREATE TABLE IF NOT EXISTS wavelet_simulations (
|
"""CREATE TABLE IF NOT EXISTS wavelet_simulations (
|
||||||
@@ -3274,19 +3285,22 @@ def remove_market_custom_ticker(ticker: str) -> bool:
|
|||||||
def get_instruments_watchlist() -> List[Dict]:
|
def get_instruments_watchlist() -> List[Dict]:
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT ticker, name, asset_class, sort_order, added_at, saxo_symbol "
|
"SELECT ticker, name, asset_class, sort_order, added_at, saxo_option_symbol, saxo_quote_symbol "
|
||||||
"FROM instruments_watchlist ORDER BY sort_order ASC, added_at ASC"
|
"FROM instruments_watchlist ORDER BY sort_order ASC, added_at ASC"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
def set_instrument_watchlist_saxo_symbol(ticker: str, saxo_symbol: Optional[str]) -> bool:
|
def set_instrument_watchlist_saxo_option_symbol(ticker: str, saxo_symbol: Optional[str]) -> bool:
|
||||||
"""Link (or unlink, if saxo_symbol is None/empty) a tracked instrument to a Saxo
|
"""Link (or unlink, if saxo_symbol is None/empty) a tracked instrument to the Saxo
|
||||||
watchlist symbol. Linking also adds that symbol to services.saxo_scheduler's own
|
symbol whose OPTIONS CHAIN Options Lab should analyze for it — e.g. CL=F -> MCL:XCME
|
||||||
watchlist if it isn't there yet, so it actually starts getting snapshotted. Unlinking
|
(a micro futures option chain), often NOT the same Saxo instrument as
|
||||||
does NOT remove it from the Saxo watchlist — it may still be wanted directly, or by
|
saxo_quote_symbol (the underlying's own spot/futures price feed). Linking also adds
|
||||||
another linked instrument; remove it from Config -> Saxo if it's truly no longer needed."""
|
the symbol to services.saxo_scheduler's own watchlist if it isn't there yet, so its
|
||||||
|
option chain 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
|
from services.saxo_scheduler import get_watchlist, set_watchlist
|
||||||
|
|
||||||
ticker = ticker.upper()
|
ticker = ticker.upper()
|
||||||
@@ -3297,7 +3311,7 @@ def set_instrument_watchlist_saxo_symbol(ticker: str, saxo_symbol: Optional[str]
|
|||||||
if row is None:
|
if row is None:
|
||||||
conn.close()
|
conn.close()
|
||||||
return False
|
return False
|
||||||
conn.execute("UPDATE instruments_watchlist SET saxo_symbol = ? WHERE ticker = ?", (saxo_symbol, ticker))
|
conn.execute("UPDATE instruments_watchlist SET saxo_option_symbol = ? WHERE ticker = ?", (saxo_symbol, ticker))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -3308,6 +3322,25 @@ def set_instrument_watchlist_saxo_symbol(ticker: str, saxo_symbol: Optional[str]
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def set_instrument_watchlist_saxo_quote_symbol(ticker: str, saxo_symbol: Optional[str]) -> bool:
|
||||||
|
"""Link (or unlink) a tracked instrument to the Saxo symbol used to price it in the
|
||||||
|
Cockpit (services.saxo_client.get_saxo_quote_with_volatility), instead of yfinance's
|
||||||
|
unadjusted continuous-futures tickers. No scheduler sync needed — chart bars are
|
||||||
|
pulled live from Saxo per request, nothing to pre-accumulate."""
|
||||||
|
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_quote_symbol = ? WHERE ticker = ?", (saxo_symbol, ticker))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def add_instrument_watchlist(ticker: str, name: str = "", asset_class: str = "unknown") -> bool:
|
def add_instrument_watchlist(ticker: str, name: str = "", asset_class: str = "unknown") -> bool:
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -151,11 +151,14 @@ export const useRemoveWatchlistInstrument = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSetWatchlistSaxoLink = () => {
|
// Which Saxo OPTIONS CHAIN Options Lab should analyze for this underlying (e.g.
|
||||||
|
// CL=F -> MCL:XCME) — independent of useSetWatchlistSaxoQuoteLink below, often a
|
||||||
|
// different Saxo instrument.
|
||||||
|
export const useSetWatchlistSaxoOptionLink = () => {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: string | null }) =>
|
mutationFn: ({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: string | null }) =>
|
||||||
api.put(`/watchlist/${encodeURIComponent(ticker)}/saxo-link`, { saxo_symbol: saxoSymbol }).then(r => r.data),
|
api.put(`/watchlist/${encodeURIComponent(ticker)}/saxo-option-link`, { saxo_symbol: saxoSymbol }).then(r => r.data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: ['instruments-watchlist'] })
|
qc.invalidateQueries({ queryKey: ['instruments-watchlist'] })
|
||||||
qc.invalidateQueries({ queryKey: ['instruments-watchlist-quotes'] })
|
qc.invalidateQueries({ queryKey: ['instruments-watchlist-quotes'] })
|
||||||
@@ -165,6 +168,20 @@ export const useSetWatchlistSaxoLink = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which Saxo spot/futures symbol prices this instrument in the Cockpit, instead of
|
||||||
|
// yfinance's unadjusted continuous-futures tickers.
|
||||||
|
export const useSetWatchlistSaxoQuoteLink = () => {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: string | null }) =>
|
||||||
|
api.put(`/watchlist/${encodeURIComponent(ticker)}/saxo-quote-link`, { saxo_symbol: saxoSymbol }).then(r => r.data),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['instruments-watchlist'] })
|
||||||
|
qc.invalidateQueries({ queryKey: ['instruments-watchlist-quotes'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Latest cycle report — shares the 'cycle-report-latest' queryKey with RapportIA.tsx
|
// Latest cycle report — shares the 'cycle-report-latest' queryKey with RapportIA.tsx
|
||||||
export const useLatestCycleReport = () =>
|
export const useLatestCycleReport = () =>
|
||||||
useQuery({
|
useQuery({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
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, useSetWatchlistSaxoLink, 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, useSetWatchlistSaxoOptionLink, useSetWatchlistSaxoQuoteLink, 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 { 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'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
@@ -331,16 +331,27 @@ function RiskProfilesCard() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline Saxo-symbol link editor for one watchlist row — lets Options Lab's Saxo section
|
// Inline Saxo-symbol link editor for one watchlist row. Two independent kinds, often
|
||||||
// show only broker data for instruments actually tracked here, instead of relying on an
|
// pointing at DIFFERENT Saxo instruments for the same tracked ticker:
|
||||||
// independently-managed Saxo watchlist. Linking auto-adds the symbol to the Saxo
|
// 'option' — which Saxo options chain Options Lab should analyze (e.g. CL=F -> the
|
||||||
// scheduler's watchlist (Config -> Saxo below) so it starts getting snapshotted.
|
// Saxo micro-futures option chain MCL:XCME). Auto-adds to the Saxo
|
||||||
function SaxoLinkPicker({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: string | null }) {
|
// scheduler's watchlist (Config -> Saxo below) so it gets snapshotted.
|
||||||
|
// 'quote' — which Saxo spot/futures symbol prices this instrument in the Cockpit,
|
||||||
|
// replacing yfinance's unadjusted continuous-futures tickers.
|
||||||
|
const SAXO_LINK_KIND_META = {
|
||||||
|
option: { label: 'Option', placeholder: 'Search Saxo option symbol…', activeCls: 'text-emerald-400 hover:text-emerald-300 bg-emerald-900/20 border-emerald-700/30' },
|
||||||
|
quote: { label: 'Quote', placeholder: 'Search Saxo spot/futures symbol…', activeCls: 'text-sky-400 hover:text-sky-300 bg-sky-900/20 border-sky-700/30' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
function SaxoLinkPicker({ ticker, kind, saxoSymbol }: { ticker: string; kind: keyof typeof SAXO_LINK_KIND_META; saxoSymbol: string | null }) {
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [value, setValue] = useState(saxoSymbol ?? '')
|
const [value, setValue] = useState(saxoSymbol ?? '')
|
||||||
const [showDropdown, setShowDropdown] = useState(false)
|
const [showDropdown, setShowDropdown] = useState(false)
|
||||||
const { mutate: setLink, isPending } = useSetWatchlistSaxoLink()
|
const optionLink = useSetWatchlistSaxoOptionLink()
|
||||||
|
const quoteLink = useSetWatchlistSaxoQuoteLink()
|
||||||
|
const { mutate: setLink, isPending } = kind === 'option' ? optionLink : quoteLink
|
||||||
const { data: catalogMatches } = useSaxoCatalog(undefined, value.length >= 2 ? value : undefined)
|
const { data: catalogMatches } = useSaxoCatalog(undefined, value.length >= 2 ? value : undefined)
|
||||||
|
const meta = SAXO_LINK_KIND_META[kind]
|
||||||
|
|
||||||
const save = (sym?: string) => {
|
const save = (sym?: string) => {
|
||||||
const resolved = (sym ?? value).trim().toUpperCase() || null
|
const resolved = (sym ?? value).trim().toUpperCase() || null
|
||||||
@@ -349,12 +360,12 @@ function SaxoLinkPicker({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: st
|
|||||||
|
|
||||||
if (!editing) {
|
if (!editing) {
|
||||||
return saxoSymbol ? (
|
return saxoSymbol ? (
|
||||||
<button onClick={() => { setEditing(true); setValue(saxoSymbol) }} 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">
|
<button onClick={() => { setEditing(true); setValue(saxoSymbol) }} className={clsx('flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border', meta.activeCls)}>
|
||||||
<Link2 className="w-2.5 h-2.5" /> {saxoSymbol}
|
<Link2 className="w-2.5 h-2.5" /> {meta.label}: {saxoSymbol}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={() => 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">
|
<button onClick={() => 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">
|
||||||
<Unlink className="w-2.5 h-2.5" /> Link Saxo
|
<Unlink className="w-2.5 h-2.5" /> {meta.label}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -368,7 +379,7 @@ function SaxoLinkPicker({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: st
|
|||||||
onFocus={() => setShowDropdown(true)}
|
onFocus={() => setShowDropdown(true)}
|
||||||
onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') setEditing(false) }}
|
onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') setEditing(false) }}
|
||||||
onBlur={() => save()}
|
onBlur={() => save()}
|
||||||
placeholder="Search Saxo symbol…"
|
placeholder={meta.placeholder}
|
||||||
className="bg-dark-800 border border-slate-700/40 rounded px-1.5 py-0.5 text-[9px] text-white w-28 focus:outline-none focus:border-blue-500/50"
|
className="bg-dark-800 border border-slate-700/40 rounded px-1.5 py-0.5 text-[9px] text-white w-28 focus:outline-none focus:border-blue-500/50"
|
||||||
/>
|
/>
|
||||||
{isPending && <span className="text-[9px] text-slate-600 ml-1">…</span>}
|
{isPending && <span className="text-[9px] text-slate-600 ml-1">…</span>}
|
||||||
@@ -461,7 +472,8 @@ function WatchlistCard() {
|
|||||||
<span className="text-[9px] text-slate-700 bg-dark-700 px-1.5 py-0.5 rounded capitalize">{item.asset_class}</span>
|
<span className="text-[9px] text-slate-700 bg-dark-700 px-1.5 py-0.5 rounded capitalize">{item.asset_class}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<SaxoLinkPicker ticker={item.ticker} saxoSymbol={item.saxo_symbol ?? null} />
|
<SaxoLinkPicker ticker={item.ticker} kind="option" saxoSymbol={item.saxo_option_symbol ?? null} />
|
||||||
|
<SaxoLinkPicker ticker={item.ticker} kind="quote" saxoSymbol={item.saxo_quote_symbol ?? null} />
|
||||||
<button onClick={() => removeTicker(item.ticker)} className="text-slate-600 hover:text-red-400 transition-colors">
|
<button onClick={() => removeTicker(item.ticker)} className="text-slate-600 hover:text-red-400 transition-colors">
|
||||||
<X className="w-3.5 h-3.5" />
|
<X className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -971,15 +971,17 @@ export default function Dashboard() {
|
|||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Options Lab — Saxo-only IV highlights, scoped to watchlist instruments that have
|
{/* Options Lab — Saxo-only IV highlights, scoped to watchlist instruments that have
|
||||||
a Saxo symbol linked (Config -> Instruments Watchlist). Deliberately not the
|
a Saxo OPTION symbol linked (Config -> Instruments Watchlist, "Option" picker —
|
||||||
yfinance-based IV batch — matches Options Lab itself, which now only shows Saxo. */}
|
distinct from the "Quote" picker used for Watchlist Radar's price/vol source).
|
||||||
|
Deliberately not the yfinance-based IV batch — matches Options Lab itself,
|
||||||
|
which now only shows Saxo. */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const watchlist: any[] = watchlistItems ?? []
|
const watchlist: any[] = watchlistItems ?? []
|
||||||
const linked = watchlist.filter((w: any) => w.saxo_symbol)
|
const linked = watchlist.filter((w: any) => w.saxo_option_symbol)
|
||||||
const bySaxoSymbol: Record<string, any> = {}
|
const bySaxoSymbol: Record<string, any> = {}
|
||||||
for (const item of ((saxoIvWatchlistData as any)?.items ?? [])) bySaxoSymbol[item.ticker] = item
|
for (const item of ((saxoIvWatchlistData as any)?.items ?? [])) bySaxoSymbol[item.ticker] = item
|
||||||
const highlights = linked
|
const highlights = linked
|
||||||
.map((w: any) => bySaxoSymbol[w.saxo_symbol] ? { ...bySaxoSymbol[w.saxo_symbol], watchlistTicker: w.ticker } : null)
|
.map((w: any) => bySaxoSymbol[w.saxo_option_symbol] ? { ...bySaxoSymbol[w.saxo_option_symbol], watchlistTicker: w.ticker } : null)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.sort((a: any, b: any) => Math.abs((b.iv_rank ?? 50) - 50) - Math.abs((a.iv_rank ?? 50) - 50))
|
.sort((a: any, b: any) => Math.abs((b.iv_rank ?? 50) - 50) - Math.abs((a.iv_rank ?? 50) - 50))
|
||||||
|
|
||||||
@@ -993,7 +995,7 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
{linked.length === 0 ? (
|
{linked.length === 0 ? (
|
||||||
<div className="text-[10px] text-slate-600 mt-1">
|
<div className="text-[10px] text-slate-600 mt-1">
|
||||||
Link a watchlist instrument to a Saxo symbol (Config → Instruments Watchlist) to see IV highlights here
|
Link a watchlist instrument to a Saxo option symbol (Config → Instruments Watchlist → "Option") to see IV highlights here
|
||||||
</div>
|
</div>
|
||||||
) : highlights.length > 0 ? (
|
) : highlights.length > 0 ? (
|
||||||
<div className="space-y-1.5 mt-1">
|
<div className="space-y-1.5 mt-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user