feat: ticker validation + SLV/USO/WEAT/CORN/TUR added to ETFs watchlist

Backend:
- GET /api/market/validate?symbol= — validates ticker against yfinance,
  returns {valid, name, price} or {valid: false, reason: 'helpful message'}
- Added SLV, USO, WEAT, CORN, TUR to ETFs WATCHLIST category

Frontend:
- validateTicker() async helper exported from useApi.ts
- InstrumentPicker (PatternLab): custom ticker field now validates before selecting
  Shows spinner while checking, red error message if not found on yfinance
- InstrumentLens (PatternExplorer): same validation on Go button + Enter key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-22 20:15:26 +02:00
parent a92094e1f3
commit 6cca7f66b6
5 changed files with 131 additions and 36 deletions

View File

@@ -3,6 +3,7 @@ import {
useRunPatternLab, useEvaluatePatternLab, useSaveLabPattern,
usePatternLabRuns, useDeleteLabRun,
useInstrumentScan, useEvaluateInstrumentScan,
validateTicker,
} from '../hooks/useApi'
import {
FlaskConical, Play, CheckCircle2, XCircle, MinusCircle,
@@ -146,9 +147,11 @@ function OutcomeRow({ out }: { out: any }) {
// ── Shared instrument picker ───────────────────────────────────────────────────
function InstrumentPicker({ onSelect }: { onSelect: (ticker: string, name: string) => void }) {
const [search, setSearch] = useState('')
const [cat, setCat] = useState<string | null>(null)
const [custom, setCustom] = useState('')
const [search, setSearch] = useState('')
const [cat, setCat] = useState<string | null>(null)
const [custom, setCustom] = useState('')
const [validating, setValidating] = useState(false)
const [tickerError, setTickerError] = useState<string | null>(null)
const seen = new Set<string>()
const unique = INSTRUMENTS.filter(i => { if (seen.has(i.ticker)) return false; seen.add(i.ticker); return true })
@@ -162,6 +165,26 @@ function InstrumentPicker({ onSelect }: { onSelect: (ticker: string, name: strin
return true
})
const handleCustom = async () => {
const sym = custom.trim().toUpperCase()
if (!sym) return
setTickerError(null)
setValidating(true)
try {
const res = await validateTicker(sym)
if (res.valid) {
onSelect(sym, res.name || sym)
setCustom('')
} else {
setTickerError(res.reason || `Ticker '${sym}' not found`)
}
} catch {
setTickerError(`Could not validate '${sym}' — check your connection`)
} finally {
setValidating(false)
}
}
return (
<div className="space-y-2">
{/* Category pills */}
@@ -186,10 +209,24 @@ function InstrumentPicker({ onSelect }: { onSelect: (ticker: string, name: strin
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search…"
className="w-full pl-6 pr-2 py-1.5 text-xs bg-dark-700 border border-slate-700/50 rounded text-slate-200 placeholder-slate-600 focus:outline-none focus:border-violet-500" />
</div>
<input value={custom} onChange={e => setCustom(e.target.value)}
onKeyDown={e => e.key === 'Enter' && custom.trim() && onSelect(custom.trim(), custom.trim())}
placeholder="Custom ticker"
className="w-28 px-2 py-1.5 text-xs bg-dark-700 border border-slate-700/50 rounded font-mono text-slate-200 placeholder-slate-600 focus:outline-none focus:border-violet-500" />
<div className="flex flex-col gap-1">
<div className="flex gap-1">
<input value={custom} onChange={e => { setCustom(e.target.value); setTickerError(null) }}
onKeyDown={e => e.key === 'Enter' && handleCustom()}
placeholder="Custom ticker"
className={clsx(
'w-28 px-2 py-1.5 text-xs bg-dark-700 border rounded font-mono text-slate-200 placeholder-slate-600 focus:outline-none uppercase',
tickerError ? 'border-red-500/70 focus:border-red-400' : 'border-slate-700/50 focus:border-violet-500'
)} />
<button onClick={handleCustom} disabled={!custom.trim() || validating}
className="px-2 py-1.5 text-xs bg-dark-700 border border-slate-700/50 rounded text-slate-400 hover:text-slate-200 hover:border-violet-500 disabled:opacity-40 transition-colors">
{validating ? <RefreshCw className="w-3 h-3 animate-spin" /> : '→'}
</button>
</div>
{tickerError && (
<p className="text-[9px] text-red-400 max-w-[140px] leading-tight">{tickerError}</p>
)}
</div>
</div>
{/* Grid */}
<div className="grid grid-cols-3 gap-1 max-h-60 overflow-y-auto pr-1">