feat: Phase 2 — Pattern Reliability, Contre-thèses & Calibration probabiliste
Sprint 2.1 — Pattern Reliability Score - database.py: get_pattern_reliability() — win_rate × log(n+1) sur trades matures (≥35% horizon) - database.py: get_all_pattern_reliability_map() pour injection rapide dans les prompts - ai_analyzer.py: inject reliability_map dans suggest_patterns (patterns fiables mis en avant) - auto_cycle.py: charge reliability_map avant suggestion et le passe au suggéreur - routers/analytics.py: GET /api/analytics/reliability - PatternEditor.tsx: ReliabilityBadge sur chaque card + usePatternReliability hook - useApi.ts: usePatternReliability, useCalibration hooks Sprint 2.2 — Contre-thèses & Invalidation Triggers - database.py: migration ALTER TABLE — counter_thesis, invalidation_trigger, invalidation_probability - database.py: save_custom_pattern() persiste les 3 nouveaux champs - ai_analyzer.py: counter_thesis + invalidation_trigger + invalidation_probability dans le JSON schema - auto_cycle.py: détection automatique des triggers d'invalidation contre les news (keyword match) - routers/analytics.py: GET /api/analytics/invalidation-alerts - PatternEditor.tsx: affichage contre-thèse dans les cards + champs dans le formulaire - PatternEditor.tsx: affichage dans AiSuggestModal (suggestions IA) - routers/patterns.py: PatternRequest inclut les 3 nouveaux champs Sprint 2.3 — Calibration probabiliste & Demi-vie KB - database.py: migration — predicted_probability sur pattern_score_history - database.py: save_pattern_scores() stocke probability du pattern à chaque scoring run - database.py: get_calibration_data() — Brier score + buckets de calibration par décile - database.py: expires_at + confidence_decay_days sur knowledge_base - database.py: decay_kb_confidence() — decay automatique + archivage à 0 - auto_cycle.py: decay_kb_confidence() appelé au début de chaque cycle (non-bloquant) - routers/analytics.py: GET /api/analytics/calibration + POST /api/analytics/kb/decay - frontend/src/pages/Analytics.tsx: nouvelle page — tableau fiabilité + calibration Brier - App.tsx + Sidebar.tsx: route /analytics + entrée menu Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
245
frontend/src/pages/Analytics.tsx
Normal file
245
frontend/src/pages/Analytics.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { useState } from 'react'
|
||||
import { usePatternReliability, useCalibration } from '../hooks/useApi'
|
||||
import clsx from 'clsx'
|
||||
import { BarChart2, Target, TrendingUp, AlertTriangle } from 'lucide-react'
|
||||
|
||||
function ReliabilityTable({ data }: { data: any[] }) {
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="card text-center py-10 text-slate-500">
|
||||
<BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div>Aucune donnée de fiabilité</div>
|
||||
<div className="text-xs mt-1">Les données apparaissent après 3+ trades matures (≥35% de l'horizon écoulé) par pattern</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-slate-500 border-b border-slate-700/30">
|
||||
<th className="text-left py-2 pr-3 font-medium">Pattern</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Trades</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Win Rate</th>
|
||||
<th className="text-right py-2 px-2 font-medium">PnL moyen</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Max gain</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Max perte</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Score fiabilité</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{data.map((r: any) => {
|
||||
const wr = r.win_rate_pct
|
||||
const wrColor = wr >= 60 ? 'text-emerald-400' : wr >= 40 ? 'text-amber-400' : 'text-red-400'
|
||||
const rel = r.reliability_score
|
||||
const relColor = rel >= 1.5 ? 'text-emerald-400' : rel >= 0.8 ? 'text-amber-400' : 'text-red-400'
|
||||
return (
|
||||
<tr key={r.pattern_id} className="hover:bg-dark-700/30 transition-colors">
|
||||
<td className="py-2 pr-3">
|
||||
<div className="text-white font-medium truncate max-w-[200px]">{r.pattern_name}</div>
|
||||
<div className="text-slate-600 font-mono text-[10px]">{r.pattern_id}</div>
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-slate-300 font-mono">{r.trade_count}</td>
|
||||
<td className={clsx('text-right py-2 px-2 font-mono font-bold', wrColor)}>{wr}%</td>
|
||||
<td className={clsx('text-right py-2 px-2 font-mono', r.avg_pnl_pct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{r.avg_pnl_pct > 0 ? '+' : ''}{r.avg_pnl_pct}%
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 font-mono text-emerald-400/70">+{r.max_pnl_pct}%</td>
|
||||
<td className="text-right py-2 px-2 font-mono text-red-400/70">{r.max_loss_pct}%</td>
|
||||
<td className={clsx('text-right py-2 px-2 font-mono font-bold', relColor)}>
|
||||
{rel.toFixed(2)}
|
||||
<div className="text-[9px] text-slate-600 font-normal">WR × log(n+1)</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CalibrationSection({ data }: { data: any }) {
|
||||
if (!data) return null
|
||||
|
||||
const { brier_score, interpretation, buckets, sample_size } = data
|
||||
|
||||
if (!brier_score && sample_size === 0) {
|
||||
return (
|
||||
<div className="card text-center py-10 text-slate-500">
|
||||
<Target className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div>Pas encore de données de calibration</div>
|
||||
<div className="text-xs mt-1">Nécessite des trades matures avec probabilité stockée vs résultat réalisé</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const brierColor = brier_score < 0.15 ? 'text-emerald-400' : brier_score < 0.25 ? 'text-amber-400' : 'text-red-400'
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Score summary */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="card text-center">
|
||||
<div className={clsx('text-2xl font-bold font-mono', brierColor)}>{brier_score?.toFixed(3) ?? '—'}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Brier Score</div>
|
||||
<div className="text-xs text-slate-600">(0 = parfait, 1 = nul)</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-white">{sample_size}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Trades analysés</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className={clsx('text-sm font-bold mt-1', brierColor)}>{interpretation ?? '—'}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Interprétation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calibration buckets */}
|
||||
{buckets && buckets.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-2 font-medium">Calibration par décile (prédit vs réalisé)</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-slate-500 border-b border-slate-700/30">
|
||||
<th className="text-left py-1.5 pr-3 font-medium">Prob. prédite</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Taux réel</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Biais</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Trades</th>
|
||||
<th className="py-1.5 pl-3 font-medium">Barre</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{buckets.map((b: any) => {
|
||||
const bias = b.bias
|
||||
const biasColor = Math.abs(bias) < 0.05 ? 'text-emerald-400' : Math.abs(bias) < 0.15 ? 'text-amber-400' : 'text-red-400'
|
||||
const actualPct = Math.round(b.actual_rate * 100)
|
||||
const predPct = Math.round(b.predicted_mid * 100)
|
||||
return (
|
||||
<tr key={b.predicted_range}>
|
||||
<td className="py-1.5 pr-3 text-slate-400 font-mono">{b.predicted_range}</td>
|
||||
<td className="text-right py-1.5 px-2 font-mono text-white">{actualPct}%</td>
|
||||
<td className={clsx('text-right py-1.5 px-2 font-mono font-bold', biasColor)}>
|
||||
{bias > 0 ? '+' : ''}{(bias * 100).toFixed(1)}%
|
||||
</td>
|
||||
<td className="text-right py-1.5 px-2 text-slate-500">{b.count}</td>
|
||||
<td className="py-1.5 pl-3">
|
||||
<div className="flex items-center gap-1 h-4">
|
||||
{/* Predicted (grey) vs actual (colored) */}
|
||||
<div className="relative w-32 h-2 bg-dark-700 rounded-full overflow-hidden">
|
||||
<div className="absolute h-full bg-slate-600 rounded-full" style={{ width: `${predPct}%` }} />
|
||||
<div className={clsx('absolute h-full rounded-full opacity-80', actualPct >= predPct ? 'bg-emerald-500' : 'bg-red-500')}
|
||||
style={{ width: `${actualPct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 mt-2">
|
||||
Barre grise = prob. prédite · Barre colorée = taux réalisé · Vert si réel ≥ prédit, rouge sinon
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Analytics() {
|
||||
const { data: reliabilityData, isLoading: loadingR } = usePatternReliability()
|
||||
const [calDays, setCalDays] = useState(365)
|
||||
const { data: calData, isLoading: loadingC } = useCalibration(calDays)
|
||||
|
||||
const reliability: any[] = (reliabilityData as any)?.reliability ?? []
|
||||
const topPatterns = reliability.slice(0, 5)
|
||||
const bottomPatterns = reliability.filter((r: any) => r.trade_count >= 3).slice(-3)
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<BarChart2 className="w-5 h-5 text-blue-400" /> Analytics & Calibration
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Fiabilité historique des patterns · Calibration probabiliste · Brier score
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Summary KPIs */}
|
||||
{reliability.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<div className="card">
|
||||
<div className="text-2xl font-bold text-white font-mono">{reliability.length}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Patterns avec historique</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="text-2xl font-bold text-white font-mono">
|
||||
{reliability.reduce((s: number, r: any) => s + r.trade_count, 0)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Trades matures analysés</div>
|
||||
</div>
|
||||
{topPatterns[0] && (
|
||||
<div className="card border-emerald-700/30">
|
||||
<div className="text-xs text-slate-500 mb-1 flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3 text-emerald-400" /> Meilleur pattern
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-emerald-400 truncate">{topPatterns[0].pattern_name}</div>
|
||||
<div className="text-xs font-mono text-slate-400">{topPatterns[0].win_rate_pct}% WR</div>
|
||||
</div>
|
||||
)}
|
||||
{bottomPatterns[0] && (
|
||||
<div className="card border-red-700/30">
|
||||
<div className="text-xs text-slate-500 mb-1 flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3 text-red-400" /> À éviter
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-red-400 truncate">{bottomPatterns[0].pattern_name}</div>
|
||||
<div className="text-xs font-mono text-slate-400">{bottomPatterns[0].win_rate_pct}% WR</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reliability table */}
|
||||
<div className="card">
|
||||
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<BarChart2 className="w-4 h-4 text-blue-400" /> Fiabilité par pattern
|
||||
<span className="text-xs text-slate-500 font-normal">(trades ≥35% de l'horizon uniquement)</span>
|
||||
</div>
|
||||
{loadingR ? (
|
||||
<div className="space-y-2">{[1,2,3].map(i => <div key={i} className="h-8 bg-dark-700 animate-pulse rounded" />)}</div>
|
||||
) : (
|
||||
<ReliabilityTable data={reliability} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Calibration */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-blue-400" /> Calibration probabiliste
|
||||
</div>
|
||||
<select
|
||||
value={calDays}
|
||||
onChange={e => setCalDays(Number(e.target.value))}
|
||||
className="bg-dark-700 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
|
||||
>
|
||||
<option value={90}>90 jours</option>
|
||||
<option value={180}>180 jours</option>
|
||||
<option value={365}>1 an</option>
|
||||
<option value={730}>2 ans</option>
|
||||
</select>
|
||||
</div>
|
||||
{loadingC ? (
|
||||
<div className="space-y-2">{[1,2,3].map(i => <div key={i} className="h-8 bg-dark-700 animate-pulse rounded" />)}</div>
|
||||
) : (
|
||||
<CalibrationSection data={calData} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user