feat: origin tracing on all market_events

- DB: colonne origin (migration + UPDATE heuristique sur données legacy)
- save/update_market_event: persist origin
- Tous les points de création taguent leur origine:
    bootstrap_macro/eco/ma/legacy | detector_news/eco/technical/report | manual
- UI MarketEvents: badge d'origine avec icône + description dans le panneau détail,
  icône tooltip dans la liste gauche, message explicite si pas de source_refs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-25 20:58:05 +02:00
parent d5da4737ef
commit bd28b6a73a
8 changed files with 78 additions and 16 deletions

View File

@@ -44,6 +44,7 @@ interface MarketEvent {
impact_score: number
absorption_pct: number | null
source_refs: SourceRef[]
origin?: string
impacts?: InstrumentImpact[]
evaluated?: boolean
}
@@ -71,6 +72,19 @@ const LEVEL_COLORS: Record<string, string> = {
medium: 'text-amber-400',
short: 'text-emerald-400',
}
const ORIGIN_META: Record<string, { label: string; icon: string; color: string; description: string }> = {
bootstrap_macro: { label: 'Bootstrap macro', icon: '📚', color: 'text-slate-400', description: 'Données historiques macro/géopolitiques chargées au démarrage' },
bootstrap_eco: { label: 'Bootstrap éco', icon: '📅', color: 'text-amber-400', description: 'Calendrier économique FRED historique (FOMC, CPI, NFP…)' },
bootstrap_ma: { label: 'Bootstrap MA', icon: '📈', color: 'text-cyan-400', description: 'Signal technique MA détecté sur données historiques (yfinance)' },
bootstrap_legacy: { label: 'Bootstrap legacy', icon: '🗄️', color: 'text-slate-500', description: 'Données initiales (bootstrap, origine exacte inconnue)' },
detector_news: { label: 'News RSS', icon: '📰', color: 'text-red-400', description: 'Généré depuis un flux RSS — validé par IA (GPT-4o-mini)' },
detector_eco: { label: 'Surprise FRED', icon: '📊', color: 'text-amber-400', description: 'Surprise économique FRED détectée par le cycle (z-score > seuil)' },
detector_technical: { label: 'Signal technique', icon: '📐', color: 'text-cyan-400', description: 'Croisement MA50/MA100/MA200 détecté automatiquement (yfinance)' },
detector_report: { label: 'Rapport instit.', icon: '📋', color: 'text-blue-400', description: 'Rapport institutionnel haute importance (COT, EIA, Fed…)' },
manual: { label: 'Manuel', icon: '✏️', color: 'text-emerald-400', description: 'Créé manuellement dans l\'interface' },
unknown: { label: 'Inconnu', icon: '❓', color: 'text-slate-600', description: 'Origine non tracée (données antérieures au versioning)' },
}
const DIR_COLORS: Record<string, string> = {
bullish: 'text-emerald-400',
bearish: 'text-red-400',
@@ -454,19 +468,37 @@ function EventDetail({
</div>
)}
{/* Sources */}
{(detail.source_refs?.length ?? 0) > 0 && (
<div>
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">Sources</div>
{/* Origin + Sources */}
<div>
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">Provenance</div>
{/* Origin badge */}
{(() => {
const meta = ORIGIN_META[detail.origin ?? 'unknown'] ?? ORIGIN_META['unknown']
return (
<div className="flex items-start gap-2 bg-slate-800/50 rounded-lg px-3 py-2 mb-2 border border-slate-700/30">
<span className="text-base shrink-0">{meta.icon}</span>
<div className="flex-1 min-w-0">
<div className={`text-xs font-semibold ${meta.color}`}>{meta.label}</div>
<div className="text-xs text-slate-500 mt-0.5">{meta.description}</div>
</div>
</div>
)
})()}
{/* Source refs */}
{(detail.source_refs?.length ?? 0) > 0 ? (
<div className="space-y-1.5">
{detail.source_refs!.map((ref, i) => (
<div key={i} className="flex items-start gap-2 bg-slate-800/40 rounded-lg px-3 py-2">
<div key={i} className="flex items-start gap-2 bg-slate-800/30 rounded-lg px-3 py-2 border border-slate-800/60">
<div className="flex-1 min-w-0">
<div className="text-xs text-slate-200 font-medium truncate">{ref.title}</div>
<div className="flex items-center gap-2 mt-0.5">
<div className="text-xs text-slate-200 font-medium">{ref.title}</div>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
<span className="text-xs text-slate-500">{ref.source}</span>
<span className="text-xs text-slate-600">{ref.date}</span>
<span className="text-xs text-slate-600">score brut: {ref.original_score?.toFixed(2)}</span>
{ref.original_score > 0 && (
<span className="text-xs text-slate-600">
score brut: <span className="text-slate-400">{ref.original_score.toFixed(2)}</span>
</span>
)}
</div>
</div>
{ref.url && (
@@ -478,8 +510,12 @@ function EventDetail({
</div>
))}
</div>
</div>
)}
) : (
<div className="text-xs text-slate-600 italic px-3 py-2 bg-slate-800/20 rounded-lg border border-slate-800/40">
Aucune source liée cet événement ne référence pas de document ou d'article spécifique.
</div>
)}
</div>
{/* Instrument impacts */}
<div>
@@ -652,6 +688,11 @@ function EventRow({
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-slate-500">{ev.start_date}</span>
{ev.sub_type && <span className="text-xs text-slate-600">{ev.sub_type}</span>}
{ev.origin && (
<span className="text-xs shrink-0" title={(ORIGIN_META[ev.origin] ?? ORIGIN_META['unknown']).description}>
{(ORIGIN_META[ev.origin] ?? ORIGIN_META['unknown']).icon}
</span>
)}
</div>
</div>
<div className="shrink-0 text-right">