'use client';
import { useState, useEffect } from 'react';
const GREEN = '#16A34A';
const RED = '#DC2626';
const BLUE = '#2563EB';
export default function PaperDashboard() {
const [data, setData] = useState(null);
const [trades, setTrades] = useState({});
const [loading, setLoading] = useState(true);
const [activeStrat, setActiveStrat] = useState(null);
const [resetting, setResetting] = useState(false);
useEffect(() => {
const fetchState = async () => {
try {
const res = await fetch('/api/state');
const json = await res.json();
setData(json);
if (!activeStrat && json.strategies?.length) {
setActiveStrat(json.strategies[0].name);
}
setLoading(false);
} catch (e) {
console.error('State fetch error:', e);
}
};
fetchState();
const interval = setInterval(fetchState, 2000);
return () => clearInterval(interval);
}, [activeStrat]);
useEffect(() => {
if (!activeStrat) return;
const fetchTrades = async () => {
try {
const res = await fetch(`/api/trades?strategy=${encodeURIComponent(activeStrat)}`);
const json = await res.json();
setTrades(prev => ({ ...prev, [activeStrat]: json.trades || [] }));
} catch (e) {
console.error('Trades fetch error:', e);
}
};
fetchTrades();
const interval = setInterval(fetchTrades, 10000);
return () => clearInterval(interval);
}, [activeStrat]);
const handleReset = async () => {
if (!confirm('Reset ALL paper trading data? This clears all history, stats, and open positions for every strategy.')) return;
setResetting(true);
try {
await fetch('/api/reset', { method: 'POST' });
setTrades({});
} catch (e) {
console.error('Reset error:', e);
}
setTimeout(() => setResetting(false), 2000);
};
if (loading) {
return (
);
}
const market = data?.market;
const strategies = data?.strategies || [];
const paperByStrategy = data?.paperByStrategy || {};
const activeStratData = strategies.find(s => s.name === activeStrat);
const activeStats = paperByStrategy[activeStrat];
const activeTrades = trades[activeStrat] || [];
return (
Kalbot
PAPER
๐ฐ Live
{data?.lastUpdate ? 'Live' : 'Offline'}
{strategies.map(s => (
))}
{activeStrat && activeStratData && (
)}
Worker: {formatUptime(data?.workerUptime)}
{data?.lastUpdate ? new Date(data.lastUpdate).toLocaleTimeString() : 'never'}
);
}
function MarketCardCompact({ market }) {
if (!market) {
return (
No active market โ waiting...
);
}
const timeLeft = market.closeTime ? getTimeLeft(market.closeTime) : null;
return (
{timeLeft && (
โฑ {timeLeft}
)}
โฟ
${(market.volume || 0).toLocaleString()} vol
{market.ticker}
);
}
function StrategyDetailView({ strategy, stats, trades }) {
const s = stats || { balance: 1000, totalPnL: 0, wins: 0, losses: 0, winRate: 0, totalTrades: 0, openPositions: [] };
const pnlColor = s.totalPnL >= 0 ? GREEN : RED;
return (
{strategy.name}
{strategy.mode}
= 0 ? '+' : ''}$${s.totalPnL}`} color={pnlColor} />
= 50 ? GREEN : RED} />
{strategy.config && Object.entries(strategy.config).map(([k, v]) => (
{k}
{typeof v === 'number' ? v : String(v)}
))}
{strategy.consecutiveLosses !== undefined && (
Consecutive Losses
0 ? 'text-red-600' : 'text-gray-700'}>
{strategy.consecutiveLosses}
)}
{strategy.currentBetSize !== undefined && (
Next Bet
${strategy.currentBetSize}
)}
{strategy.round !== undefined && (
Cycle Round
{strategy.round}/{strategy.maxRounds}
)}
{strategy.cycleWins !== undefined && (
Cycles Won/Lost
{strategy.cycleWins}W / {strategy.cycleLosses}L
)}
{strategy.cycleWinRate !== undefined && (
Cycle Win Rate
= 50 ? 'text-green-600' : 'text-red-600'}>
{strategy.cycleWinRate}%
)}
{strategy.paused && (
โ ๏ธ PAUSED โ max losses reached
)}
{s.openPositions.length > 0 && (
Open Positions ({s.openPositions.length})
{s.openPositions.map((t, i) => )}
)}
Trade History ({trades.length})
{trades.length === 0 ? (
No settled trades yet.
) : (
trades.map((t, i) =>
)
)}
);
}
function AllStrategiesOverview({ paperByStrategy, strategies }) {
const entries = strategies.map(s => ({
name: s.name,
stats: paperByStrategy[s.name] || { balance: 1000, totalPnL: 0, winRate: 0, totalTrades: 0 },
enabled: s.enabled,
paused: s.paused
}));
if (!entries.length) return null;
entries.sort((a, b) => b.stats.totalPnL - a.stats.totalPnL);
return (
๐ Strategy Leaderboard
{entries.map((e, i) => {
const pnlColor = e.stats.totalPnL >= 0 ? GREEN : RED;
return (
{i + 1}.
{e.name}
{e.stats.totalTrades} trades
{e.stats.winRate}% wr
{e.stats.totalPnL >= 0 ? '+' : ''}${e.stats.totalPnL}
);
})}
);
}
function StatBox({ label, value, color }) {
return (
);
}
function TradeRow({ trade, isOpen }) {
const won = trade.result && trade.side.toLowerCase() === trade.result.toLowerCase();
const isNeutral = trade.result === 'cancelled' || trade.result === 'expired';
const pnlColor = trade.pnl == null ? 'text-gray-400' : trade.pnl > 0 ? 'text-green-600' : trade.pnl < 0 ? 'text-red-600' : 'text-gray-600';
const price = Number(trade.price);
const cost = Number(trade.cost ?? trade.size);
const hasExpected = isOpen && Number.isFinite(price) && price > 0 && price < 100 && Number.isFinite(cost) && cost > 0;
const grossPayout = hasExpected ? (100 / price) * cost : null;
const winPnL = hasExpected ? grossPayout - cost : null;
const losePnL = hasExpected ? -cost : null;
return (
{isOpen ? (
) : (
{isNeutral ? 'โ' : won ? 'โ
' : 'โ'}
)}
{trade.side}
@ {trade.price}ยข
${trade.size}
{trade.pnl != null ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl}` : 'open'}
{trade.reason}
{hasExpected && (
If win: +${winPnL.toFixed(2)}
If lose: ${losePnL.toFixed(2)}
Gross payout: ${grossPayout.toFixed(2)}
)}
{trade.result && !isOpen && (
Result: {trade.result}
)}
{!trade.result && }
{trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString() : ''}
);
}
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`;
}