← back to Ken
kalshi-dash/src/pages/Trading.jsx
395 lines
import React, { useState, useEffect } from 'react';
import { api, post } from '../api';
/* ─── Stat card ─── */
function StatCard({ label, value, color }) {
return (
<div className="card">
<p className="stat-label mb-2">{label}</p>
<p className="stat-v" style={{ color }}>{value}</p>
</div>
);
}
/* ─── Config row ─── */
function ConfigRow({ label, value, highlight }) {
return (
<div
className="flex justify-between items-center px-3 py-2 rounded"
style={{ background: 'var(--surface-el)' }}
>
<span className="text-sm" style={{ color: 'var(--text-muted)' }}>{label}</span>
<span
className="mono text-sm font-semibold"
style={{ color: highlight || 'var(--text)' }}
>
{value}
</span>
</div>
);
}
/* ─── Env toggle button ─── */
function EnvBtn({ label, active, color, onClick, disabled }) {
return (
<button
onClick={onClick}
disabled={disabled}
className="flex-1 py-2 rounded-lg text-sm font-bold transition"
style={{
background: active ? `${color}22` : 'var(--surface-el)',
color: active ? color : 'var(--text-muted)',
border: `1px solid ${active ? color + '55' : 'var(--border)'}`,
}}
>
{label}
</button>
);
}
/* ─── Main Trading page ─── */
export default function Trading() {
const [mode, setMode] = useState(null);
const [history, setHistory] = useState(null);
const [system, setSystem] = useState(null);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState('');
useEffect(() => {
load();
const i = setInterval(load, 15000);
return () => clearInterval(i);
}, []);
async function load() {
try {
const [m, h, s] = await Promise.all([
post('/trading/mode', { action: 'get' }),
api('/trading/history'),
api('/system'),
]);
setMode(m);
setHistory(h);
setSystem(s);
} catch {}
setLoading(false);
}
async function doAction(action, body = {}) {
setActionLoading(action);
try {
await post('/trading/mode', { action, ...body });
await load();
} catch {}
setActionLoading('');
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div
className="animate-spin w-8 h-8 border-2 border-t-transparent rounded-full pulse-cyan"
style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
/>
</div>
);
}
const tc = mode?.trade_config || {};
const ts = mode?.trader_state || {};
const summary = history?.summary || {};
const trades = history?.trades || [];
const mem = system?.memory || {};
const totalPnl = parseInt(summary.total_pnl || 0);
const pnlColor = totalPnl > 0 ? 'var(--green)' : totalPnl < 0 ? 'var(--red)' : 'var(--text-muted)';
return (
<div className="space-y-6 fade-in">
{/* ── Page Header ── */}
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-3">
{/* Dollar icon */}
<span className="text-2xl" style={{ color: 'var(--green)' }}>$</span>
<h1 className="heading text-3xl font-bold" style={{ color: 'var(--text)' }}>
Trading
</h1>
{/* Environment badge */}
<span className={`badge ${mode?.env === 'prod' ? 'bg-green' : 'bg-orange'}`}>
{(mode?.env || 'demo').toUpperCase()}
</span>
{mode?.safe_mode && (
<span className="badge bg-orange">SAFE MODE</span>
)}
{mode?.auto_trader_enabled && (
<span className="badge bg-green">AUTO ON</span>
)}
</div>
<p className="section-subtitle mt-1">Mode control, execution, and P&L</p>
</div>
<button onClick={load} className="btn btn-o btn-sm">↻ Refresh</button>
</div>
{/* ── Trading Controls ── */}
<div className="grid grid-cols-3 gap-4">
{/* Environment */}
<div className="card-glow">
<p className="stat-label mb-3">Environment</p>
<div className="flex gap-2 mb-3">
<EnvBtn
label="DEMO"
active={mode?.env === 'demo'}
color="var(--orange)"
onClick={() => doAction('switch_env', { env: 'demo' })}
disabled={actionLoading === 'switch_env'}
/>
<EnvBtn
label="PROD"
active={mode?.env === 'prod'}
color="var(--green)"
onClick={() => doAction('switch_env', { env: 'prod' })}
disabled={actionLoading === 'switch_env'}
/>
</div>
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>
API keys: {mode?.kalshi_connected ? (
<span style={{ color: 'var(--green)' }}>Configured</span>
) : (
<span style={{ color: 'var(--red)' }}>Not set</span>
)}
</p>
</div>
{/* Safe mode */}
<div className="card-glow">
<p className="stat-label mb-3">Safe Mode</p>
<button
onClick={() => doAction('toggle_safe_mode')}
disabled={actionLoading === 'toggle_safe_mode'}
className="btn w-full btn-lg font-bold transition"
style={{
background: mode?.safe_mode ? 'rgba(247,147,26,0.15)' : 'var(--red-dim)',
color: mode?.safe_mode ? 'var(--orange)' : 'var(--red)',
border: `1px solid ${mode?.safe_mode ? 'rgba(247,147,26,0.4)' : 'rgba(255,92,92,0.4)'}`,
}}
>
{actionLoading === 'toggle_safe_mode' ? '...' : (
mode?.safe_mode ? 'SAFE MODE — ON' : 'SAFE MODE — OFF'
)}
</button>
<p className="text-xs mt-2" style={{ color: 'var(--text-muted)' }}>
{mode?.safe_mode ? 'No real orders will be placed' : (
<span style={{ color: 'var(--red)' }}>Real orders will be executed!</span>
)}
</p>
</div>
{/* Auto trader */}
<div className="card-glow">
<p className="stat-label mb-3">Auto Trader</p>
<button
onClick={() => doAction('toggle_auto_trader')}
disabled={actionLoading === 'toggle_auto_trader'}
className="btn w-full btn-lg font-bold transition"
style={{
background: mode?.auto_trader_enabled ? 'var(--green-dim)' : 'var(--surface-el)',
color: mode?.auto_trader_enabled ? 'var(--green)' : 'var(--text-muted)',
border: `1px solid ${mode?.auto_trader_enabled ? 'rgba(0,195,137,0.4)' : 'var(--border)'}`,
}}
>
{actionLoading === 'toggle_auto_trader' ? '...' : (
mode?.auto_trader_enabled ? 'AUTO TRADER: ON' : 'AUTO TRADER: OFF'
)}
</button>
<p className="text-xs mt-2" style={{ color: 'var(--text-muted)' }}>
Min confidence: <span className="mono">{tc.min_confidence}%</span>
·
Budget: <span className="mono">${(tc.budget_cents / 100).toFixed(0)}</span>
</p>
</div>
</div>
{/* ── Portfolio Stats ── */}
<div className="grid grid-cols-6 gap-3">
<StatCard label="Total Trades" value={summary.total_trades || 0} color="var(--orange)" />
<StatCard label="Open" value={summary.open_trades || 0} color="var(--blue)" />
<StatCard label="Wins" value={summary.winning_trades || 0} color="var(--green)" />
<StatCard label="Losses" value={summary.losing_trades || 0} color="var(--red)" />
<StatCard label="Total P&L" value={summary.total_pnl ? `$${(totalPnl / 100).toFixed(2)}` : '$0.00'} color={pnlColor} />
<StatCard label="Exposure" value={`$${(parseInt(summary.open_exposure || 0) / 100).toFixed(2)}`} color="var(--purple)" />
</div>
{/* ── Config + System ── */}
<div className="grid grid-cols-2 gap-4">
{/* Trade Config */}
<div className="card">
<p className="section-title mb-3" style={{ fontSize: '1rem' }}>
<span style={{ color: 'var(--cyan)' }}>⚙</span> Trade Config
</p>
<div className="space-y-2">
<ConfigRow label="Budget" value={`$${(tc.budget_cents / 100).toFixed(0)}`} highlight="var(--cyan)" />
<ConfigRow label="Max per trade" value={`$${(tc.max_position_cents / 100).toFixed(0)}`} />
<ConfigRow label="Max positions" value={tc.max_open_positions} />
<ConfigRow label="Daily loss limit" value={`$${(tc.daily_loss_limit_cents / 100).toFixed(0)}`} highlight="var(--red)" />
<ConfigRow label="Min confidence" value={`${tc.min_confidence}%`} highlight="var(--orange)" />
<ConfigRow label="Take profit" value={`${tc.take_profit_pct}%`} highlight="var(--green)" />
<ConfigRow label="Stop loss" value={`${tc.stop_loss_pct}%`} highlight="var(--red)" />
</div>
</div>
{/* System Health */}
<div className="card">
<p className="section-title mb-3" style={{ fontSize: '1rem' }}>
<span style={{ color: 'var(--cyan)' }}>♥</span> System Health
</p>
<div className="space-y-2">
<ConfigRow label="Memory (RSS)" value={`${mem.rss_mb || '?'} MB`} />
<ConfigRow label="Heap Used" value={`${mem.heapUsed_mb || '?'} MB`} />
<ConfigRow label="Heap Total" value={`${mem.heapTotal_mb || '?'} MB`} />
<ConfigRow label="Uptime" value={`${system?.uptime_hours || '?'}h`} highlight="var(--blue)" />
<ConfigRow label="Cached Markets" value={system?.cachedMarkets || 0} highlight="var(--cyan)" />
<ConfigRow label="PID" value={system?.pid || '?'} />
<ConfigRow
label="Available Budget"
value={`$${((ts.available || 0) / 100).toFixed(2)}`}
highlight="var(--green)"
/>
</div>
</div>
</div>
{/* ── Trade History ── */}
<div className="card overflow-x-auto" style={{ padding: 0 }}>
{/* Table header */}
<div
className="flex items-center justify-between px-5 py-4"
style={{ borderBottom: '1px solid var(--border)' }}
>
<p className="section-title" style={{ fontSize: '1rem' }}>
<span style={{ color: 'var(--orange)' }}>◈</span> Trade History
</p>
{trades.length > 0 && (
<span className="badge bg-orange">{trades.length} trades</span>
)}
</div>
<table>
<thead>
<tr>
<th>Time</th>
<th>Market</th>
<th>Side</th>
<th>Qty</th>
<th>Entry</th>
<th>Exit</th>
<th>P&L</th>
<th>Type</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{trades.length === 0 ? (
<tr>
<td colSpan={9} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
<div className="flex flex-col items-center gap-2">
<span className="text-2xl" style={{ color: 'var(--text-muted)' }}>$</span>
<span className="text-sm">No trades yet — signals are generating, waiting for execution</span>
</div>
</td>
</tr>
) : (
trades.map((t, i) => {
const pnl = parseInt(t.pnl_cents) || 0;
const pnlColor = pnl > 0 ? 'var(--green)' : pnl < 0 ? 'var(--red)' : 'var(--text-muted)';
const statusClass =
t.status === 'open' ? 'bg-blue' :
t.status === 'profit' ? 'bg-green' :
t.status === 'stopped' ? 'bg-red' : 'bg-orange';
return (
<tr key={i}>
{/* Time */}
<td className="whitespace-nowrap">
<span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
{new Date(t.created_at).toLocaleDateString()}{' '}
{new Date(t.created_at).toLocaleTimeString()}
</span>
</td>
{/* Market */}
<td className="max-w-[200px]">
<a
href={`https://kalshi.com/search?query=${encodeURIComponent((t.ticker || '').split('-')[0])}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm truncate block hover:underline transition heading"
style={{ color: 'var(--cyan)' }}
>
{t.title || t.ticker}
</a>
</td>
{/* Side */}
<td>
<span className={`badge ${t.side === 'yes' ? 'bg-green' : 'bg-red'}`}>
{(t.side || '').toUpperCase()}
</span>
</td>
{/* Qty */}
<td>
<span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
{t.count}
</span>
</td>
{/* Entry price */}
<td>
<span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
{t.price_cents}c
</span>
</td>
{/* Exit price */}
<td>
<span className="mono text-sm" style={{ color: t.exit_price ? 'var(--text-dim)' : 'var(--text-muted)' }}>
{t.exit_price ? `${t.exit_price}c` : '—'}
</span>
</td>
{/* P&L */}
<td>
<span className="mono text-sm font-bold" style={{ color: pnlColor }}>
{pnl !== 0
? `${pnl > 0 ? '+' : ''}$${(pnl / 100).toFixed(2)}`
: '—'
}
</span>
</td>
{/* Signal type */}
<td>
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>
{t.signal_type}
</span>
</td>
{/* Status */}
<td>
<span className={`badge ${statusClass}`}>{t.status}</span>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
);
}