'use client'; import { useState, useEffect } from 'react'; const GREEN = '#28CC95'; const RED = '#FF6B6B'; const BLUE = '#4A90D9'; export default function PaperDashboard() { const [data, setData] = useState(null); const [trades, setTrades] = useState({}); const [loading, setLoading] = useState(true); const [activeStrat, setActiveStrat] = useState(null); 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]); // Fetch trades for active strategy 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]); if (loading) { return (
Loading Paper Trading...
); } 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 (
{/* Header */}

Kalbot

PAPER
{data?.lastUpdate ? 'Live' : 'Offline'}
{/* Market Card (compact) */} {/* Strategy Tabs */}
{strategies.map(s => ( ))}
{/* Active Strategy View */} {activeStrat && activeStratData && ( )} {/* All Strategies Overview */}
{/* Worker Uptime */}
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 (

BTC Up or Down

15 min

{timeLeft && ( ⏱ {timeLeft} )}
Up {market.yesPct}%
Down {market.noPct}%
${(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 Stats */}

{strategy.name}

{strategy.mode}
= 0 ? '+' : ''}$${s.totalPnL}`} color={pnlColor} /> = 50 ? GREEN : RED} />
{/* Strategy Config */}
{strategy.config && Object.entries(strategy.config).map(([k, v]) => (
{k} {typeof v === 'number' ? v : String(v)}
))} {/* Strategy-specific extra fields */} {strategy.consecutiveLosses !== undefined && (
Consecutive Losses 0 ? 'text-red-400' : 'text-gray-300'}> {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-400' : 'text-red-400'}> {strategy.cycleWinRate}%
)} {strategy.paused && (
⚠️ PAUSED — max losses reached
)}
{/* Open Positions for this strategy */} {s.openPositions.length > 0 && (

Open Positions

{s.openPositions.map((t, i) => )}
)} {/* Trade History for this strategy */}

Trade History ({trades.length})

{trades.length === 0 ? (

No trades yet. Strategy is watching...

) : ( 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; // Sort by PnL descending 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 (

{label}

{value}

); } function TradeRow({ trade, isOpen }) { const won = trade.pnl > 0; const pnlColor = trade.pnl == null ? 'text-gray-400' : won ? 'text-green-400' : 'text-red-400'; return (
{isOpen ? ( ) : ( {won ? '✅' : '❌'} )} {trade.side} @ {trade.price}¢ ${trade.size}
{trade.pnl != null ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl}` : 'open'}
{trade.reason}
{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`; }