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,13 +3,14 @@ import { Link } from 'react-router-dom'
import clsx from 'clsx'
import {
TreePine, Search, ChevronRight, ChevronDown,
TrendingUp, TrendingDown, Zap, BookOpen, ExternalLink,
TrendingUp, TrendingDown, Zap, BookOpen, ExternalLink, RefreshCw,
} from 'lucide-react'
import {
useAllPatterns,
usePatternTaxonomy,
usePatternsByInstrument,
useLastScores,
validateTicker,
} from '../hooks/useApi'
import { INSTRUMENTS, INSTRUMENT_CATEGORIES } from '../constants/instruments'
@@ -591,7 +592,9 @@ function InstrumentRow({ pattern, searchTerm }: { pattern: Pattern; searchTerm:
function InstrumentLens({ patterns }: { patterns: Pattern[] }) {
const [searchTerm, setSearchTerm] = useState('')
const [activeCategory, setActiveCategory] = useState<string | null>(null)
const [customInput, setCustomInput] = useState('')
const [customInput, setCustomInput] = useState('')
const [validating, setValidating] = useState(false)
const [tickerError, setTickerError] = useState<string | null>(null)
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Selected ticker — either clicked from grid or typed
@@ -629,9 +632,24 @@ function InstrumentLens({ patterns }: { patterns: Pattern[] }) {
)
}, [uniqueInstruments, activeCategory, searchTerm])
const handleCustom = () => {
const t = customInput.trim()
if (t) { setSelectedTicker(t); setCustomInput('') }
const handleCustom = async () => {
const sym = customInput.trim().toUpperCase()
if (!sym) return
setTickerError(null)
setValidating(true)
try {
const res = await validateTicker(sym)
if (res.valid) {
setSelectedTicker(sym)
setCustomInput('')
} else {
setTickerError(res.reason || `Ticker '${sym}' not found`)
}
} catch {
setTickerError(`Could not validate '${sym}' — check your connection`)
} finally {
setValidating(false)
}
}
return (
@@ -660,12 +678,25 @@ function InstrumentLens({ patterns }: { patterns: Pattern[] }) {
placeholder="Search instruments…"
className="w-full pl-8 pr-3 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-blue-600" />
</div>
<input type="text" value={customInput} onChange={e => setCustomInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCustom()}
placeholder="Custom ticker…"
className="w-32 px-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-blue-600 font-mono" />
<button onClick={handleCustom}
className="px-2.5 py-1.5 text-xs bg-blue-700 hover:bg-blue-600 text-white rounded transition-colors">Go</button>
<div className="flex flex-col gap-1">
<div className="flex gap-1">
<input type="text" value={customInput}
onChange={e => { setCustomInput(e.target.value); setTickerError(null) }}
onKeyDown={e => e.key === 'Enter' && handleCustom()}
placeholder="Custom ticker…"
className={clsx(
'w-32 px-2 py-1.5 text-xs bg-dark-700 border rounded text-slate-200 placeholder-slate-600 focus:outline-none font-mono uppercase',
tickerError ? 'border-red-500/70 focus:border-red-400' : 'border-slate-700/50 focus:border-blue-600'
)} />
<button onClick={handleCustom} disabled={!customInput.trim() || validating}
className="px-2.5 py-1.5 text-xs bg-blue-700 hover:bg-blue-600 disabled:opacity-40 text-white rounded transition-colors flex items-center gap-1">
{validating ? <><RefreshCw className="w-3 h-3 animate-spin" /> </> : 'Go'}
</button>
</div>
{tickerError && (
<p className="text-[9px] text-red-400 max-w-[200px] leading-tight">{tickerError}</p>
)}
</div>
{selectedTicker && (
<button onClick={() => setSelectedTicker('')}
className="text-xs text-slate-500 hover:text-slate-300 transition-colors">Clear</button>