feat: saxo price
This commit is contained in:
@@ -66,26 +66,36 @@ def watchlist_quotes():
|
|||||||
|
|
||||||
@router.post("/{ticker}")
|
@router.post("/{ticker}")
|
||||||
def add_ticker(ticker: str):
|
def add_ticker(ticker: str):
|
||||||
|
"""Adds a tracked instrument. yfinance validation is best-effort, not a gate — an
|
||||||
|
instrument can be entirely Saxo-sourced (both saxo_option_symbol and
|
||||||
|
saxo_quote_symbol linked, see the two PUT .../saxo-*-link endpoints below) with no
|
||||||
|
yfinance equivalent at all, so a ticker yfinance doesn't recognize is still added,
|
||||||
|
just without a name/asset_class lookup or an initial price to report back."""
|
||||||
from services.data_fetcher import get_quote
|
from services.data_fetcher import get_quote
|
||||||
from services.database import get_instruments_watchlist, add_instrument_watchlist
|
from services.database import get_instruments_watchlist, add_instrument_watchlist
|
||||||
import yfinance as yf
|
import yfinance as yf
|
||||||
ticker = ticker.strip().upper()
|
ticker = ticker.strip().upper()
|
||||||
if any(row["ticker"] == ticker for row in get_instruments_watchlist()):
|
if any(row["ticker"] == ticker for row in get_instruments_watchlist()):
|
||||||
raise HTTPException(409, f"'{ticker}' is already in the watchlist")
|
raise HTTPException(409, f"'{ticker}' is already in the watchlist")
|
||||||
|
|
||||||
q = get_quote(ticker)
|
q = get_quote(ticker)
|
||||||
if not q or not q.get("price"):
|
|
||||||
raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance")
|
|
||||||
name = ticker
|
name = ticker
|
||||||
asset_class = "unknown"
|
asset_class = "unknown"
|
||||||
try:
|
if q and q.get("price"):
|
||||||
info = yf.Ticker(ticker).fast_info
|
try:
|
||||||
name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker
|
info = yf.Ticker(ticker).fast_info
|
||||||
quote_type = getattr(info, "quote_type", "") or ""
|
name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker
|
||||||
asset_class = _QUOTE_TYPE_TO_ASSET_CLASS.get(quote_type.upper(), "unknown")
|
quote_type = getattr(info, "quote_type", "") or ""
|
||||||
except Exception:
|
asset_class = _QUOTE_TYPE_TO_ASSET_CLASS.get(quote_type.upper(), "unknown")
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
add_instrument_watchlist(ticker, name, asset_class)
|
add_instrument_watchlist(ticker, name, asset_class)
|
||||||
return {"ticker": ticker, "name": name, "asset_class": asset_class, "price": q["price"]}
|
return {
|
||||||
|
"ticker": ticker, "name": name, "asset_class": asset_class,
|
||||||
|
"price": q.get("price") if q else None,
|
||||||
|
"yfinance_recognized": bool(q and q.get("price")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{ticker}")
|
@router.delete("/{ticker}")
|
||||||
|
|||||||
@@ -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, useSetWatchlistSaxoOptionLink, useSetWatchlistSaxoQuoteLink, 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, 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'
|
||||||
|
|
||||||
@@ -439,18 +439,21 @@ function WatchlistCard() {
|
|||||||
const { mutate: removeTicker } = useRemoveWatchlistInstrument()
|
const { mutate: removeTicker } = useRemoveWatchlistInstrument()
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const [note, setNote] = useState('')
|
||||||
|
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
const sym = input.trim().toUpperCase()
|
const sym = input.trim().toUpperCase()
|
||||||
if (!sym) return
|
if (!sym) return
|
||||||
setError('')
|
setError('')
|
||||||
const check = await validateTicker(sym)
|
setNote('')
|
||||||
if (!check.valid) {
|
// yfinance recognition is informational only, not a gate — an instrument can be
|
||||||
setError(check.reason ?? 'Ticker not found')
|
// entirely Saxo-sourced (link its Option/Quote symbols below) with no yfinance
|
||||||
return
|
// equivalent at all.
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await addTicker(sym)
|
const result = await addTicker(sym)
|
||||||
|
if (!(result as any)?.yfinance_recognized) {
|
||||||
|
setNote(`'${sym}' added, but yfinance doesn't recognize it — no fallback price until you link it to Saxo (Option/Quote below).`)
|
||||||
|
}
|
||||||
setInput('')
|
setInput('')
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = (e as any)?.response?.data?.detail ?? (e as { message?: string })?.message ?? 'Could not add ticker'
|
const msg = (e as any)?.response?.data?.detail ?? (e as { message?: string })?.message ?? 'Could not add ticker'
|
||||||
@@ -487,6 +490,7 @@ function WatchlistCard() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="text-[10px] text-red-400 mb-3">{error}</div>}
|
{error && <div className="text-[10px] text-red-400 mb-3">{error}</div>}
|
||||||
|
{note && <div className="text-[10px] text-amber-400 mb-3">{note}</div>}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-xs text-slate-600">Loading…</div>
|
<div className="text-xs text-slate-600">Loading…</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user