import { useState } from 'react'
import { useBayesianPosteriors, useRegimeClusters, useRegimeTransitions, usePatternEmbeddings, api } from '../hooks/useApi'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import clsx from 'clsx'
import { Brain, GitBranch, Layers, Cpu, RefreshCw, AlertTriangle } from 'lucide-react'
// ── Bayesian Posteriors ───────────────────────────────────────────────────────
function BayesianTable({ data }: { data: any[] }) {
if (!data || data.length === 0) {
return (
No Bayesian data yet
Posteriors are computed after mature trades (≥35% of the horizon)
)
}
const withData = data.filter(d => d.sample_size > 0)
const withoutData = data.filter(d => d.sample_size === 0)
return (
{withData.length > 0 && (
Pattern
Prior GPT
Bayesian WR
CI 95%
Mature trades
Drift
Confidence
{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 (
{d.pattern_name}
{d.asset_class ?? '—'}
{Math.round(d.prior_probability * 100)}%
{wr}%
[{Math.round(d.lower_ci_95 * 100)}%–{Math.round(d.upper_ci_95 * 100)}%]
{d.sample_size}
{drift >= 0 ? '+' : ''}{Math.round(drift * 100)}%
{d.confidence_level}
)
})}
)}
{withoutData.length > 0 && (
{withoutData.length} pattern(s) without mature trades — GPT prior only
)}
)
}
// ── Regime Transition Matrix ──────────────────────────────────────────────────
function TransitionMatrix({ data }: { data: any }) {
if (!data || !data.matrix || Object.keys(data.matrix).length === 0) {
return (
No transitions detected yet
Clusters fill up as cycles run
)
}
const { matrix, labels, cluster_ids } = data
const ids: number[] = cluster_ids ?? Object.keys(matrix).map(Number)
return (
Transition probability from one regime to another (rows = source, columns = destination)
· {data.n_transitions} total transitions
From ↓ / To →
{ids.map(dst => (
C{dst}
{(labels?.[dst] ?? `Cluster ${dst}`).split(' ').slice(0, 2).join(' ')}
))}
{ids.map(src => (
C{src}
{(labels?.[src] ?? `Cluster ${src}`).split(' ').slice(0, 2).join(' ')}
{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 (
{pct > 0 ? `${pct}%` : '—'}
)
})}
))}
Diagonal = stays in same cluster · Green = dominant transition · Blue = self-transition
)
}
// ── Cluster History Timeline ──────────────────────────────────────────────────
const CLUSTER_COLORS: Record = {
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 (
No cluster detected yet — run a cycle to get started
)
}
// Afficher les 30 derniers points
const recent = [...data].reverse().slice(0, 30)
return (
{Object.entries(CLUSTER_COLORS).map(([k, col]) => (
C{k}
))}
{recent.map((r: any, i: number) => (
))}
Each square = 1 snapshot · White ring = anomaly detected · {data.length} total entries
{/* Latest cluster detail */}
{data[0] && (
Current cluster
{data[0].cluster_label}
({data[0].dominant_regime})
{data[0].anomaly_flag ? (
Anomaly
) : null}
{data[0].timestamp?.slice(0, 16)}
)}
)
}
// ── Embeddings Summary ────────────────────────────────────────────────────────
function EmbeddingsSummary({ data }: { data: any[] }) {
if (!data || data.length === 0) {
return (
No embeddings available — they are generated automatically during pattern filtering
)
}
return (
{data.length} vectorized patterns
Pattern
Class
Model
Updated
{data.slice(0, 20).map((e: any, i: number) => (
{e.name ?? e.pattern_id}
{e.asset_class ?? '—'}
{e.model_version}
{e.updated_at?.slice(0, 10)}
))}
)
}
// ── 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('/analytics/bayesian/update').then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['bayesian-posteriors'] }),
})
const regimeDetect = useMutation({
mutationFn: () => api.post('/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 (
{/* Header */}
Advanced Analytics — Phase 4
Bayesian updating · Regime clustering · Semantic embeddings
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"
>
Bayesian update
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"
>
Detect regime
{/* KPIs */}
{patternsWithData.length}
Active Bayesian patterns
{avgBayesWR !== null ? `${avgBayesWR}%` : '—'}
Average Bayesian win rate
{latestCluster ? `C${latestCluster.cluster_id}` : '—'}
{latestCluster?.cluster_label ?? 'No cluster'}
{embeddings.length}
Vectorized patterns
{/* Bayesian Posteriors */}
Bayesian posteriors by pattern
Beta(α,β) · CI 95%
{bayesUpdate.data && (
{bayesUpdate.data.message}
)}
{loadingBayes ? (
) : (
)}
Reading: GPT Prior = probability stated by GPT-4o ·
Bayesian WR = observed win rate with Laplace smoothing (weak β prior) ·
Drift = posterior − prior gap · CI 95% based on Beta distribution
{/* Regime Clustering */}
{/* Cluster timeline */}
Cluster history
setRegimeDays(Number(e.target.value))}
className="bg-dark-700 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
>
30 days
90 days
180 days
{loadingClusters ? (
) : (
)}
{/* Transition matrix */}
Transition matrix
P(regime j | regime i)
{loadingTrans ? (
) : (
)}
{/* Embeddings */}
Semantic embeddings
text-embedding-3-small · Cosine similarity replaces Jaccard
{loadingEmb ? (
) : (
)}
How it works: Each newly suggested pattern is vectorized and compared
to existing patterns via cosine similarity (threshold: 0.75). If similarity is > 0.75,
the pattern is rejected as a semantic duplicate — more precise than Jaccard which only sees common words.
)
}