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:
@@ -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>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user