Files
OpenFin/frontend/src/pages/Analytics.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

246 lines
11 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 { 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>No reliability data</div>
<div className="text-xs mt-1">Data appears after 3+ mature trades (35% of horizon elapsed) per 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">Avg PnL</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 loss</th>
<th className="text-right py-2 px-2 font-medium">Reliability score</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>No calibration data yet</div>
<div className="text-xs mt-1">Requires mature trades with stored probability vs realized outcome</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 = perfect, 1 = null)</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 analyzed</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">Interpretation</div>
</div>
</div>
{/* Calibration buckets */}
{buckets && buckets.length > 0 && (
<div>
<div className="text-xs text-slate-500 mb-2 font-medium">Calibration by decile (predicted vs realized)</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">Predicted prob.</th>
<th className="text-right py-1.5 px-2 font-medium">Actual rate</th>
<th className="text-right py-1.5 px-2 font-medium">Bias</th>
<th className="text-right py-1.5 px-2 font-medium">Trades</th>
<th className="py-1.5 pl-3 font-medium">Bar</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">
Grey bar = predicted prob. · Colored bar = realized rate · Green if actual predicted, red otherwise
</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">
Historical pattern reliability · Probabilistic calibration · 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 with history</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">Mature trades analyzed</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" /> Best 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" /> To avoid
</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" /> Reliability by pattern
<span className="text-xs text-slate-500 font-normal">(trades 35% of horizon only)</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" /> Probabilistic calibration
</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 days</option>
<option value={180}>180 days</option>
<option value={365}>1 year</option>
<option value={730}>2 years</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>
)
}