Files
KalBot/app/dash/page.js

366 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
const GREEN = '#16A34A';
const RED = '#DC2626';
const BLUE = '#2563EB';
const AMBER = '#D97706';
export default function LiveDashboard() {
const [data, setData] = useState(null);
const [trades, setTrades] = useState([]);
const [loading, setLoading] = useState(true);
const [toggling, setToggling] = useState(null);
const [activeTrade, setActiveTrade] = useState(null);
useEffect(() => {
const fetchState = async () => {
try {
const res = await fetch('/api/live-state');
const json = await res.json();
setData(json);
setLoading(false);
} catch (e) {
console.error('State fetch error:', e);
}
};
fetchState();
const interval = setInterval(fetchState, 2000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const fetchTrades = async () => {
try {
const res = await fetch('/api/live-trades');
const json = await res.json();
setTrades(json.trades || []);
} catch (e) {
console.error('Trades fetch error:', e);
}
};
fetchTrades();
const interval = setInterval(fetchTrades, 10000);
return () => clearInterval(interval);
}, []);
const sendCommand = async (action, strategy = null) => {
setToggling(strategy || action);
try {
await fetch('/api/live-toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, strategy })
});
} catch (e) {
console.error('Command error:', e);
}
setTimeout(() => setToggling(null), 1500);
};
if (loading) {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<div className="text-green-400 text-lg animate-pulse">Loading Live Trading...</div>
</div>
);
}
const market = data?.market;
const live = data?.live || {};
const strategies = data?.strategies || [];
const enabledSet = new Set(live.enabledStrategies || []);
return (
<div className="min-h-screen bg-gray-950 text-gray-100 font-sans pb-20">
{/* Header */}
<header className="sticky top-0 z-50 bg-gray-900/95 backdrop-blur border-b border-gray-800 px-4 py-3">
<div className="flex items-center justify-between max-w-lg mx-auto">
<div className="flex items-center gap-2">
<h1 className="text-lg font-bold text-green-400">Kalbot</h1>
<span className="text-xs bg-red-900/50 text-red-400 px-2 py-0.5 rounded-full font-bold border border-red-800">LIVE</span>
</div>
<div className="flex items-center gap-3">
<a href="/paper" className="text-[10px] px-2 py-1 rounded bg-gray-800 text-gray-400 border border-gray-700 hover:bg-gray-700 transition-colors">
📝 Paper
</a>
<div className="flex items-center gap-1.5">
<span className={`w-2 h-2 rounded-full ${data?.lastUpdate ? 'bg-green-500 animate-pulse' : 'bg-red-500'}`} />
<span className="text-xs text-gray-500">{data?.lastUpdate ? 'Live' : 'Offline'}</span>
</div>
</div>
</div>
</header>
<main className="max-w-lg mx-auto px-4 pt-4 space-y-4">
{/* Kill Switch + Balance */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<div className="flex items-center justify-between mb-3">
<div>
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Kalshi Balance</p>
<p className="text-2xl font-bold text-white">
{live.balance != null ? `$${live.balance.toFixed(2)}` : '—'}
</p>
{live.portfolioValue != null && (
<p className="text-[10px] text-gray-500">Portfolio: ${live.portfolioValue.toFixed(2)}</p>
)}
</div>
<button
onClick={() => sendCommand(live.paused ? 'resume' : 'pause')}
disabled={toggling === 'pause' || toggling === 'resume'}
className={`px-4 py-2 rounded-lg font-bold text-sm transition-all ${
live.paused
? 'bg-green-600 hover:bg-green-500 text-white'
: 'bg-red-600 hover:bg-red-500 text-white animate-pulse'
} disabled:opacity-50`}
>
{live.paused ? '▶ Resume' : '⏸ Kill Switch'}
</button>
</div>
<div className="grid grid-cols-4 gap-2">
<StatBox label="PnL" value={`${live.totalPnL >= 0 ? '+' : ''}$${live.totalPnL?.toFixed(2) || '0.00'}`} color={live.totalPnL >= 0 ? GREEN : RED} dark />
<StatBox label="Win Rate" value={`${live.winRate || 0}%`} color={(live.winRate || 0) >= 50 ? GREEN : RED} dark />
<StatBox label="Trades" value={live.totalTrades || 0} dark />
<StatBox label="Daily Loss" value={`$${live.dailyLoss?.toFixed(2) || '0.00'}`} color={live.dailyLoss > 0 ? AMBER : null} dark />
</div>
{live.paused && (
<div className="mt-3 text-xs text-red-400 bg-red-900/30 border border-red-800 rounded-lg p-2 text-center font-medium">
TRADING PAUSED No new orders will be placed
</div>
)}
<div className="flex justify-between text-[10px] text-gray-600 mt-3">
<span>Per-trade cap: ${live.maxPerTrade?.toFixed(2) || '5.00'}</span>
<span>Daily limit: ${live.maxDailyLoss?.toFixed(2) || '20.00'}</span>
</div>
</div>
{/* Market Card */}
<MarketCard market={market} />
{/* Strategy Toggles */}
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
<div className="px-4 py-3 border-b border-gray-800">
<h3 className="text-xs text-gray-400 uppercase tracking-wider font-bold"> Strategy Controls</h3>
</div>
{strategies.map((s, i) => {
const isEnabled = enabledSet.has(s.name);
const isToggling = toggling === s.name;
return (
<div key={s.name} className={`flex items-center justify-between px-4 py-3 ${i < strategies.length - 1 ? 'border-b border-gray-800' : ''}`}>
<div className="flex items-center gap-3">
<span className={`w-2 h-2 rounded-full ${isEnabled ? 'bg-green-500' : 'bg-gray-600'}`} />
<div>
<p className="text-sm font-medium capitalize text-gray-200">{s.name}</p>
{s.config && (
<p className="text-[10px] text-gray-600">
${s.config.betSize || 1}/trade {s.config.cooldownMs ? `${(s.config.cooldownMs/1000).toFixed(0)}s cd` : ''}
</p>
)}
</div>
</div>
<button
onClick={() => sendCommand(isEnabled ? 'disable' : 'enable', s.name)}
disabled={isToggling}
className={`relative w-12 h-6 rounded-full transition-all duration-300 ${
isEnabled ? 'bg-green-600' : 'bg-gray-700'
} disabled:opacity-50`}
>
<span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all duration-300 ${
isEnabled ? 'left-6' : 'left-0.5'
}`} />
</button>
</div>
);
})}
{strategies.length === 0 && (
<p className="text-gray-600 text-xs text-center py-4">No strategies loaded yet.</p>
)}
</div>
{/* Open Orders */}
{live.openOrders?.length > 0 && (
<div>
<h4 className="text-[10px] text-gray-500 uppercase tracking-wider mb-2 font-bold">
Open Orders ({live.openOrders.length})
</h4>
{live.openOrders.map((o, i) => (
<LiveOrderRow key={o.orderId || i} order={o} isOpen />
))}
</div>
)}
{/* Kalshi Positions */}
{live.positions?.length > 0 && (
<div>
<h4 className="text-[10px] text-gray-500 uppercase tracking-wider mb-2 font-bold">
Kalshi Positions ({live.positions.length})
</h4>
{live.positions.map((p, i) => (
<div key={i} className="bg-gray-900 rounded-lg p-3 border border-gray-800 mb-2">
<div className="flex items-center justify-between">
<span className="text-xs font-mono text-gray-300">{p.market_ticker || p.ticker}</span>
<span className="text-xs text-gray-400">{p.position_fp || p.position || 0} contracts</span>
</div>
{p.realized_pnl_dollars && (
<p className="text-[10px] text-gray-500 mt-1">
Realized PnL: <span className={Number(p.realized_pnl_dollars) >= 0 ? 'text-green-400' : 'text-red-400'}>
${Number(p.realized_pnl_dollars).toFixed(2)}
</span>
</p>
)}
</div>
))}
</div>
)}
{/* Trade History */}
<div>
<h4 className="text-[10px] text-gray-500 uppercase tracking-wider mb-2 font-bold">
Trade History ({trades.length})
</h4>
{trades.length === 0 ? (
<p className="text-gray-600 text-xs text-center py-4">No settled trades yet. Enable a strategy to start.</p>
) : (
trades.map((t, i) => <LiveOrderRow key={i} order={t} />)
)}
</div>
</main>
{/* Footer */}
<div className="fixed bottom-0 left-0 right-0 bg-gray-900/95 backdrop-blur border-t border-gray-800 py-2 px-4">
<div className="max-w-lg mx-auto flex justify-between text-xs text-gray-600">
<span>Worker: {formatUptime(data?.workerUptime)}</span>
<span>{data?.lastUpdate ? new Date(data.lastUpdate).toLocaleTimeString() : 'never'}</span>
</div>
</div>
</div>
);
}
function MarketCard({ market }) {
if (!market) {
return (
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-gray-600 text-center text-sm">No active market waiting...</p>
</div>
);
}
const timeLeft = market.closeTime ? getTimeLeft(market.closeTime) : null;
return (
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="font-bold text-sm text-gray-100">BTC Up or Down</h2>
<p className="text-[10px] text-gray-600">15 min</p>
</div>
<div className="flex items-center gap-2">
{timeLeft && (
<span className="text-[10px] bg-gray-800 px-2 py-0.5 rounded-full text-gray-400"> {timeLeft}</span>
)}
<span className="text-lg"></span>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex-1">
<div className="flex justify-between text-xs mb-1">
<span className="text-gray-400">Up</span>
<span style={{ color: GREEN }} className="font-medium">{market.yesPct}%</span>
</div>
<div className="w-full bg-gray-800 rounded-full h-1.5">
<div className="h-1.5 rounded-full transition-all duration-500" style={{ width: `${market.yesPct}%`, backgroundColor: GREEN }} />
</div>
</div>
<div className="flex-1">
<div className="flex justify-between text-xs mb-1">
<span className="text-gray-400">Down</span>
<span style={{ color: BLUE }} className="font-medium">{market.noPct}%</span>
</div>
<div className="w-full bg-gray-800 rounded-full h-1.5">
<div className="h-1.5 rounded-full transition-all duration-500" style={{ width: `${market.noPct}%`, backgroundColor: BLUE }} />
</div>
</div>
</div>
<div className="flex justify-between text-[10px] text-gray-600 mt-2">
<span>${(market.volume || 0).toLocaleString()} vol</span>
<span className="font-mono">{market.ticker}</span>
</div>
</div>
);
}
function LiveOrderRow({ order, isOpen }) {
const won = order.result && order.side?.toLowerCase() === order.result?.toLowerCase();
const isNeutral = order.result === 'cancelled' || order.result === 'expired';
const rawPnl = order?.pnl != null ? Number(order.pnl) : null;
const pnlVal = Number.isFinite(rawPnl)
? (Number.isInteger(rawPnl) ? rawPnl / 100 : rawPnl)
: null;
const pnlColor = pnlVal == null ? 'text-gray-600' : pnlVal > 0 ? 'text-green-400' : pnlVal < 0 ? 'text-red-400' : 'text-gray-400';
return (
<div className="bg-gray-900 rounded-lg p-3 border border-gray-800 mb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{isOpen ? (
<span className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" />
) : (
<span>{isNeutral ? '' : won ? '✅' : '❌'}</span>
)}
<span className="text-sm font-medium capitalize text-gray-200">{order.side}</span>
<span className="text-xs text-gray-500">@ {order.priceCents || order.price}¢</span>
<span className="text-[10px] text-gray-600">{order.contracts || 1}x</span>
</div>
<span className={`text-sm font-bold ${pnlColor}`}>
{pnlVal != null ? `${pnlVal >= 0 ? '+' : ''}$${pnlVal.toFixed(2)}` : 'open'}
</span>
</div>
<div className="flex justify-between mt-1">
<span className="text-[10px] text-gray-600">{order.reason}</span>
<span className="text-[10px] text-gray-600 capitalize">{order.strategy}</span>
</div>
{order.result && !isOpen && (
<div className="flex justify-between items-center mt-0.5">
<span className="text-[10px] text-gray-600">Result: {order.result}</span>
<span className="text-[10px] text-gray-600">
{order.settleTime ? new Date(order.settleTime).toLocaleTimeString() : order.createdAt ? new Date(order.createdAt).toLocaleTimeString() : ''}
</span>
</div>
)}
</div>
);
}
function StatBox({ label, value, color, dark }) {
return (
<div className={`rounded-lg p-2 border text-center ${dark ? 'bg-gray-800 border-gray-700' : 'bg-gray-50 border-gray-100'}`}>
<p className="text-[9px] text-gray-500 uppercase tracking-wider">{label}</p>
<p className="text-xs font-bold mt-0.5" style={color ? { color } : { color: dark ? '#F3F4F6' : '#111827' }}>{value}</p>
</div>
);
}
function getTimeLeft(closeTime) {
const diff = new Date(closeTime).getTime() - Date.now();
if (diff <= 0) return 'Closing...';
const mins = Math.floor(diff / 60000);
const secs = Math.floor((diff % 60000) / 1000);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function formatUptime(seconds) {
if (!seconds) return '0s';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}h ${m}m`;
if (m > 0) return `${m}m ${s}s`;
return `${s}s`;
}