mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-16 21:41:02 +00:00
Feat: Per-strategy paper dashboard for strat hunting
This commit is contained in:
381
app/paper/page.js
Normal file
381
app/paper/page.js
Normal file
@@ -0,0 +1,381 @@
|
||||
'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`;
|
||||
}
|
||||
Reference in New Issue
Block a user