← 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">🚨</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>
);
}