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,
|
useGeoRiskScore, useAllQuotes,
|
||||||
useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
|
useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
|
||||||
useTradeMtm, useRiskDashboard, useGeoNews,
|
useTradeMtm, useRiskDashboard, useGeoNews,
|
||||||
useSimPortfolioRisk, useCycleStatus, useKnowledgeState,
|
useSimPortfolioRisk, useCycleStatus,
|
||||||
} from '../hooks/useApi'
|
} from '../hooks/useApi'
|
||||||
import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain, Newspaper } from 'lucide-react'
|
import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain, Newspaper } from 'lucide-react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
@@ -86,7 +86,6 @@ export default function Dashboard() {
|
|||||||
const { data: riskDashboard } = useRiskDashboard()
|
const { data: riskDashboard } = useRiskDashboard()
|
||||||
const { data: simRisk } = useSimPortfolioRisk()
|
const { data: simRisk } = useSimPortfolioRisk()
|
||||||
const { data: cycleStatusData } = useCycleStatus()
|
const { data: cycleStatusData } = useCycleStatus()
|
||||||
const { data: knowledgeState } = useKnowledgeState()
|
|
||||||
const { data: geoNews } = useGeoNews()
|
const { data: geoNews } = useGeoNews()
|
||||||
|
|
||||||
const [pnlView, setPnlView] = useState<'simulated' | 'portfolio'>(() =>
|
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(rawScore ?? 0, ...(sp.trade_rankings ?? []).map((r: any) =>
|
||||||
Math.max(0, Math.min(100, (rawScore ?? 0) + (r.score_delta ?? 0)))))
|
Math.max(0, Math.min(100, (rawScore ?? 0) + (r.score_delta ?? 0)))))
|
||||||
: null
|
: null
|
||||||
|
const addedDate = p.created_at ? p.created_at.slice(0, 10) : null
|
||||||
return (
|
return (
|
||||||
<div key={p.id} className="flex items-center justify-between gap-2">
|
<div key={p.id} className="flex items-center gap-2">
|
||||||
<span className="text-xs text-slate-300 truncate flex-1 min-w-0">{p.name}</span>
|
<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 ? (
|
{bestEff !== null ? (
|
||||||
<span className={clsx('text-xs font-bold shrink-0', scoreColor(bestEff))}>{bestEff}</span>
|
<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 ── */}
|
{/* ── Command Center: Intelligence & Contexte ── */}
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
|
||||||
{/* Super Contexte IA */}
|
{/* Top 5 trades loggés */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const state = (knowledgeState as any)?.state
|
const top5 = [...mtmTrades]
|
||||||
const synthesis = state?.synthesis
|
.sort((a, b) => (b.pnl_pct ?? -999) - (a.pnl_pct ?? -999))
|
||||||
const insights = (synthesis?.regime_insights?.length ?? 0)
|
.slice(0, 5)
|
||||||
+ (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
|
|
||||||
return (
|
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">
|
<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" />
|
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-purple-400 mt-1">
|
{top5.length > 0 ? (
|
||||||
{state ? `${insights} insights actifs` : 'Aucune synthèse'}
|
<div className="space-y-1.5 mt-1">
|
||||||
</div>
|
{top5.map((t: any, i: number) => {
|
||||||
<div className="text-[10px] text-slate-500 mt-1 line-clamp-2">
|
const pnl: number | null = t.pnl_pct ?? null
|
||||||
{excerpt ?? 'Lancer une synthèse dans Super Contexte'}
|
const isBear = t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear')
|
||||||
</div>
|
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>
|
</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() {
|
export default function GeoRadar() {
|
||||||
const { data: news, isLoading } = useGeoNews()
|
const { data: news, isLoading } = useGeoNews()
|
||||||
const { data: riskScore } = useGeoRiskScore()
|
const { data: riskScore } = useGeoRiskScore()
|
||||||
const [filterCat, setFilterCat] = useState<string>('all')
|
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 [tab, setTab] = useState<'news' | 'patterns'>('news')
|
||||||
const [days, setDays] = useState(2)
|
const [days, setDays] = useState(2)
|
||||||
|
|
||||||
const { data: patternRelevance, isLoading: relLoading } = usePatternRelevance(days)
|
const { data: patternRelevance, isLoading: relLoading } = usePatternRelevance(days)
|
||||||
|
|
||||||
const categories = ['all', ...Array.from(new Set(news?.map(n => n.category) ?? []))]
|
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 patternsWithNews = (patternRelevance ?? []).filter((p: any) => p.matching_news?.length > 0).length
|
||||||
const totalPatterns = patternRelevance?.length ?? 0
|
const totalPatterns = patternRelevance?.length ?? 0
|
||||||
@@ -333,25 +365,63 @@ export default function GeoRadar() {
|
|||||||
{/* News tab */}
|
{/* News tab */}
|
||||||
{tab === 'news' && (
|
{tab === 'news' && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap gap-2">
|
{/* Filters row */}
|
||||||
{categories.slice(0, 8).map(cat => (
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<button key={cat} onClick={() => setFilterCat(cat)}
|
{/* Category filter */}
|
||||||
className={clsx('px-3 py-1 rounded text-xs border transition-colors', {
|
<div className="flex flex-wrap gap-1.5">
|
||||||
'bg-blue-600 border-blue-500 text-white': filterCat === cat,
|
{categories.slice(0, 8).map(cat => (
|
||||||
'border-slate-700 text-slate-400 hover:border-slate-500': filterCat !== cat,
|
<button key={cat} onClick={() => setFilterCat(cat)}
|
||||||
})}>
|
className={clsx('px-2.5 py-1 rounded text-xs border transition-colors', {
|
||||||
{cat === 'all' ? 'Tous' : CATEGORY_LABELS[cat] ?? cat}
|
'bg-blue-600 border-blue-500 text-white': filterCat === cat,
|
||||||
</button>
|
'border-slate-700 text-slate-400 hover:border-slate-500': filterCat !== cat,
|
||||||
))}
|
})}>
|
||||||
|
{cat === 'all' ? 'Toutes catégories' : CATEGORY_LABELS[cat] ?? cat}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</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 ? (
|
{isLoading ? (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<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>)}
|
{[1,2,3,4].map(i => <div key={i} className="card animate-pulse h-32 bg-dark-700"></div>)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{filtered?.map((n, i) => <NewsCard key={n.id || i} news={n} />)}
|
{filtered.map((n, i) => <NewsCard key={n.id || i} news={n} />)}
|
||||||
{(!filtered || filtered.length === 0) && (
|
{filtered.length === 0 && (
|
||||||
<div className="card col-span-2 text-center py-8 text-slate-500">
|
<div className="card col-span-2 text-center py-8 text-slate-500">
|
||||||
Démarrer le backend pour charger les actualités
|
Démarrer le backend pour charger les actualités
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user