import { useState, useEffect, useMemo, useRef } from 'react' import { useSearchParams } from 'react-router-dom' import { useAllQuotes, useHistory, useAddMarketTicker, useRemoveMarketTicker } from '../hooks/useApi' import clsx from 'clsx' import type { Quote, AssetClass } from '../types' import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts' import { TrendingUp, TrendingDown, BarChart2, RefreshCw, Search, Plus, X } from 'lucide-react' type ExtAssetClass = AssetClass | 'custom' const ASSET_CLASSES: { key: ExtAssetClass; label: string; emoji: string }[] = [ { key: 'energy', label: 'Energy', emoji: 'โ›ฝ' }, { key: 'metals', label: 'Metals', emoji: '๐Ÿฅ‡' }, { key: 'agriculture', label: 'Agriculture', emoji: '๐ŸŒพ' }, { key: 'indices', label: 'Indices', emoji: '๐Ÿ“Š' }, { key: 'etfs', label: 'ETFs', emoji: '๐Ÿ—‚' }, { key: 'equities', label: 'Equities', emoji: '๐Ÿ“ˆ' }, { key: 'forex', label: 'Forex', emoji: '๐Ÿ’ฑ' }, ] function QuoteCard({ q, selected, onClick, onRemove }: { q: Quote; selected: boolean; onClick: () => void; onRemove?: () => void }) { if (!q.price) return null const pos = q.change_pct >= 0 return (
{q.name || q.symbol}
{q.symbol}
{pos ? ( ) : ( )} {onRemove && ( )}
{q.price.toFixed(q.price > 100 ? 2 : 4)} {pos ? '+' : ''}{q.change_pct.toFixed(2)}%
{q.iv !== undefined && (
IV: {(q.iv * 100).toFixed(1)}%
)}
) } const PERIOD_OPTIONS = [ { label: '5D', value: '5d' }, { label: '1M', value: '1mo' }, { label: '3M', value: '3mo' }, { label: '6M', value: '6mo' }, { label: '1Y', value: '1y' }, { label: '2Y', value: '2y' }, ] function PriceChart({ symbol, name }: { symbol: string; name: string }) { const [period, setPeriod] = useState('3mo') const { data: hist, isLoading } = useHistory(symbol, period) const chartData = hist?.map(h => ({ date: h.date.slice(0, 10), close: h.close, open: h.open, high: h.high, low: h.low, })) ?? [] const first = chartData[0]?.close ?? 0 const last = chartData[chartData.length - 1]?.close ?? 0 const isUp = last >= first const chg = first > 0 ? ((last - first) / first * 100).toFixed(2) : '0' return (
{name}
{symbol}
{isUp ? '+' : ''}{chg}%
{PERIOD_OPTIONS.map(p => ( ))}
{isLoading ? (
) : chartData.length > 0 ? ( v.slice(5)} interval="preserveStartEnd" /> v > 1000 ? `${(v/1000).toFixed(1)}k` : v.toFixed(2)} /> [v.toFixed(4), 'Price']} /> ) : (
No data available
)}
) } export default function Markets() { const { data: allQuotes, isLoading, refetch } = useAllQuotes() const [searchParams, setSearchParams] = useSearchParams() const [activeClass, setActiveClass] = useState('energy') const [selectedSymbol, setSelectedSymbol] = useState<{ symbol: string; name: string } | null>(null) const [searchQuery, setSearchQuery] = useState('') const [addInput, setAddInput] = useState('') const [addError, setAddError] = useState('') const [showAddInput, setShowAddInput] = useState(false) const initialSymbol = useRef(searchParams.get('symbol')) const { mutateAsync: addTicker, isPending: adding } = useAddMarketTicker() const { mutate: removeTicker } = useRemoveMarketTicker() const handleAddTicker = async () => { const sym = addInput.trim().toUpperCase() if (!sym) return setAddError('') try { await addTicker(sym) setAddInput('') setShowAddInput(false) setActiveClass('custom') } catch (e: unknown) { const msg = (e as { message?: string })?.message ?? 'Ticker not found' setAddError(msg) } } // Auto-select symbol coming from another page (e.g. Portfolio ticker link) useEffect(() => { if (!initialSymbol.current || !allQuotes) return const sym = initialSymbol.current initialSymbol.current = null for (const [cls, quotes] of Object.entries(allQuotes)) { const match = (quotes as Quote[]).find(q => q.symbol.toUpperCase() === sym.toUpperCase()) if (match) { setActiveClass(cls as ExtAssetClass) setSelectedSymbol({ symbol: match.symbol, name: match.name || match.symbol }) break } } setSearchParams({}, { replace: true }) }, [allQuotes, setSearchParams]) const allQuotesFlat = useMemo(() => { if (!allQuotes) return [] return Object.entries(allQuotes).flatMap(([cls, quotes]) => (quotes as Quote[]).filter(q => q.price).map(q => ({ ...q, assetClass: cls as AssetClass })) ) }, [allQuotes]) const searchResults = useMemo(() => { const q = searchQuery.trim().toLowerCase() if (!q) return [] return allQuotesFlat .filter(quote => quote.symbol.toLowerCase().includes(q) || (quote.name ?? '').toLowerCase().includes(q)) .slice(0, 8) }, [searchQuery, allQuotesFlat]) const currentQuotes = allQuotes?.[activeClass] ?? [] return (

Markets & Prices

Real-time data ยท Implied volatility ยท Options opportunities

{/* Asset class tabs + ticker search */}
{ASSET_CLASSES.map(({ key, label, emoji }) => ( ))} {(allQuotes?.['custom']?.length ?? 0) > 0 && ( )} {/* Add custom ticker button + inline input */}
{showAddInput ? (
{ setAddInput(e.target.value); setAddError('') }} onKeyDown={e => { if (e.key === 'Enter') handleAddTicker() if (e.key === 'Escape') { setShowAddInput(false); setAddInput(''); setAddError('') } }} placeholder="e.g. EURCHF=X" className="bg-dark-700 border border-slate-600 rounded px-2.5 py-1 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-purple-500 w-36 font-mono uppercase" /> {addError && {addError}}
) : ( )}
setSearchQuery(e.target.value)} onKeyDown={e => { if (e.key === 'Escape') setSearchQuery('') }} placeholder="Search ticker..." className="bg-dark-700 border border-slate-700 rounded pl-8 pr-3 py-1.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-blue-500 w-52" /> {searchResults.length > 0 && (
{searchResults.map(q => ( ))}
)}
{/* Quote grid */}
{activeClass === 'custom' ? 'โญ Custom' : `${ASSET_CLASSES.find(c => c.key === activeClass)?.emoji ?? ''} ${ASSET_CLASSES.find(c => c.key === activeClass)?.label ?? ''}`}
{isLoading ? ( [1,2,3,4].map(i =>
) ) : currentQuotes.length > 0 ? ( currentQuotes.map(q => ( setSelectedSymbol({ symbol: q.symbol, name: q.name || q.symbol })} onRemove={activeClass === 'custom' ? () => removeTicker(q.symbol) : undefined} /> )) ) : (
Start the backend to load prices
)}
{/* Chart */}
{selectedSymbol ? ( ) : (
Select an instrument
to view the price chart
)}
{/* Market overview table */}
Overview โ€” All Markets
{allQuotes && Object.entries(allQuotes).flatMap(([cls, quotes]) => quotes.map(q => q.price && ( )) )}
Instrument Class Price 1d Chg IV Vol (est.) Signal
{q.name || q.symbol}
{q.symbol}
{cls} {q.price.toFixed(q.price > 100 ? 2 : 4)} = 0 ? 'positive' : 'negative')}> {q.change_pct >= 0 ? '+' : ''}{q.change_pct.toFixed(2)}% {q.iv ? `${(q.iv * 100).toFixed(1)}%` : 'โ€”'} {q.change_pct > 1.5 ? โ–ฒ Bullish : q.change_pct < -1.5 ? โ–ผ Bearish : โ†’ Neutral}
) }