← back to Ken
react-dash/src/pages/Models.jsx
163 lines
import React, { useState, useEffect } from 'react';
import { api, post } from '../api';
import { ScatterChart, Scatter, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell, ZAxis, Legend } from 'recharts';
const ChartTooltip = ({ active, payload }) => {
if (!active || !payload?.length) return null;
const d = payload[0]?.payload;
if (!d) return null;
return (
<div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
<p className="text-gray-400 mb-1 font-mono">{d.id}</p>
<p className="text-green-400">P(Yes): {d.pYes?.toFixed(1)}%</p>
<p className={d.edge >= 0 ? 'text-green-400' : 'text-red-400'}>Edge: {d.edge?.toFixed(2)}%</p>
<p className="text-blue-400">Confidence: {d.confidence?.toFixed(0)}%</p>
<p className="text-purple-400">Signal: {d.signal}</p>
</div>
);
};
export default function Models() {
const [models, setMo] = useState([]);
const [preds, setPr] = useState([]);
const [loading, setL] = useState(true);
useEffect(() => { api('/models').then(d => { setMo(d.models || []); setPr(d.predictions || []); setL(false); }).catch(() => setL(false)); }, []);
async function run() {
const r = await post('/models', { action: 'run_predictions' });
window.location.reload();
}
if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
// Scatter data: P(Yes) vs Edge
const scatterData = preds.filter(p => p.edge != null && p.p_yes != null).map(p => ({
id: (p.condition_id || '').substring(0, 12),
pYes: parseFloat((p.p_yes * 100).toFixed(1)),
edge: parseFloat((p.edge * 100).toFixed(2)),
confidence: parseFloat(((p.confidence || 0.5) * 100).toFixed(0)),
signal: p.recommendation || 'HOLD',
}));
const buyData = scatterData.filter(d => d.signal.startsWith('BUY'));
const holdData = scatterData.filter(d => !d.signal.startsWith('BUY') && d.signal !== 'SKIP');
const skipData = scatterData.filter(d => d.signal === 'SKIP');
// Edge distribution bar
const edgeBars = scatterData.map(d => ({
name: d.id,
edge: d.edge,
})).sort((a, b) => b.edge - a.edge);
// Confidence histogram
const confBuckets = [0, 0, 0, 0, 0]; // 0-20, 20-40, 40-60, 60-80, 80-100
scatterData.forEach(d => {
const idx = Math.min(Math.floor(d.confidence / 20), 4);
confBuckets[idx]++;
});
const confData = confBuckets.map((count, i) => ({
range: `${i * 20}-${(i + 1) * 20}%`,
count,
}));
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div><h1 className="text-2xl font-bold">Models & Predictions</h1><p className="text-gray-500 text-sm">Probabilistic forecasting pipeline</p></div>
<button onClick={run} className="btn btn-p">Run Predictions</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Model Versions</p><p className="stat-v text-blue-400">{models.length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Predictions</p><p className="stat-v text-purple-400">{preds.length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Active</p><p className="stat-v text-green-400">{models.filter(m => m.active).length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Buy Signals</p><p className="stat-v text-green-400">{buyData.length}</p></div>
</div>
{/* Charts Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* P(Yes) vs Edge Scatter */}
<div className="card">
<h2 className="font-semibold mb-3">P(Yes) vs Edge</h2>
{scatterData.length > 0 ? (
<ResponsiveContainer width="100%" height={280}>
<ScatterChart margin={{ top: 10, right: 10, bottom: 10, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis type="number" dataKey="pYes" name="P(Yes)" unit="%" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} domain={[0, 100]} />
<YAxis type="number" dataKey="edge" name="Edge" unit="%" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
<ZAxis type="number" dataKey="confidence" range={[40, 200]} />
<Tooltip content={<ChartTooltip />} />
{buyData.length > 0 && <Scatter name="BUY" data={buyData} fill="#10b981" fillOpacity={0.8} />}
{holdData.length > 0 && <Scatter name="HOLD" data={holdData} fill="#eab308" fillOpacity={0.8} />}
{skipData.length > 0 && <Scatter name="SKIP" data={skipData} fill="#ef4444" fillOpacity={0.6} />}
<Legend wrapperStyle={{ fontSize: 11, color: '#9ca3af' }} />
</ScatterChart>
</ResponsiveContainer>
) : <p className="text-gray-500 text-sm text-center py-16">Run predictions to see chart</p>}
</div>
{/* Edge Distribution */}
<div className="card">
<h2 className="font-semibold mb-3">Edge Distribution</h2>
{edgeBars.length > 0 ? (
<ResponsiveContainer width="100%" height={280}>
<BarChart data={edgeBars} barSize={20}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="name" tick={{ fill: '#6b7280', fontSize: 9 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} />
<Tooltip formatter={(v) => [`${v}%`, 'Edge']} contentStyle={{ background: '#111827', border: '1px solid #374151', borderRadius: 8, fontSize: 12 }} />
<Bar dataKey="edge" name="Edge %">
{edgeBars.map((entry, i) => (
<Cell key={i} fill={entry.edge > 0 ? '#10b981' : '#ef4444'} fillOpacity={0.8} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
) : <p className="text-gray-500 text-sm text-center py-16">No predictions yet</p>}
</div>
</div>
{/* Confidence Distribution */}
{scatterData.length > 0 && (
<div className="card">
<h2 className="font-semibold mb-3">Confidence Distribution</h2>
<ResponsiveContainer width="100%" height={160}>
<BarChart data={confData} barSize={40}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="range" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} allowDecimals={false} />
<Tooltip formatter={(v) => [v, 'Predictions']} contentStyle={{ background: '#111827', border: '1px solid #374151', borderRadius: 8, fontSize: 12 }} />
<Bar dataKey="count" fill="#8b5cf6" fillOpacity={0.7} radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
)}
{/* Model Versions Table */}
<div className="card"><h2 className="font-semibold mb-3">Model Versions</h2>
<table><thead><tr><th>ID</th><th>Type</th><th>Tag</th><th>Status</th><th>Created</th></tr></thead><tbody>
{models.map(m => <tr key={m.id}><td className="font-mono">#{m.id}</td><td>{m.model_type}</td><td>{m.version_tag}</td>
<td><span className={`badge ${m.active ? 'bg-green' : 'bg-yellow'}`}>{m.active ? 'Active' : 'Off'}</span></td>
<td className="text-gray-400 text-sm">{new Date(m.created_at).toLocaleDateString()}</td></tr>)}
</tbody></table>
</div>
{/* Predictions Table */}
<div className="card"><h2 className="font-semibold mb-3">Predictions</h2>
<table><thead><tr><th>Market</th><th>P(Yes)</th><th>Confidence</th><th>Edge</th><th>Signal</th><th>Time</th></tr></thead><tbody>
{preds.length === 0 ? <tr><td colSpan={6} className="text-center text-gray-500 py-8">No predictions yet</td></tr>
: preds.map((p, i) => <tr key={i}>
<td className="font-mono text-xs max-w-[140px] truncate">{p.question || p.condition_id?.substring(0, 20)}</td>
<td className="font-mono text-green-400">{(p.p_yes * 100).toFixed(1)}%</td>
<td className="font-mono">{p.confidence ? (p.confidence * 100).toFixed(0) + '%' : '-'}</td>
<td className={`font-mono ${(p.edge || 0) > 0 ? 'text-green-400' : 'text-red-400'}`}>{p.edge ? (p.edge * 100).toFixed(2) + '%' : '-'}</td>
<td><span className={`badge ${p.recommendation?.startsWith('BUY') ? 'bg-green' : p.recommendation === 'SKIP' ? 'bg-red' : 'bg-yellow'}`}>{p.recommendation || '-'}</span></td>
<td className="text-gray-400 text-sm">{new Date(p.asof_ts).toLocaleString()}</td>
</tr>)}
</tbody></table>
</div>
</div>
);
}