diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 0b8bbf1..48f1fb1 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -9,7 +9,7 @@ import { useWaveletWatchlistSignals, } from '../hooks/useApi' import { Clock, Globe, ShieldAlert, ArrowUpRight, Newspaper, Waves, Link2 } from 'lucide-react' -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import clsx from 'clsx' import type { Quote } from '../types' import { format } from 'date-fns' @@ -48,9 +48,15 @@ const CURRENCY_FLAGS: Record = { } const IMPACT_DOT: Record = { - high: 'bg-red-500', medium: 'bg-yellow-400', low: 'bg-slate-400', + high: 'bg-red-500', medium: 'bg-yellow-400', low: 'bg-emerald-500', } +// The Economic Events card's currency filter is a closed set of 4 major economies +// (not every SUPPORTED_CURRENCIES value) — deliberately simple: toggle chips, not a +// full currency picker. GBP as the 4th pick: heavily represented in the feed already +// and a major G7/FX market alongside US/EUR/China. +const ECO_CURRENCY_FILTERS = ['USD', 'EUR', 'CNY', 'GBP'] as const + const formatDateShort = (dateStr: string) => { const d = new Date(dateStr + 'T00:00:00Z') return d.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) @@ -139,9 +145,10 @@ function EcoEventRow({ ev, showActual }: { ev: any; showActual: boolean }) { } export default function Dashboard() { + const navigate = useNavigate() const { data: riskScore, isLoading: riskLoading } = useGeoRiskScore() const { data: allQuotes } = useAllQuotes() - const { data: ecoCalendarData } = useEcoCalendar({ period: 'recent', limit: 100 }) + const { data: ecoCalendarData } = useEcoCalendar({ period: 'recent', limit: 150, impacts: 'high,medium,low' }) const { data: portfolio } = usePortfolioSummary() const { data: lastScoresData } = useLastScores() const { data: allPatternsData } = useAllPatterns() @@ -156,6 +163,9 @@ export default function Dashboard() { const { data: watchlistItems } = useInstrumentsWatchlist() const { data: watchlistQuotesData } = useInstrumentsWatchlistQuotes() const { data: instrumentCatalogIds } = useInstrumentCatalogIds() + const [ecoImpactFilter, setEcoImpactFilter] = useState>(new Set(['high', 'medium', 'low'])) + const [ecoCurrencyFilter, setEcoCurrencyFilter] = useState>(new Set(ECO_CURRENCY_FILTERS)) + const [ecoShowNextWeek, setEcoShowNextWeek] = useState(false) const [watchlistChartTicker, setWatchlistChartTicker] = useState(null) const [watchlistChartPeriod, setWatchlistChartPeriod] = useState('3m') const activeWatchlistTicker = watchlistChartTicker ?? (watchlistItems as any)?.[0]?.ticker ?? '' @@ -277,32 +287,42 @@ export default function Dashboard() { }, [watchlistQuotesData]) // FF economic events — today (with actual/forecast as they release) + rest of the - // current week (Mon-Sun), grouped by date. Past events beyond today are dropped — - // the point of this card is "am I up to date", not a historical log. - const { todayEvents, restOfWeekGrouped } = useMemo(() => { - const events: any[] = (ecoCalendarData as any)?.events ?? [] + // current week (Mon-Sun) + optionally next week, grouped by date. Filtered by impact + // (high/medium/low, toggleable) and currency (closed set of 4 majors, toggleable). + // Past events beyond today are dropped — the point of this card is "am I up to date". + const groupByDate = (evs: any[]) => { + const sorted = [...evs].sort((a, b) => `${a.event_date}${a.event_time ?? ''}`.localeCompare(`${b.event_date}${b.event_time ?? ''}`)) + const groups: { date: string; events: any[] }[] = [] + for (const ev of sorted) { + const last = groups[groups.length - 1] + if (!last || last.date !== ev.event_date) groups.push({ date: ev.event_date, events: [ev] }) + else last.events.push(ev) + } + return groups + } + + const { todayEvents, restOfWeekGrouped, nextWeekGrouped } = useMemo(() => { + const all: any[] = (ecoCalendarData as any)?.events ?? [] + const events = all.filter((ev: any) => ecoImpactFilter.has(ev.impact) && ecoCurrencyFilter.has(ev.currency)) const now = new Date() const todayISO = now.toISOString().slice(0, 10) const dow = now.getUTCDay() // 0=Sun..6=Sat const sunday = new Date(now) sunday.setUTCDate(now.getUTCDate() + (dow === 0 ? 0 : 7 - dow)) const weekEndISO = sunday.toISOString().slice(0, 10) + const nextSunday = new Date(sunday) + nextSunday.setUTCDate(sunday.getUTCDate() + 7) + const nextWeekEndISO = nextSunday.toISOString().slice(0, 10) const today = events .filter((ev: any) => ev.event_date === todayISO) .sort((a: any, b: any) => (a.event_time ?? '').localeCompare(b.event_time ?? '')) - const restOfWeek = events - .filter((ev: any) => ev.event_date > todayISO && ev.event_date <= weekEndISO) - .sort((a: any, b: any) => `${a.event_date}${a.event_time ?? ''}`.localeCompare(`${b.event_date}${b.event_time ?? ''}`)) - const groups: { date: string; events: any[] }[] = [] - for (const ev of restOfWeek) { - const last = groups[groups.length - 1] - if (!last || last.date !== ev.event_date) groups.push({ date: ev.event_date, events: [ev] }) - else last.events.push(ev) - } - return { todayEvents: today, restOfWeekGrouped: groups } - }, [ecoCalendarData]) + const restOfWeek = events.filter((ev: any) => ev.event_date > todayISO && ev.event_date <= weekEndISO) + const nextWeek = events.filter((ev: any) => ev.event_date > weekEndISO && ev.event_date <= nextWeekEndISO) + + return { todayEvents: today, restOfWeekGrouped: groupByDate(restOfWeek), nextWeekGrouped: groupByDate(nextWeek) } + }, [ecoCalendarData, ecoImpactFilter, ecoCurrencyFilter]) return (
@@ -643,10 +663,49 @@ export default function Dashboard() { ) })()} - {/* Economic Events — real Forex Factory feed: today (actual as it releases) + rest of week */} + {/* Economic Events — real Forex Factory feed: today (actual as it releases) + rest of week (+ next week, optional) */}
Economic Events
- {(todayEvents.length > 0 || restOfWeekGrouped.length > 0) ? ( +
+
+ {(['high', 'medium', 'low'] as const).map(imp => ( +
+
+ {ECO_CURRENCY_FILTERS.map(cur => ( + + ))} +
+ +
+ {(todayEvents.length > 0 || restOfWeekGrouped.length > 0 || (ecoShowNextWeek && nextWeekGrouped.length > 0)) ? (
{todayEvents.length > 0 && (
@@ -668,8 +727,23 @@ export default function Dashboard() {
))} + {ecoShowNextWeek && nextWeekGrouped.length > 0 && ( + <> +
Next week
+ {nextWeekGrouped.map(group => ( +
+
+ {formatDateShort(group.date)} +
+
+ {group.events.map((ev: any, i: number) => )} +
+
+ ))} + + )}
- ) :
No events this week
} + ) :
No events match filters
}
@@ -1088,25 +1162,36 @@ export default function Dashboard() { } /> - {/* Wavelets Signal — latest per-ticker signal from the watchlist scan (computed each cycle) */} + {/* Wavelets Signal — latest per-ticker signal from the watchlist scan (computed each + cycle). Single click on the card = Wavelets Simulation overview; double-click a + row = that instrument's Analysis page, Wavelets panel open directly. */} {(() => { const signals: any[] = waveletSignalsData?.signals ?? [] + const nameByTicker: Record = {} + for (const w of ((watchlistItems as any[]) ?? [])) nameByTicker[w.ticker] = w.name || w.ticker return ( - -
+
+ Wavelets Signal -
+ {signals.length > 0 ? (
{signals.slice(0, 5).map((s: any, i: number) => ( -
+
navigate(`/instruments/${encodeURIComponent(s.ticker)}?tab=wavelets`)} + title="Double-click to open in Instrument Analysis → Wavelets" + className="flex items-center gap-1.5 text-[10px] rounded px-1 -mx-1 py-0.5 cursor-pointer hover:bg-dark-700/40 transition-colors" + > {s.direction === 'up' ? '↑' : '↓'} - {s.ticker} + + {nameByTicker[s.ticker] || s.ticker} + {s.band_label} · {s.signal_kind} {s.computed_at && {s.computed_at.slice(0, 10)}}
@@ -1115,7 +1200,7 @@ export default function Dashboard() { ) : (
No wavelet signal detected — le prochain cycle en calculera
)} - +
) })()} diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 81f296d..22675af 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react' -import { useParams, useNavigate } from 'react-router-dom' +import { useParams, useNavigate, useSearchParams } from 'react-router-dom' import { Sparkles, RefreshCw, ChevronDown, TrendingUp, TrendingDown, Minus, BarChart2, Clock, Calendar, AlertCircle, @@ -1426,7 +1426,10 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i const [loadingNarr, setLoadingNarr] = useState(false) const [selectorOpen, setSelectorOpen] = useState(false) const [selectedDate, setSelectedDate] = useState(null) - const [tabUnder, setTabUnder] = useState<'counters' | 'analyse' | 'wavelets'>('counters') + const [searchParams] = useSearchParams() + // ?tab=wavelets lets other pages (e.g. Dashboard's Wavelets Signal card) deep-link + // straight into the Wavelets panel instead of always landing on Counters. + const [tabUnder, setTabUnder] = useState<'counters' | 'analyse' | 'wavelets'>(() => (searchParams.get('tab') === 'wavelets' ? 'wavelets' : 'counters')) const [waveletLevels, setWaveletLevels] = useState(4) const [waveletFamily, setWaveletFamily] = useState<'gmw' | 'morlet' | 'bump'>('gmw') const [waveletMethod, setWaveletMethod] = useState<'cwt' | 'ssq'>('cwt')