import { useState, useEffect, useMemo, useRef } from 'react' import { useSearchParams } from 'react-router-dom' import { useAllQuotes, useHistory } 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 } from 'lucide-react' const ASSET_CLASSES: { key: AssetClass; label: string; emoji: string }[] = [ { key: 'energy', label: 'Énergie', emoji: '⛽' }, { key: 'metals', label: 'Métaux', emoji: '🥇' }, { key: 'agriculture', label: 'Agriculture', emoji: '🌾' }, { key: 'indices', label: 'Indices', emoji: '📊' }, { key: 'equities', label: 'Actions', emoji: '📈' }, { key: 'forex', label: 'Forex', emoji: '💱' }, ] function QuoteCard({ q, selected, onClick }: { q: Quote; selected: boolean; onClick: () => void }) { if (!q.price) return null const pos = q.change_pct >= 0 return (
{q.name || q.symbol}
{q.symbol}
{pos ? ( ) : ( )}
{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: '1A', value: '1y' }, { label: '2A', 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), 'Prix']} /> ) : (
Aucune donnée disponible
)}
) } 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 initialSymbol = useRef(searchParams.get('symbol')) // 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 AssetClass) 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 (

Marchés & Prix

Données en temps réel · Volatilité implicite · Opportunités options

{/* Asset class tabs + ticker search */}
{ASSET_CLASSES.map(({ key, label, emoji }) => ( ))}
setSearchQuery(e.target.value)} onKeyDown={e => { if (e.key === 'Escape') setSearchQuery('') }} placeholder="Rechercher un 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 */}
{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 })} /> )) ) : (
Démarrer le backend pour charger les prix
)}
{/* Chart */}
{selectedSymbol ? ( ) : (
Sélectionner un instrument
pour voir le graphique de prix
)}
{/* Market overview table */}
Vue d'ensemble — Tous marchés
{allQuotes && Object.entries(allQuotes).flatMap(([cls, quotes]) => quotes.map(q => q.price && ( )) )}
Instrument Classe Prix Var. 1j Vol. IV (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 ? ▲ Haussier : q.change_pct < -1.5 ? ▼ Baissier : → Neutre}
) }