Files
KalBot/app/paper/page.js

422 lines
17 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';
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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-green-600 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-gray-50 text-gray-900 font-sans pb-20">
<header className="sticky top-0 z-50 bg-white/95 backdrop-blur border-b border-gray-200 px-4 py-3 shadow-sm">
<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-amber-100 text-amber-700 px-2 py-0.5 rounded-full font-medium">PAPER</span>
</div>
<div className="flex items-center gap-3">
<a href="/dash" className="text-[10px] px-2 py-1 rounded bg-gray-900 text-green-400 border border-gray-700 hover:bg-gray-800 transition-colors font-bold">
💰 Live
</a>
<button
onClick={handleReset}
disabled={resetting}
className="text-[10px] px-2 py-1 rounded bg-red-50 text-red-600 border border-red-200 hover:bg-red-100 transition-colors disabled:opacity-50 font-medium"
>
{resetting ? 'Resetting...' : '🗑 Reset All'}
</button>
<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">
<MarketCardCompact market={market} />
<div className="flex gap-1 bg-gray-100 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 text-gray-900 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<span className={`inline-block w-1.5 h-1.5 rounded-full mr-1.5 ${
s.enabled && !s.paused ? 'bg-green-500' : 'bg-red-500'
}`} />
{s.name}
</button>
))}
</div>
{activeStrat && activeStratData && (
<StrategyDetailView
strategy={activeStratData}
stats={activeStats}
trades={activeTrades}
/>
)}
<AllStrategiesOverview paperByStrategy={paperByStrategy} strategies={strategies} />
</main>
<div className="fixed bottom-0 left-0 right-0 bg-white/95 backdrop-blur border-t border-gray-200 py-2 px-4">
<div className="max-w-lg mx-auto flex justify-between text-xs text-gray-400">
<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 rounded-xl p-4 border border-gray-200 shadow-sm">
<p className="text-gray-400 text-center text-sm">No active market waiting...</p>
</div>
);
}
const timeLeft = market.closeTime ? getTimeLeft(market.closeTime) : null;
return (
<div className="bg-white rounded-xl p-4 border border-gray-200 shadow-sm">
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="font-bold text-sm text-gray-900">BTC Up or Down</h2>
<p className="text-[10px] text-gray-400">15 min</p>
</div>
<div className="flex items-center gap-2">
{timeLeft && (
<span className="text-[10px] bg-gray-100 px-2 py-0.5 rounded-full text-gray-600"> {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-600">Up</span>
<span style={{ color: GREEN }} className="font-medium">{market.yesPct}%</span>
</div>
<div className="w-full bg-gray-100 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-600">Down</span>
<span style={{ color: BLUE }} className="font-medium">{market.noPct}%</span>
</div>
<div className="w-full bg-gray-100 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-400 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">
<div className="bg-white rounded-xl p-4 border border-gray-200 shadow-sm">
<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-500' : 'bg-red-500'}`} />
<h3 className="font-bold text-sm capitalize text-gray-900">{strategy.name}</h3>
</div>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 font-medium">{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>
<div className="space-y-1 text-xs text-gray-500 border-t border-gray-100 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-700">{typeof v === 'number' ? v : String(v)}</span>
</div>
))}
{strategy.consecutiveLosses !== undefined && (
<div className="flex justify-between mt-1 pt-1 border-t border-gray-100">
<span>Consecutive Losses</span>
<span className={strategy.consecutiveLosses > 0 ? 'text-red-600' : 'text-gray-700'}>
{strategy.consecutiveLosses}
</span>
</div>
)}
{strategy.currentBetSize !== undefined && (
<div className="flex justify-between">
<span>Next Bet</span>
<span className="text-gray-700">${strategy.currentBetSize}</span>
</div>
)}
{strategy.round !== undefined && (
<div className="flex justify-between">
<span>Cycle Round</span>
<span className="text-gray-700">{strategy.round}/{strategy.maxRounds}</span>
</div>
)}
{strategy.cycleWins !== undefined && (
<div className="flex justify-between">
<span>Cycles Won/Lost</span>
<span className="text-gray-700">{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-600' : 'text-red-600'}>
{strategy.cycleWinRate}%
</span>
</div>
)}
{strategy.paused && (
<div className="text-red-600 font-medium mt-1"> PAUSED max losses reached</div>
)}
</div>
</div>
{s.openPositions.length > 0 && (
<div>
<h4 className="text-[10px] text-gray-400 uppercase tracking-wider mb-2 font-bold">Open Positions ({s.openPositions.length})</h4>
{s.openPositions.map((t, i) => <TradeRow key={i} trade={t} isOpen />)}
</div>
)}
<div>
<h4 className="text-[10px] text-gray-400 uppercase tracking-wider mb-2 font-bold">
Trade History ({trades.length})
</h4>
{trades.length === 0 ? (
<p className="text-gray-400 text-xs text-center py-4">No settled trades yet.</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;
entries.sort((a, b) => b.stats.totalPnL - a.stats.totalPnL);
return (
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden shadow-sm">
<div className="px-4 py-3 border-b border-gray-100">
<h3 className="text-xs text-gray-500 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-gray-100' : ''}`}>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400 w-4">{i + 1}.</span>
<span className={`w-1.5 h-1.5 rounded-full ${e.enabled && !e.paused ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-sm font-medium capitalize text-gray-800">{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-gray-50 rounded-lg p-2 border border-gray-100 text-center">
<p className="text-[9px] text-gray-400 uppercase tracking-wider">{label}</p>
<p className="text-xs font-bold mt-0.5" style={color ? { color } : { color: '#111827' }}>{value}</p>
</div>
);
}
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 (
<div className="bg-white rounded-lg p-3 border border-gray-200 mb-2 shadow-sm">
<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-900">{trade.side}</span>
<span className="text-xs text-gray-400">@ {trade.price}¢</span>
<span className="text-[10px] text-gray-400">${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-400">{trade.reason}</span>
</div>
{hasExpected && (
<div className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[10px] text-gray-500">
<span>
If win: <span className="text-green-600 font-medium">+${winPnL.toFixed(2)}</span>
</span>
<span>
If lose: <span className="text-red-600 font-medium">${losePnL.toFixed(2)}</span>
</span>
<span>
Gross payout: <span className="text-gray-700 font-medium">${grossPayout.toFixed(2)}</span>
</span>
</div>
)}
<div className="flex justify-between items-center mt-0.5">
{trade.result && !isOpen && (
<span className="text-[10px] text-gray-400">Result: {trade.result}</span>
)}
{!trade.result && <span />}
<span className="text-[10px] text-gray-400">
{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`;
}