feat: news filters (période + tri), Top Trades cockpit, date ajout patterns

GeoRadar:
- Filtre période : Aujourd'hui / 7j / 30j / Toutes
- Tri : Impact ↓/↑ · Récentes/Anciennes
- Compteur de news filtrées

Dashboard:
- Remplace Super Contexte par "Top Trades" (top 5 par PnL%)
  avec ticker, stratégie, date d'entrée, PnL%
- Card "Scores Patterns" : ajoute la date d'ajout (created_at)
  sous le nom de chaque pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-19 22:40:08 +02:00
parent bbc9461a8b
commit a662d81f00
2 changed files with 123 additions and 35 deletions

View File

@@ -3,7 +3,7 @@ import {
useGeoRiskScore, useAllQuotes,
useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
useTradeMtm, useRiskDashboard, useGeoNews,
useSimPortfolioRisk, useCycleStatus, useKnowledgeState,
useSimPortfolioRisk, useCycleStatus,
} from '../hooks/useApi'
import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain, Newspaper } from 'lucide-react'
import { Link } from 'react-router-dom'
@@ -86,7 +86,6 @@ export default function Dashboard() {
const { data: riskDashboard } = useRiskDashboard()
const { data: simRisk } = useSimPortfolioRisk()
const { data: cycleStatusData } = useCycleStatus()
const { data: knowledgeState } = useKnowledgeState()
const { data: geoNews } = useGeoNews()
const [pnlView, setPnlView] = useState<'simulated' | 'portfolio'>(() =>
@@ -262,9 +261,13 @@ export default function Dashboard() {
? Math.max(rawScore ?? 0, ...(sp.trade_rankings ?? []).map((r: any) =>
Math.max(0, Math.min(100, (rawScore ?? 0) + (r.score_delta ?? 0)))))
: null
const addedDate = p.created_at ? p.created_at.slice(0, 10) : null
return (
<div key={p.id} className="flex items-center justify-between gap-2">
<span className="text-xs text-slate-300 truncate flex-1 min-w-0">{p.name}</span>
<div key={p.id} className="flex items-center gap-2">
<div className="flex-1 min-w-0">
<span className="text-xs text-slate-300 truncate block">{p.name}</span>
{addedDate && <span className="text-[9px] text-slate-700 font-mono">{addedDate}</span>}
</div>
{bestEff !== null ? (
<span className={clsx('text-xs font-bold shrink-0', scoreColor(bestEff))}>{bestEff}</span>
) : (
@@ -571,29 +574,44 @@ export default function Dashboard() {
{/* ── Command Center: Intelligence & Contexte ── */}
<div className="grid grid-cols-4 gap-3">
{/* Super Contexte IA */}
{/* Top 5 trades loggés */}
{(() => {
const state = (knowledgeState as any)?.state
const synthesis = state?.synthesis
const insights = (synthesis?.regime_insights?.length ?? 0)
+ (synthesis?.pattern_insights?.length ?? 0)
+ (synthesis?.recurring_mistakes?.length ?? 0)
const priority = synthesis?.strategic_priorities?.[0]
const excerpt = typeof priority === 'string' ? priority
: typeof state?.narrative === 'string' ? state.narrative.slice(0, 90)
: null
const top5 = [...mtmTrades]
.sort((a, b) => (b.pnl_pct ?? -999) - (a.pnl_pct ?? -999))
.slice(0, 5)
return (
<Link to="/super-contexte" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🧠 Super Contexte</span>
<span className="section-title mb-0 text-[10px]">🏆 Top Trades</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className="text-sm font-bold text-purple-400 mt-1">
{state ? `${insights} insights actifs` : 'Aucune synthèse'}
</div>
<div className="text-[10px] text-slate-500 mt-1 line-clamp-2">
{excerpt ?? 'Lancer une synthèse dans Super Contexte'}
</div>
{top5.length > 0 ? (
<div className="space-y-1.5 mt-1">
{top5.map((t: any, i: number) => {
const pnl: number | null = t.pnl_pct ?? null
const isBear = t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear')
const entryDate = t.entry_date ? t.entry_date.slice(0, 10) : null
return (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className="text-[9px] text-slate-700 font-mono w-3 shrink-0">{i + 1}</span>
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
<span className="font-mono text-slate-200 font-bold w-9 truncate shrink-0">{t.underlying ?? '—'}</span>
<span className="text-slate-500 truncate flex-1 text-[9px]">{t.strategy ?? ''}</span>
{entryDate && <span className="text-[8px] text-slate-700 shrink-0 font-mono">{entryDate}</span>}
{pnl !== null ? (
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
</span>
) : (
<span className="text-slate-700 shrink-0 text-[9px]"></span>
)}
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
)}
</Link>
)
})()}

View File

@@ -252,17 +252,49 @@ function PatternRelevanceCard({ p }: { p: any }) {
)
}
type SortMode = 'impact_desc' | 'impact_asc' | 'date_desc' | 'date_asc'
type DateRange = 'all' | 'today' | '7d' | '30d'
const DATE_RANGE_LABELS: Record<DateRange, string> = {
all: 'Toutes', today: "Aujourd'hui", '7d': '7 jours', '30d': '30 jours',
}
const SORT_LABELS: Record<SortMode, string> = {
impact_desc: 'Impact ↓', impact_asc: 'Impact ↑', date_desc: 'Récentes ↓', date_asc: 'Anciennes ↑',
}
function isWithinRange(dateStr: string, range: DateRange): boolean {
if (range === 'all') return true
const d = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - d.getTime()
if (range === 'today') return diffMs < 86_400_000
if (range === '7d') return diffMs < 7 * 86_400_000
if (range === '30d') return diffMs < 30 * 86_400_000
return true
}
export default function GeoRadar() {
const { data: news, isLoading } = useGeoNews()
const { data: riskScore } = useGeoRiskScore()
const [filterCat, setFilterCat] = useState<string>('all')
const [sortMode, setSortMode] = useState<SortMode>('impact_desc')
const [dateRange, setDateRange] = useState<DateRange>('all')
const [tab, setTab] = useState<'news' | 'patterns'>('news')
const [days, setDays] = useState(2)
const { data: patternRelevance, isLoading: relLoading } = usePatternRelevance(days)
const categories = ['all', ...Array.from(new Set(news?.map(n => n.category) ?? []))]
const filtered = filterCat === 'all' ? news : news?.filter(n => n.category === filterCat)
const filtered = (news ?? [])
.filter(n => filterCat === 'all' || n.category === filterCat)
.filter(n => isWithinRange(n.date, dateRange))
.sort((a, b) => {
if (sortMode === 'impact_desc') return b.impact_score - a.impact_score
if (sortMode === 'impact_asc') return a.impact_score - b.impact_score
if (sortMode === 'date_desc') return new Date(b.date).getTime() - new Date(a.date).getTime()
return new Date(a.date).getTime() - new Date(b.date).getTime()
})
const patternsWithNews = (patternRelevance ?? []).filter((p: any) => p.matching_news?.length > 0).length
const totalPatterns = patternRelevance?.length ?? 0
@@ -333,25 +365,63 @@ export default function GeoRadar() {
{/* News tab */}
{tab === 'news' && (
<>
<div className="flex flex-wrap gap-2">
{categories.slice(0, 8).map(cat => (
<button key={cat} onClick={() => setFilterCat(cat)}
className={clsx('px-3 py-1 rounded text-xs border transition-colors', {
'bg-blue-600 border-blue-500 text-white': filterCat === cat,
'border-slate-700 text-slate-400 hover:border-slate-500': filterCat !== cat,
})}>
{cat === 'all' ? 'Tous' : CATEGORY_LABELS[cat] ?? cat}
</button>
))}
{/* Filters row */}
<div className="flex flex-wrap items-center gap-3">
{/* Category filter */}
<div className="flex flex-wrap gap-1.5">
{categories.slice(0, 8).map(cat => (
<button key={cat} onClick={() => setFilterCat(cat)}
className={clsx('px-2.5 py-1 rounded text-xs border transition-colors', {
'bg-blue-600 border-blue-500 text-white': filterCat === cat,
'border-slate-700 text-slate-400 hover:border-slate-500': filterCat !== cat,
})}>
{cat === 'all' ? 'Toutes catégories' : CATEGORY_LABELS[cat] ?? cat}
</button>
))}
</div>
</div>
{/* Sort + date range row */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Période :</span>
<div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
{(['today', '7d', '30d', 'all'] as DateRange[]).map(r => (
<button key={r} onClick={() => setDateRange(r)}
className={clsx('px-2.5 py-1 rounded text-xs transition-colors', {
'bg-slate-600 text-white': dateRange === r,
'text-slate-400 hover:text-slate-200': dateRange !== r,
})}>
{DATE_RANGE_LABELS[r]}
</button>
))}
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Tri :</span>
<div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
{(['impact_desc', 'impact_asc', 'date_desc', 'date_asc'] as SortMode[]).map(s => (
<button key={s} onClick={() => setSortMode(s)}
className={clsx('px-2.5 py-1 rounded text-xs transition-colors', {
'bg-slate-600 text-white': sortMode === s,
'text-slate-400 hover:text-slate-200': sortMode !== s,
})}>
{SORT_LABELS[s]}
</button>
))}
</div>
</div>
<span className="text-xs text-slate-600">{filtered.length} news</span>
</div>
{isLoading ? (
<div className="grid grid-cols-2 gap-4">
{[1,2,3,4].map(i => <div key={i} className="card animate-pulse h-32 bg-dark-700"></div>)}
</div>
) : (
<div className="grid grid-cols-2 gap-4">
{filtered?.map((n, i) => <NewsCard key={n.id || i} news={n} />)}
{(!filtered || filtered.length === 0) && (
{filtered.map((n, i) => <NewsCard key={n.id || i} news={n} />)}
{filtered.length === 0 && (
<div className="card col-span-2 text-center py-8 text-slate-500">
Démarrer le backend pour charger les actualités
</div>