Files
OpenFin/frontend/src/pages/AnalyticsAdvanced.tsx
OpenSquared dcbc9f19fc feat: translate all UI strings to English for international release
Complete French→English translation across all frontend pages and backend
services — every label, button, header, empty state, toast, and nav item
is now in English. Build verified clean (tsc + vite). No i18n library
added; direct string replacement throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 09:06:37 +02:00

447 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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, 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 (
<div className="text-center py-10 text-slate-500">
<Brain className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>No Bayesian data yet</div>
<div className="text-xs mt-1">Posteriors are computed after mature trades (35% of the 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">Bayesian WR</th>
<th className="text-right py-2 px-2 font-medium">CI 95%</th>
<th className="text-right py-2 px-2 font-medium">Mature trades</th>
<th className="text-right py-2 px-2 font-medium">Drift</th>
<th className="text-center py-2 px-2 font-medium">Confidence</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) without mature trades GPT prior only
</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>No transitions detected yet</div>
<div className="text-xs mt-1">Clusters fill up as cycles run</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">
Transition probability from one regime to another (rows = source, columns = destination)
· {data.n_transitions} total transitions
</div>
<table className="text-xs border-collapse">
<thead>
<tr>
<th className="text-slate-500 p-2 font-normal text-right pr-3">From / To </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 = stays in same cluster · Green = dominant transition · Blue = self-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">
No cluster detected yet run a cycle to get started
</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 ? ' ⚠️ anomaly' : ''}`}
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">
Each square = 1 snapshot · White ring = anomaly detected · {data.length} total entries
</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">Current cluster</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" /> Anomaly
</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" />
No embeddings available they are generated automatically during pattern filtering
</div>
)
}
return (
<div className="space-y-2">
<div className="text-xs text-slate-500">{data.length} vectorized patterns</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">Class</th>
<th className="text-right py-1.5 px-2 font-medium">Model</th>
<th className="text-right py-1.5 px-2 font-medium">Updated</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('/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 (
<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" /> Advanced Analytics Phase 4
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Bayesian updating · Regime clustering · Semantic embeddings
</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')} />
Detect regime
</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">Active Bayesian patterns</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">Average Bayesian win rate</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 ?? 'No 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">Vectorized patterns</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" />
Bayesian posteriors by pattern
<span className="text-xs text-slate-500 font-normal">Beta(α,β) · CI 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">Reading:</strong> 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
</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" />
Cluster history
</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 days</option>
<option value={90}>90 days</option>
<option value={180}>180 days</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" />
Transition matrix
<span className="text-xs text-slate-500 font-normal">P(regime j | regime 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" />
Semantic embeddings
<span className="text-xs text-slate-500 font-normal">
text-embedding-3-small · Cosine similarity replaces 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">How it works:</strong> Each newly suggested pattern is vectorized and compared
to existing patterns via cosine similarity (threshold: 0.75). If similarity is &gt; 0.75,
the pattern is rejected as a semantic duplicate more precise than Jaccard which only sees common words.
</div>
</div>
</div>
)
}