← back to Bertha

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>
  );
}