Files
OpenFin/frontend/src/pages/AnalyticsAdvanced.tsx
OpenSquared 246deaf631 feat: Phase 4 — Moteur Probabiliste & Apprentissage Automatique
Sprint 4.1 — Bayesian Updating
- database.py: update_bayesian_posteriors() — Beta(α,β) posteriors sur trades matures
- database.py: get_bayesian_posteriors() — posteriors + IC 95% + dérive prior GPT vs posterior
- Colonnes Bayésiennes ajoutées : bayesian_alpha, bayesian_beta, bayesian_win_rate, bayesian_sample_size
- auto_cycle.py: appel update_bayesian_posteriors() en Step 5.5 (après scoring)

Sprint 4.2 — Détection Automatique de Régimes (K-Means numpy pur)
- database.py: detect_and_save_regime_clusters() — K-Means sur 7 gauges macro (VIX, slope, DXY…)
- database.py: get_regime_cluster_history() — timeline des clusters
- database.py: get_regime_transition_matrix() — P(cluster j | cluster i) sur N transitions
- Table regime_clusters avec anomaly_flag (points > 3σ)
- auto_cycle.py: appel detect_and_save_regime_clusters() en Step 5.6

Sprint 4.3 — Embeddings Sémantiques (remplace Jaccard)
- database.py: get_or_create_pattern_embedding() — OpenAI text-embedding-3-small, stocké en DB
- database.py: max_cosine_similarity_vs_existing() — similarité cosinus vs patterns existants
- Table pattern_embeddings avec vecteur JSON + model_version
- auto_cycle.py: _is_duplicate_pattern() — cosinus seuil 0.75 avec fallback Jaccard automatique

Sprint 4.4 — Tableau de Bord Analytique Avancé
- AnalyticsAdvanced.tsx: nouvelle page /analytics-advanced
  • BayesianTable : prior GPT vs WR bayésien ± IC 95%, dérive, niveau de confiance
  • ClusterTimeline : timeline colorée des clusters + anomalies
  • TransitionMatrix : heatmap P(j|i) avec diagonale auto-transition
  • EmbeddingsSummary : liste des patterns vectorisés
  • Boutons "Bayesian update" et "Détecter régime" avec mutation React Query
- analytics.py router : 5 nouveaux endpoints (bayesian, regime-clusters, transitions, detect, embeddings)
- useApi.ts : 4 nouveaux hooks (useBayesianPosteriors, useRegimeClusters, useRegimeTransitions, usePatternEmbeddings)
- App.tsx + Sidebar.tsx : route /analytics-advanced + entrée menu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:46:34 +02:00

451 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useBayesianPosteriors, useRegimeClusters, useRegimeTransitions, usePatternEmbeddings } from '../hooks/useApi'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import axios from 'axios'
import clsx from 'clsx'
import { Brain, GitBranch, Layers, Cpu, RefreshCw, AlertTriangle } from 'lucide-react'
const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:8000'
const api = axios.create({ baseURL: API_BASE })
// ── Bayesian Posteriors ───────────────────────────────────────────────────────
function BayesianTable({ data }: { data: any[] }) {
if (!data || data.length === 0) {
return (
<div className="text-center py-10 text-slate-500">
<Brain className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Pas encore de données bayésiennes</div>
<div className="text-xs mt-1">Les posteriors se calculent après des trades matures (35% de l'horizon)</div>
</div>
)
}
const withData = data.filter(d => d.sample_size > 0)
const withoutData = data.filter(d => d.sample_size === 0)
return (
<div className="space-y-4">
{withData.length > 0 && (
<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">Prior GPT</th>
<th className="text-right py-2 px-2 font-medium">WR Bayésien</th>
<th className="text-right py-2 px-2 font-medium">IC 95%</th>
<th className="text-right py-2 px-2 font-medium">Trades matures</th>
<th className="text-right py-2 px-2 font-medium">Dérive</th>
<th className="text-center py-2 px-2 font-medium">Confiance</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{withData.map((d: any) => {
const wr = d.bayesian_win_rate_pct
const wrColor = wr >= 60 ? 'text-emerald-400' : wr >= 40 ? 'text-amber-400' : 'text-red-400'
const drift = d.prior_vs_posterior_drift
const driftColor = Math.abs(drift) < 0.05 ? 'text-slate-400'
: drift > 0 ? 'text-emerald-400' : 'text-red-400'
const confColor = d.confidence_level === 'haute' ? 'text-emerald-400'
: d.confidence_level === 'moyenne' ? 'text-amber-400' : 'text-slate-500'
return (
<tr key={d.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-[180px]">{d.pattern_name}</div>
<div className="text-slate-600 font-mono text-[10px]">{d.asset_class ?? ''}</div>
</td>
<td className="text-right py-2 px-2 font-mono text-slate-400">
{Math.round(d.prior_probability * 100)}%
</td>
<td className={clsx('text-right py-2 px-2 font-mono font-bold', wrColor)}>
{wr}%
</td>
<td className="text-right py-2 px-2 font-mono text-slate-500 text-[10px]">
[{Math.round(d.lower_ci_95 * 100)}%{Math.round(d.upper_ci_95 * 100)}%]
</td>
<td className="text-right py-2 px-2 text-slate-300 font-mono">
{d.sample_size}
</td>
<td className={clsx('text-right py-2 px-2 font-mono', driftColor)}>
{drift >= 0 ? '+' : ''}{Math.round(drift * 100)}%
</td>
<td className={clsx('text-center py-2 px-2 text-[10px] font-bold uppercase', confColor)}>
{d.confidence_level}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
{withoutData.length > 0 && (
<div className="text-xs text-slate-600 mt-1">
{withoutData.length} pattern(s) sans trades matures — prior GPT uniquement
</div>
)}
</div>
)
}
// ── Regime Transition Matrix ──────────────────────────────────────────────────
function TransitionMatrix({ data }: { data: any }) {
if (!data || !data.matrix || Object.keys(data.matrix).length === 0) {
return (
<div className="text-center py-8 text-slate-500">
<GitBranch className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Pas encore de transitions détectées</div>
<div className="text-xs mt-1">Les clusters se remplissent au fil des cycles</div>
</div>
)
}
const { matrix, labels, cluster_ids } = data
const ids: number[] = cluster_ids ?? Object.keys(matrix).map(Number)
return (
<div className="overflow-x-auto">
<div className="text-xs text-slate-500 mb-2">
Probabilité de transition d'un régime à l'autre (lignes = source, colonnes = destination)
· {data.n_transitions} transitions totales
</div>
<table className="text-xs border-collapse">
<thead>
<tr>
<th className="text-slate-500 p-2 font-normal text-right pr-3">De ↓ / Vers →</th>
{ids.map(dst => (
<th key={dst} className="p-2 font-medium text-slate-300 text-center min-w-[90px]">
<div className="text-[10px] text-slate-500">C{dst}</div>
<div className="truncate max-w-[88px]">{(labels?.[dst] ?? `Cluster ${dst}`).split(' ').slice(0, 2).join(' ')}</div>
</th>
))}
</tr>
</thead>
<tbody>
{ids.map(src => (
<tr key={src} className="border-t border-slate-800/40">
<td className="p-2 text-right pr-3 font-medium text-slate-300">
<div className="text-[10px] text-slate-500">C{src}</div>
<div className="text-xs truncate max-w-[120px]">{(labels?.[src] ?? `Cluster ${src}`).split(' ').slice(0, 2).join(' ')}</div>
</td>
{ids.map(dst => {
const prob = matrix?.[src]?.[dst] ?? 0
const pct = Math.round(prob * 100)
const bg = src === dst
? 'bg-blue-500/20 text-blue-300'
: pct >= 50 ? 'bg-emerald-500/20 text-emerald-300'
: pct >= 25 ? 'bg-amber-500/10 text-amber-300'
: 'text-slate-600'
return (
<td key={dst} className={clsx('p-2 text-center font-mono font-bold rounded', bg)}>
{pct > 0 ? `${pct}%` : ''}
</td>
)
})}
</tr>
))}
</tbody>
</table>
<div className="text-[10px] text-slate-600 mt-2">
Diagonal = reste dans le même cluster · Vert = transition dominante · Bleu = auto-transition
</div>
</div>
)
}
// ── Cluster History Timeline ──────────────────────────────────────────────────
const CLUSTER_COLORS: Record<number, string> = {
0: 'bg-red-500',
1: 'bg-emerald-500',
2: 'bg-orange-500',
3: 'bg-yellow-500',
4: 'bg-slate-400',
}
function ClusterTimeline({ data }: { data: any[] }) {
if (!data || data.length === 0) {
return (
<div className="text-center py-6 text-slate-500 text-xs">
Aucun cluster détecté encore — lancez un cycle pour démarrer
</div>
)
}
// Afficher les 30 derniers points
const recent = [...data].reverse().slice(0, 30)
return (
<div className="space-y-2">
<div className="flex gap-1 flex-wrap">
{Object.entries(CLUSTER_COLORS).map(([k, col]) => (
<span key={k} className="flex items-center gap-1 text-[10px] text-slate-400">
<span className={clsx('w-2 h-2 rounded-full inline-block', col)} />
C{k}
</span>
))}
</div>
<div className="flex gap-0.5 flex-wrap">
{recent.map((r: any, i: number) => (
<div
key={i}
title={`${r.timestamp?.slice(0, 16)} · ${r.cluster_label} · ${r.dominant_regime}${r.anomaly_flag ? ' anomalie' : ''}`}
className={clsx(
'w-5 h-5 rounded cursor-default',
CLUSTER_COLORS[r.cluster_id] ?? 'bg-slate-600',
r.anomaly_flag ? 'ring-2 ring-white/50' : ''
)}
/>
))}
</div>
<div className="text-[10px] text-slate-600">
Chaque carré = 1 snapshot · Blanc cerclé = anomalie détectée · {data.length} entrées au total
</div>
{/* Latest cluster detail */}
{data[0] && (
<div className="mt-3 p-3 rounded bg-dark-700/60 border border-slate-700/30 text-xs">
<div className="text-slate-400 mb-1">Cluster actuel</div>
<div className="flex items-center gap-2">
<span className={clsx('w-3 h-3 rounded-full', CLUSTER_COLORS[data[0].cluster_id])} />
<span className="text-white font-semibold">{data[0].cluster_label}</span>
<span className="text-slate-500">({data[0].dominant_regime})</span>
{data[0].anomaly_flag ? (
<span className="flex items-center gap-1 text-amber-400 font-bold">
<AlertTriangle className="w-3 h-3" /> Anomalie
</span>
) : null}
</div>
<div className="text-slate-600 mt-0.5">{data[0].timestamp?.slice(0, 16)}</div>
</div>
)}
</div>
)
}
// ── Embeddings Summary ────────────────────────────────────────────────────────
function EmbeddingsSummary({ data }: { data: any[] }) {
if (!data || data.length === 0) {
return (
<div className="text-center py-6 text-slate-500 text-xs">
<Cpu className="w-6 h-6 mx-auto mb-1 opacity-20" />
Aucun embedding disponible — ils se génèrent automatiquement lors du filtrage des patterns
</div>
)
}
return (
<div className="space-y-2">
<div className="text-xs text-slate-500">{data.length} patterns vectorisés</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">Pattern</th>
<th className="text-left py-1.5 px-2 font-medium">Classe</th>
<th className="text-right py-1.5 px-2 font-medium">Modèle</th>
<th className="text-right py-1.5 px-2 font-medium">Mis à jour</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/30">
{data.slice(0, 20).map((e: any, i: number) => (
<tr key={i} className="hover:bg-dark-700/20">
<td className="py-1.5 pr-3 text-white truncate max-w-[200px]">{e.name ?? e.pattern_id}</td>
<td className="py-1.5 px-2 text-slate-400">{e.asset_class ?? ''}</td>
<td className="py-1.5 px-2 text-right font-mono text-slate-500">{e.model_version}</td>
<td className="py-1.5 px-2 text-right text-slate-600">{e.updated_at?.slice(0, 10)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
// ── Main Page ─────────────────────────────────────────────────────────────────
export default function AnalyticsAdvanced() {
const [regimeDays, setRegimeDays] = useState(90)
const qc = useQueryClient()
const { data: bayesData, isLoading: loadingBayes } = useBayesianPosteriors()
const { data: clustersData, isLoading: loadingClusters } = useRegimeClusters(regimeDays)
const { data: transData, isLoading: loadingTrans } = useRegimeTransitions(180)
const { data: embData, isLoading: loadingEmb } = usePatternEmbeddings()
const posteriors: any[] = (bayesData as any)?.posteriors ?? []
const clusters: any[] = (clustersData as any)?.clusters ?? []
const embeddings: any[] = (embData as any)?.embeddings ?? []
const bayesUpdate = useMutation({
mutationFn: () => api.post('/api/analytics/bayesian/update').then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['bayesian-posteriors'] }),
})
const regimeDetect = useMutation({
mutationFn: () => api.post('/api/analytics/regime-detect').then(r => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['regime-clusters'] })
qc.invalidateQueries({ queryKey: ['regime-transitions'] })
},
})
const patternsWithData = posteriors.filter((p: any) => p.sample_size > 0)
const avgBayesWR = patternsWithData.length
? Math.round(patternsWithData.reduce((s: number, p: any) => s + p.bayesian_win_rate_pct, 0) / patternsWithData.length)
: null
const latestCluster = clusters[0]
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Brain className="w-5 h-5 text-purple-400" /> Analytics Avancées — Phase 4
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Bayesian updating · Clustering de régimes · Embeddings sémantiques
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => bayesUpdate.mutate()}
disabled={bayesUpdate.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-purple-600/20 hover:bg-purple-600/30 border border-purple-500/30 text-purple-300 rounded transition-colors disabled:opacity-50"
>
<RefreshCw className={clsx('w-3 h-3', bayesUpdate.isPending && 'animate-spin')} />
Bayesian update
</button>
<button
onClick={() => regimeDetect.mutate()}
disabled={regimeDetect.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 text-blue-300 rounded transition-colors disabled:opacity-50"
>
<RefreshCw className={clsx('w-3 h-3', regimeDetect.isPending && 'animate-spin')} />
Détecter régime
</button>
</div>
</div>
{/* KPIs */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="card">
<div className="text-2xl font-bold text-purple-400 font-mono">
{patternsWithData.length}
</div>
<div className="text-xs text-slate-500 mt-1">Patterns bayésiens actifs</div>
</div>
<div className="card">
<div className="text-2xl font-bold text-white font-mono">
{avgBayesWR !== null ? `${avgBayesWR}%` : ''}
</div>
<div className="text-xs text-slate-500 mt-1">Win rate bayésien moyen</div>
</div>
<div className="card">
<div className={clsx('text-2xl font-bold font-mono', latestCluster ? CLUSTER_COLORS[latestCluster.cluster_id]?.replace('bg-', 'text-') ?? 'text-slate-400' : 'text-slate-500')}>
{latestCluster ? `C${latestCluster.cluster_id}` : ''}
</div>
<div className="text-xs text-slate-500 mt-1">
{latestCluster?.cluster_label ?? 'Aucun cluster'}
</div>
</div>
<div className="card">
<div className="text-2xl font-bold text-blue-400 font-mono">{embeddings.length}</div>
<div className="text-xs text-slate-500 mt-1">Patterns vectorisés</div>
</div>
</div>
{/* Bayesian Posteriors */}
<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">
<Brain className="w-4 h-4 text-purple-400" />
Posteriors bayésiens par pattern
<span className="text-xs text-slate-500 font-normal">Beta(α,β) · IC 95%</span>
</div>
{bayesUpdate.data && (
<span className="text-xs text-emerald-400">{bayesUpdate.data.message}</span>
)}
</div>
{loadingBayes ? (
<div className="space-y-2">{[1,2,3].map(i => <div key={i} className="h-8 bg-dark-700 animate-pulse rounded" />)}</div>
) : (
<BayesianTable data={posteriors} />
)}
<div className="mt-3 p-3 bg-dark-700/40 rounded text-xs text-slate-500 border border-slate-700/20">
<strong className="text-slate-400">Lecture :</strong> Prior GPT = probabilité annoncée par GPT-4o ·
WR Bayésien = win rate observé avec lissage Laplace (β a priori faible) ·
Dérive = écart posterior prior · IC 95% basé sur la distribution Beta
</div>
</div>
{/* Regime Clustering */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Cluster timeline */}
<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">
<Layers className="w-4 h-4 text-blue-400" />
Historique des clusters
</div>
<select
value={regimeDays}
onChange={e => setRegimeDays(Number(e.target.value))}
className="bg-dark-700 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
>
<option value={30}>30 jours</option>
<option value={90}>90 jours</option>
<option value={180}>180 jours</option>
</select>
</div>
{loadingClusters ? (
<div className="h-20 bg-dark-700 animate-pulse rounded" />
) : (
<ClusterTimeline data={clusters} />
)}
</div>
{/* Transition matrix */}
<div className="card">
<div className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
<GitBranch className="w-4 h-4 text-blue-400" />
Matrice de transition
<span className="text-xs text-slate-500 font-normal">P(régime j | régime i)</span>
</div>
{loadingTrans ? (
<div className="h-32 bg-dark-700 animate-pulse rounded" />
) : (
<TransitionMatrix data={transData} />
)}
</div>
</div>
{/* Embeddings */}
<div className="card">
<div className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
<Cpu className="w-4 h-4 text-emerald-400" />
Embeddings sémantiques
<span className="text-xs text-slate-500 font-normal">
text-embedding-3-small · Similarité cosinus remplace Jaccard
</span>
</div>
{loadingEmb ? (
<div className="h-20 bg-dark-700 animate-pulse rounded" />
) : (
<EmbeddingsSummary data={embeddings} />
)}
<div className="mt-3 p-3 bg-dark-700/40 rounded text-xs text-slate-500 border border-slate-700/20">
<strong className="text-slate-400">Comment ça marche :</strong> Chaque nouveau pattern suggéré est vectorisé et comparé
aux patterns existants via similarité cosinus (seuil : 0.75). Si la similarité est &gt; 0.75,
le pattern est rejeté comme doublon sémantique plus précis que Jaccard qui ne voit que les mots communs.
</div>
</div>
</div>
)
}