Files
KalBot/app/paper/page.js

382 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.

'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 (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="text-[#28CC95] text-lg animate-pulse">Loading Paper Trading...</div>
</div>
);
}
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 (
<div className="min-h-screen bg-[#0a0a0a] text-white font-sans pb-20">
{/* Header */}
<header className="sticky top-0 z-50 bg-[#0a0a0a]/95 backdrop-blur border-b border-white/10 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" style={{ color: GREEN }}>Kalbot</h1>
<span className="text-xs bg-yellow-500/20 text-yellow-400 px-2 py-0.5 rounded-full font-medium">PAPER</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${data?.lastUpdate ? 'bg-green-400 animate-pulse' : 'bg-red-500'}`} />
<span className="text-xs text-gray-400">
{data?.lastUpdate ? 'Live' : 'Offline'}
</span>
</div>
</div>
</header>
<main className="max-w-lg mx-auto px-4 pt-4 space-y-4">
{/* Market Card (compact) */}
<MarketCardCompact market={market} />
{/* Strategy Tabs */}
<div className="flex gap-1 bg-white/5 rounded-lg p-1 overflow-x-auto">
{strategies.map(s => (
<button
key={s.name}
onClick={() => setActiveStrat(s.name)}
className={`flex-shrink-0 py-2 px-3 rounded-md text-xs font-medium transition-all whitespace-nowrap ${
activeStrat === s.name
? 'bg-white/10 text-white'
: 'text-gray-500 hover:text-gray-300'
}`}
>
<span className={`inline-block w-1.5 h-1.5 rounded-full mr-1.5 ${
s.enabled && !s.paused ? 'bg-green-400' : 'bg-red-500'
}`} />
{s.name}
</button>
))}
</div>
{/* Active Strategy View */}
{activeStrat && activeStratData && (
<StrategyDetailView
strategy={activeStratData}
stats={activeStats}
trades={activeTrades}
/>
)}
{/* All Strategies Overview */}
<AllStrategiesOverview paperByStrategy={paperByStrategy} strategies={strategies} />
</main>
{/* Worker Uptime */}
<div className="fixed bottom-0 left-0 right-0 bg-[#0a0a0a]/95 backdrop-blur border-t border-white/5 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 MarketCardCompact({ market }) {
if (!market) {
return (
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-gray-500 text-center text-sm">No active market waiting...</p>
</div>
);
}
const timeLeft = market.closeTime ? getTimeLeft(market.closeTime) : null;
return (
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="font-bold text-sm">BTC Up or Down</h2>
<p className="text-[10px] text-gray-500">15 min</p>
</div>
<div className="flex items-center gap-2">
{timeLeft && (
<span className="text-[10px] bg-white/10 px-2 py-0.5 rounded-full text-gray-300"> {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>Up</span>
<span style={{ color: GREEN }}>{market.yesPct}%</span>
</div>
<div className="w-full bg-white/10 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>Down</span>
<span style={{ color: BLUE }}>{market.noPct}%</span>
</div>
<div className="w-full bg-white/10 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 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 (
<div className="space-y-3">
{/* Strategy Stats */}
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${strategy.enabled && !strategy.paused ? 'bg-green-400' : 'bg-red-500'}`} />
<h3 className="font-bold text-sm capitalize">{strategy.name}</h3>
</div>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-yellow-500/20 text-yellow-400">{strategy.mode}</span>
</div>
<div className="grid grid-cols-4 gap-2 mb-3">
<StatBox label="Balance" value={`$${s.balance}`} />
<StatBox label="PnL" value={`${s.totalPnL >= 0 ? '+' : ''}$${s.totalPnL}`} color={pnlColor} />
<StatBox label="Win Rate" value={`${s.winRate}%`} color={s.winRate >= 50 ? GREEN : RED} />
<StatBox label="Trades" value={s.totalTrades} />
</div>
{/* Strategy Config */}
<div className="space-y-1 text-xs text-gray-400 border-t border-white/5 pt-3">
{strategy.config && Object.entries(strategy.config).map(([k, v]) => (
<div key={k} className="flex justify-between">
<span>{k}</span>
<span className="text-gray-300">{typeof v === 'number' ? v : String(v)}</span>
</div>
))}
{/* Strategy-specific extra fields */}
{strategy.consecutiveLosses !== undefined && (
<div className="flex justify-between mt-1 pt-1 border-t border-white/5">
<span>Consecutive Losses</span>
<span className={strategy.consecutiveLosses > 0 ? 'text-red-400' : 'text-gray-300'}>
{strategy.consecutiveLosses}
</span>
</div>
)}
{strategy.currentBetSize !== undefined && (
<div className="flex justify-between">
<span>Next Bet</span>
<span className="text-gray-300">${strategy.currentBetSize}</span>
</div>
)}
{strategy.round !== undefined && (
<div className="flex justify-between">
<span>Cycle Round</span>
<span className="text-gray-300">{strategy.round}/{strategy.maxRounds}</span>
</div>
)}
{strategy.cycleWins !== undefined && (
<div className="flex justify-between">
<span>Cycles Won/Lost</span>
<span className="text-gray-300">{strategy.cycleWins}W / {strategy.cycleLosses}L</span>
</div>
)}
{strategy.cycleWinRate !== undefined && (
<div className="flex justify-between">
<span>Cycle Win Rate</span>
<span className={strategy.cycleWinRate >= 50 ? 'text-green-400' : 'text-red-400'}>
{strategy.cycleWinRate}%
</span>
</div>
)}
{strategy.paused && (
<div className="text-red-400 font-medium mt-1"> PAUSED max losses reached</div>
)}
</div>
</div>
{/* Open Positions for this strategy */}
{s.openPositions.length > 0 && (
<div>
<h4 className="text-[10px] text-gray-500 uppercase tracking-wider mb-2">Open Positions</h4>
{s.openPositions.map((t, i) => <TradeRow key={i} trade={t} isOpen />)}
</div>
)}
{/* Trade History for this strategy */}
<div>
<h4 className="text-[10px] text-gray-500 uppercase tracking-wider mb-2">
Trade History ({trades.length})
</h4>
{trades.length === 0 ? (
<p className="text-gray-600 text-xs text-center py-4">No trades yet. Strategy is watching...</p>
) : (
trades.map((t, i) => <TradeRow key={i} trade={t} />)
)}
</div>
</div>
);
}
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 (
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
<div className="px-4 py-3 border-b border-white/5">
<h3 className="text-xs text-gray-400 uppercase tracking-wider font-bold">📊 Strategy Leaderboard</h3>
</div>
{entries.map((e, i) => {
const pnlColor = e.stats.totalPnL >= 0 ? GREEN : RED;
return (
<div key={e.name} className={`flex items-center justify-between px-4 py-3 ${i < entries.length - 1 ? 'border-b border-white/5' : ''}`}>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600 w-4">{i + 1}.</span>
<span className={`w-1.5 h-1.5 rounded-full ${e.enabled && !e.paused ? 'bg-green-400' : 'bg-red-500'}`} />
<span className="text-sm font-medium capitalize">{e.name}</span>
</div>
<div className="flex items-center gap-4 text-xs">
<span className="text-gray-400">{e.stats.totalTrades} trades</span>
<span className="text-gray-400">{e.stats.winRate}% wr</span>
<span className="font-bold" style={{ color: pnlColor }}>
{e.stats.totalPnL >= 0 ? '+' : ''}${e.stats.totalPnL}
</span>
</div>
</div>
);
})}
</div>
);
}
function StatBox({ label, value, color }) {
return (
<div className="bg-white/5 rounded-lg p-2 border border-white/5 text-center">
<p className="text-[9px] text-gray-500 uppercase tracking-wider">{label}</p>
<p className="text-xs font-bold mt-0.5" style={color ? { color } : {}}>{value}</p>
</div>
);
}
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 (
<div className="bg-white/5 rounded-lg p-3 border border-white/5 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-yellow-400 animate-pulse" />
) : (
<span>{won ? '✅' : '❌'}</span>
)}
<span className="text-sm font-medium capitalize">{trade.side}</span>
<span className="text-xs text-gray-500">@ {trade.price}¢</span>
<span className="text-[10px] text-gray-600">${trade.size}</span>
</div>
<span className={`text-sm font-bold ${pnlColor}`}>
{trade.pnl != null ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl}` : 'open'}
</span>
</div>
<div className="flex justify-between mt-1">
<span className="text-[10px] text-gray-600">{trade.reason}</span>
</div>
<div className="flex justify-end">
<span className="text-[10px] text-gray-600">
{trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString() : ''}
</span>
</div>
</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`;
}