feat: cockpit
This commit is contained in:
@@ -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<string, string> = {
|
||||
}
|
||||
|
||||
const IMPACT_DOT: Record<string, string> = {
|
||||
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<Set<string>>(new Set(['high', 'medium', 'low']))
|
||||
const [ecoCurrencyFilter, setEcoCurrencyFilter] = useState<Set<string>>(new Set(ECO_CURRENCY_FILTERS))
|
||||
const [ecoShowNextWeek, setEcoShowNextWeek] = useState(false)
|
||||
const [watchlistChartTicker, setWatchlistChartTicker] = useState<string | null>(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 (
|
||||
<div className="p-6 space-y-6">
|
||||
@@ -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) */}
|
||||
<div className="card col-span-1 flex flex-col overflow-hidden" style={row1Height ? { height: row1Height } : undefined}>
|
||||
<div className="section-title flex items-center gap-1 shrink-0"><Clock className="w-3 h-3" /> Economic Events</div>
|
||||
{(todayEvents.length > 0 || restOfWeekGrouped.length > 0) ? (
|
||||
<div className="flex items-center justify-between gap-1.5 mt-1.5 shrink-0">
|
||||
<div className="flex items-center gap-1">
|
||||
{(['high', 'medium', 'low'] as const).map(imp => (
|
||||
<button
|
||||
key={imp}
|
||||
onClick={() => setEcoImpactFilter(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(imp)) next.delete(imp); else next.add(imp)
|
||||
return next
|
||||
})}
|
||||
title={`${imp} impact — click to toggle`}
|
||||
className={clsx('w-2.5 h-2.5 rounded-full transition-opacity', IMPACT_DOT[imp], ecoImpactFilter.has(imp) ? 'opacity-100' : 'opacity-20')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{ECO_CURRENCY_FILTERS.map(cur => (
|
||||
<button
|
||||
key={cur}
|
||||
onClick={() => setEcoCurrencyFilter(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(cur)) next.delete(cur); else next.add(cur)
|
||||
return next
|
||||
})}
|
||||
title={`${cur} — click to toggle`}
|
||||
className={clsx('text-xs leading-none transition-opacity', ecoCurrencyFilter.has(cur) ? 'opacity-100' : 'opacity-25')}
|
||||
>
|
||||
{CURRENCY_FLAGS[cur] ?? cur}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEcoShowNextWeek(v => !v)}
|
||||
className={clsx('text-[8px] px-1.5 py-0.5 rounded shrink-0 transition-colors',
|
||||
ecoShowNextWeek ? 'bg-blue-600 text-white' : 'text-slate-500 hover:text-slate-300 border border-slate-700/40')}
|
||||
>
|
||||
+Next week
|
||||
</button>
|
||||
</div>
|
||||
{(todayEvents.length > 0 || restOfWeekGrouped.length > 0 || (ecoShowNextWeek && nextWeekGrouped.length > 0)) ? (
|
||||
<div className="mt-2 space-y-2 overflow-y-auto flex-1 min-h-0">
|
||||
{todayEvents.length > 0 && (
|
||||
<div>
|
||||
@@ -668,8 +727,23 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{ecoShowNextWeek && nextWeekGrouped.length > 0 && (
|
||||
<>
|
||||
<div className="text-[8px] text-slate-600 uppercase tracking-wide pt-1 border-t border-slate-700/30">Next week</div>
|
||||
{nextWeekGrouped.map(group => (
|
||||
<div key={group.date}>
|
||||
<div className="flex items-center gap-1.5 text-[9px] font-semibold text-slate-300 bg-dark-700/50 rounded px-1.5 py-0.5 mb-1">
|
||||
{formatDateShort(group.date)}
|
||||
</div>
|
||||
<div className="space-y-1 pl-0.5">
|
||||
{group.events.map((ev: any, i: number) => <EcoEventRow key={i} ev={ev} showActual={false} />)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : <div className="text-slate-500 text-xs mt-2">No events this week</div>}
|
||||
) : <div className="text-slate-500 text-xs mt-2">No events match filters</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1088,25 +1162,36 @@ export default function Dashboard() {
|
||||
<TradeRankList trades={mtmTrades} mode={tradesView} title="🏆 Trades Overview" linkTo="/journal"
|
||||
toggle={<TradesViewToggle value={tradesView} onChange={setTradesViewPersist} />} />
|
||||
|
||||
{/* 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<string, string> = {}
|
||||
for (const w of ((watchlistItems as any[]) ?? [])) nameByTicker[w.ticker] = w.name || w.ticker
|
||||
return (
|
||||
<Link to="/wavelets-simulation" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="card">
|
||||
<Link to="/wavelets-simulation" className="flex items-center justify-between mb-1 hover:opacity-80 transition-opacity">
|
||||
<span className="section-title mb-0 text-[10px] flex items-center gap-1">
|
||||
<Waves className="w-2.5 h-2.5" /> Wavelets Signal
|
||||
</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
</Link>
|
||||
{signals.length > 0 ? (
|
||||
<div className="space-y-1.5 mt-1">
|
||||
{signals.slice(0, 5).map((s: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-1.5 text-[10px]">
|
||||
<div
|
||||
key={i}
|
||||
onDoubleClick={() => 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"
|
||||
>
|
||||
<span className={clsx('shrink-0', s.direction === 'up' ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{s.direction === 'up' ? '↑' : '↓'}
|
||||
</span>
|
||||
<span className="font-mono text-slate-200 font-bold shrink-0">{s.ticker}</span>
|
||||
<span className="text-slate-200 font-bold shrink-0 truncate max-w-[90px]" title={s.ticker}>
|
||||
{nameByTicker[s.ticker] || s.ticker}
|
||||
</span>
|
||||
<span className="text-slate-500 truncate flex-1 text-[9px]">{s.band_label} · {s.signal_kind}</span>
|
||||
{s.computed_at && <span className="text-[8px] text-slate-700 shrink-0 font-mono">{s.computed_at.slice(0, 10)}</span>}
|
||||
</div>
|
||||
@@ -1115,7 +1200,7 @@ export default function Dashboard() {
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">No wavelet signal detected — le prochain cycle en calculera</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
|
||||
@@ -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<string | null>(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')
|
||||
|
||||
Reference in New Issue
Block a user