← back to Ken

react-dash/src/pages/Risk.jsx

256 lines

import React, { useState, useEffect } from 'react';
import { api, post } from '../api';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, RadialBarChart, RadialBar, Legend } from 'recharts';

// Risk presets calculated for $50 bankroll, 30-day experiment
const PRESETS = {
  conservative: {
    label: 'Conservative',
    color: 'blue',
    desc: 'Preserve capital. Small bets, strict limits.',
    icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
    config: {
      paper_mode: true,
      max_bet_usd: 0.25,
      max_daily_loss_usd: 2.00,
      max_open_exposure_usd: 3.00,
      max_drawdown_pct: 10,
      min_edge_threshold: 0.05,
      min_liquidity: 200,
      fractional_kelly: 0.05,
    },
    kelly_note: '5% Kelly -> ~$0.12 avg bet on 10% edge',
    survival: '99% chance to survive 30 days',
  },
  moderate: {
    label: 'Moderate',
    color: 'yellow',
    desc: 'Balanced risk/reward. Default for $50 experiment.',
    icon: 'M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3',
    config: {
      paper_mode: true,
      max_bet_usd: 0.50,
      max_daily_loss_usd: 5.00,
      max_open_exposure_usd: 5.00,
      max_drawdown_pct: 20,
      min_edge_threshold: 0.03,
      min_liquidity: 100,
      fractional_kelly: 0.10,
    },
    kelly_note: '10% Kelly -> ~$0.25 avg bet on 5% edge',
    survival: '90% chance to survive 30 days',
  },
  aggressive: {
    label: 'Aggressive',
    color: 'red',
    desc: 'Max growth. Higher risk of ruin.',
    icon: 'M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z',
    config: {
      paper_mode: true,
      max_bet_usd: 1.00,
      max_daily_loss_usd: 8.00,
      max_open_exposure_usd: 10.00,
      max_drawdown_pct: 35,
      min_edge_threshold: 0.02,
      min_liquidity: 50,
      fractional_kelly: 0.20,
    },
    kelly_note: '20% Kelly -> ~$0.50 avg bet on 5% edge',
    survival: '70% chance to survive 30 days',
  },
};

export default function Risk() {
  const [r, setR] = useState(null);
  const [loading, setL] = useState(true);
  const [applying, setApplying] = useState(null);

  const load = () => api('/risk').then(d => { setR(d.state); setL(false); });
  useEffect(() => { load(); const i = setInterval(load, 10000); return () => clearInterval(i); }, []);

  async function toggleKill() {
    const active = r?.kill_switch_active;
    if (!active) {
      const reason = prompt('Kill switch reason:');
      if (!reason) return;
      await post('/risk', { action: 'kill_switch_activate', reason });
    } else {
      await post('/risk', { action: 'kill_switch_deactivate' });
    }
    load();
  }

  async function updateCfg(key, val) {
    const cfg = { ...r.config, [key]: val };
    await post('/risk', { action: 'update_config', config: cfg });
    load();
  }

  async function applyPreset(key) {
    setApplying(key);
    const preset = PRESETS[key];
    await post('/risk', { action: 'update_config', config: preset.config });
    load();
    setTimeout(() => setApplying(null), 600);
  }

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

  const c = r?.config || {};
  const bankroll = parseFloat(r?.bankroll || 50);
  const exposure = parseFloat(r?.open_exposure || 0);
  const dailyPnl = parseFloat(r?.daily_pnl || 0);
  const drawdown = parseFloat(r?.max_drawdown_pct || 0);

  // Gauge data for risk metrics
  const gaugeData = [
    { name: 'Exposure', value: Math.min(100, (exposure / (c.max_open_exposure_usd || 5)) * 100), fill: '#3b82f6' },
    { name: 'Daily Loss', value: Math.min(100, (Math.abs(dailyPnl) / (c.max_daily_loss_usd || 5)) * 100), fill: dailyPnl < 0 ? '#ef4444' : '#10b981' },
    { name: 'Drawdown', value: Math.min(100, (drawdown / (c.max_drawdown_pct || 20)) * 100), fill: '#8b5cf6' },
  ];

  // Detect active preset
  const activePreset = Object.entries(PRESETS).find(([_, p]) =>
    p.config.fractional_kelly === c.fractional_kelly &&
    p.config.max_bet_usd === c.max_bet_usd
  )?.[0];

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div><h1 className="text-2xl font-bold">Risk Management</h1><p className="text-gray-500 text-sm">Controls, presets & kill switches</p></div>
        <button onClick={toggleKill} className={`btn ${r?.kill_switch_active ? 'btn-s' : 'btn-d'}`}>{r?.kill_switch_active ? 'Deactivate Kill Switch' : 'Activate Kill Switch'}</button>
      </div>

      {r?.kill_switch_active && (
        <div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4 flex items-center gap-3">
          <span className="text-2xl">&#128680;</span>
          <div><p className="font-semibold text-red-400">KILL SWITCH ACTIVE</p><p className="text-sm text-gray-400">{r.kill_reason}</p></div>
        </div>
      )}

      {/* Stats */}
      <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">Bankroll</p><p className="stat-v text-green-400">${bankroll.toFixed(2)}</p></div>
        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Daily P&L</p><p className={`stat-v ${dailyPnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>${dailyPnl.toFixed(2)}</p></div>
        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Exposure</p><p className="stat-v text-blue-400">${exposure.toFixed(2)}</p></div>
        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Drawdown</p><p className="stat-v text-purple-400">{drawdown.toFixed(1)}%</p></div>
      </div>

      {/* Risk Gauge Chart */}
      <div className="card">
        <h2 className="font-semibold mb-3">Risk Utilization</h2>
        <ResponsiveContainer width="100%" height={160}>
          <BarChart data={gaugeData} layout="vertical" barSize={20}>
            <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" horizontal={false} />
            <XAxis type="number" domain={[0, 100]} tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} />
            <YAxis type="category" dataKey="name" tick={{ fill: '#9ca3af', fontSize: 12 }} axisLine={false} tickLine={false} width={80} />
            <Tooltip formatter={(v) => [`${v.toFixed(0)}%`, 'Utilization']} contentStyle={{ background: '#111827', border: '1px solid #374151', borderRadius: 8, fontSize: 12 }} />
            <Bar dataKey="value" radius={[0, 4, 4, 0]}>
              {gaugeData.map((entry, i) => (
                <React.Fragment key={i}>
                  {React.createElement('rect', null)}
                </React.Fragment>
              ))}
            </Bar>
          </BarChart>
        </ResponsiveContainer>
      </div>

      {/* Risk Presets */}
      <div className="card" style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.03), rgba(139,92,246,0.03))' }}>
        <div className="mb-4">
          <h2 className="font-semibold">Risk Presets</h2>
          <p className="text-xs text-gray-500 mt-0.5">One-click profiles optimized for $50 bankroll / 30-day experiment</p>
        </div>
        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
          {Object.entries(PRESETS).map(([key, preset]) => {
            const isActive = activePreset === key;
            return (
              <button
                key={key}
                onClick={() => applyPreset(key)}
                disabled={applying === key}
                className={`text-left rounded-xl border p-4 transition-all ${isActive
                  ? `border-${preset.color}-500/50 bg-${preset.color}-500/10`
                  : 'border-gray-800 hover:border-gray-700 bg-gray-800/20 hover:bg-gray-800/40'
                  } ${applying === key ? 'opacity-50' : ''}`}
              >
                <div className="flex items-center gap-3 mb-3">
                  <div className={`w-10 h-10 rounded-lg flex items-center justify-center bg-${preset.color}-500/15`}>
                    <svg className={`w-5 h-5 text-${preset.color}-400`} fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d={preset.icon} /></svg>
                  </div>
                  <div>
                    <div className="flex items-center gap-2">
                      <p className="font-semibold">{preset.label}</p>
                      {isActive && <span className={`badge bg-${preset.color}`}>Active</span>}
                    </div>
                    <p className="text-xs text-gray-500">{preset.desc}</p>
                  </div>
                </div>
                <div className="space-y-1.5 text-xs">
                  <div className="flex justify-between"><span className="text-gray-500">Max Bet</span><span className="font-mono">${preset.config.max_bet_usd}</span></div>
                  <div className="flex justify-between"><span className="text-gray-500">Kelly Fraction</span><span className="font-mono">{preset.config.fractional_kelly}x</span></div>
                  <div className="flex justify-between"><span className="text-gray-500">Max Exposure</span><span className="font-mono">${preset.config.max_open_exposure_usd}</span></div>
                  <div className="flex justify-between"><span className="text-gray-500">Min Edge</span><span className="font-mono">{(preset.config.min_edge_threshold * 100)}%</span></div>
                  <div className="flex justify-between"><span className="text-gray-500">Max Drawdown</span><span className="font-mono">{preset.config.max_drawdown_pct}%</span></div>
                </div>
                <div className="mt-3 pt-3 border-t border-gray-800">
                  <p className="text-[10px] text-gray-500">{preset.kelly_note}</p>
                  <p className="text-[10px] text-gray-400 mt-0.5">{preset.survival}</p>
                </div>
              </button>
            );
          })}
        </div>
      </div>

      {/* Manual Config */}
      <div className="card">
        <h2 className="font-semibold mb-4">Risk Configuration</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
          {[
            { k: 'paper_mode', l: 'Paper Mode', t: 'toggle', desc: 'No real orders' },
            { k: 'max_bet_usd', l: 'Max Bet ($)', t: 'num', s: 0.05 },
            { k: 'max_daily_loss_usd', l: 'Max Daily Loss ($)', t: 'num', s: 0.5 },
            { k: 'max_open_exposure_usd', l: 'Max Exposure ($)', t: 'num', s: 0.5 },
            { k: 'max_drawdown_pct', l: 'Max Drawdown (%)', t: 'num', s: 1 },
            { k: 'min_edge_threshold', l: 'Min Edge', t: 'num', s: 0.005 },
            { k: 'min_liquidity', l: 'Min Liquidity ($)', t: 'num', s: 10 },
            { k: 'fractional_kelly', l: 'Kelly Fraction', t: 'num', s: 0.05 },
          ].map(item => (
            <div key={item.k} className="flex items-center justify-between bg-gray-800/30 rounded-lg p-3">
              <div><p className="text-sm font-medium">{item.l}</p>{item.desc && <p className="text-xs text-gray-500">{item.desc}</p>}</div>
              {item.t === 'toggle' ?
                <button onClick={() => updateCfg(item.k, !c[item.k])} className={`w-12 h-6 rounded-full relative ${c[item.k] ? 'bg-green-500' : 'bg-gray-600'}`}>
                  <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${c[item.k] ? 'left-6' : 'left-0.5'}`} />
                </button>
                : <input type="number" step={item.s} value={c[item.k] ?? ''} onChange={e => updateCfg(item.k, parseFloat(e.target.value))} className="w-24 bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-right font-mono" />}
            </div>
          ))}
        </div>
      </div>

      {/* Kelly Calculator */}
      <div className="card">
        <h2 className="font-semibold mb-3">Kelly Bet Calculator</h2>
        <p className="text-xs text-gray-500 mb-4">Suggested bet sizes for current config with $50 bankroll</p>
        <div className="grid grid-cols-3 md:grid-cols-5 gap-3">
          {[0.02, 0.03, 0.05, 0.08, 0.10].map(edge => {
            const kelly = c.fractional_kelly || 0.10;
            const fullKelly = edge / (1 - edge); // simplified for binary bet
            const betSize = Math.min(c.max_bet_usd || 0.50, bankroll * kelly * fullKelly);
            return (
              <div key={edge} className="bg-gray-800/30 rounded-lg p-3 text-center">
                <p className="text-xs text-gray-500 mb-1">{(edge * 100).toFixed(0)}% edge</p>
                <p className={`font-mono font-semibold ${betSize > 0 ? 'text-green-400' : 'text-gray-500'}`}>${betSize.toFixed(2)}</p>
                <p className="text-[10px] text-gray-600 mt-0.5">of ${bankroll.toFixed(0)}</p>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}