Files
KalBot/app/dashboard/page.js

361 lines
13 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';
export default function Dashboard() {
const [data, setData] = useState(null);
const [trades, setTrades] = useState([]);
const [loading, setLoading] = useState(true);
const [tab, setTab] = useState('market');
useEffect(() => {
const fetchState = async () => {
try {
const res = await fetch('/api/state');
const json = await res.json();
setData(json);
setLoading(false);
} catch (e) {
console.error('State fetch error:', e);
}
};
const fetchTrades = async () => {
try {
const res = await fetch('/api/trades');
const json = await res.json();
setTrades(json.trades || []);
} catch (e) {
console.error('Trades fetch error:', e);
}
};
fetchState();
fetchTrades();
const interval = setInterval(fetchState, 2000);
const tradesInterval = setInterval(fetchTrades, 10000);
return () => {
clearInterval(interval);
clearInterval(tradesInterval);
};
}, []);
if (loading) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="text-[#28CC95] text-lg animate-pulse">Loading Kalbot...</div>
</div>
);
}
const market = data?.market;
const paper = data?.paper;
const strategies = data?.strategies || [];
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">
<h1 className="text-lg font-bold" style={{ color: GREEN }}>Kalbot</h1>
<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 */}
<MarketCard market={market} />
{/* Paper Stats */}
<PaperStats paper={paper} />
{/* Tab Bar */}
<div className="flex gap-1 bg-white/5 rounded-lg p-1">
{['market', 'strategies', 'trades'].map(t => (
<button
key={t}
onClick={() => setTab(t)}
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all ${
tab === t ? 'bg-white/10 text-white' : 'text-gray-500 hover:text-gray-300'
}`}
>
{t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
{/* Tab Content */}
{tab === 'market' && <MarketDetails market={market} />}
{tab === 'strategies' && <StrategiesView strategies={strategies} />}
{tab === 'trades' && <TradesView trades={trades} openPositions={paper?.openPositions || []} />}
</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 uptime: {formatUptime(data?.workerUptime)}</span>
<span>Updated: {data?.lastUpdate ? new Date(data.lastUpdate).toLocaleTimeString() : 'never'}</span>
</div>
</div>
</div>
);
}
function MarketCard({ market }) {
if (!market) {
return (
<div className="bg-white/5 rounded-2xl p-5 border border-white/10">
<p className="text-gray-500 text-center">No active market waiting for next 15-min window...</p>
</div>
);
}
const timeLeft = market.closeTime ? getTimeLeft(market.closeTime) : null;
return (
<div className="bg-white/5 rounded-2xl p-5 border border-white/10 space-y-4">
{/* Title */}
<div className="flex items-center justify-between">
<div>
<h2 className="font-bold text-base">BTC Up or Down</h2>
<p className="text-xs text-gray-400">15 minutes</p>
</div>
<div className="flex items-center gap-2">
{timeLeft && (
<span className="text-xs bg-white/10 px-2 py-1 rounded-full text-gray-300">
{timeLeft}
</span>
)}
<span className="text-2xl"></span>
</div>
</div>
{/* Up */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Up</span>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-400">{market.yesOdds}x</span>
<span className="text-sm font-bold px-3 py-1 rounded-full border"
style={{ borderColor: GREEN, color: GREEN }}>
{market.yesPct}%
</span>
</div>
</div>
<div className="w-full bg-white/10 rounded-full h-2">
<div className="h-2 rounded-full transition-all duration-500"
style={{ width: `${market.yesPct}%`, backgroundColor: GREEN }} />
</div>
</div>
{/* Down */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Down</span>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-400">{market.noOdds}x</span>
<span className="text-sm font-bold px-3 py-1 rounded-full border"
style={{ borderColor: '#4A90D9', color: '#4A90D9' }}>
{market.noPct}%
</span>
</div>
</div>
<div className="w-full bg-white/10 rounded-full h-2">
<div className="h-2 rounded-full transition-all duration-500"
style={{ width: `${market.noPct}%`, backgroundColor: '#4A90D9' }} />
</div>
</div>
{/* Volume */}
<div className="flex justify-between text-xs text-gray-500 pt-1">
<span>${(market.volume || 0).toLocaleString()} vol</span>
<span className="font-mono text-gray-600">{market.ticker}</span>
</div>
</div>
);
}
function PaperStats({ paper }) {
if (!paper) return null;
const pnlColor = paper.totalPnL >= 0 ? GREEN : RED;
return (
<div className="grid grid-cols-4 gap-2">
<StatBox label="Balance" value={`$${paper.balance}`} />
<StatBox label="PnL" value={`${paper.totalPnL >= 0 ? '+' : ''}$${paper.totalPnL}`} color={pnlColor} />
<StatBox label="Win Rate" value={`${paper.winRate}%`} color={paper.winRate >= 50 ? GREEN : RED} />
<StatBox label="Trades" value={paper.totalTrades} />
</div>
);
}
function StatBox({ label, value, color }) {
return (
<div className="bg-white/5 rounded-xl p-3 border border-white/5 text-center">
<p className="text-[10px] text-gray-500 uppercase tracking-wider">{label}</p>
<p className="text-sm font-bold mt-0.5" style={color ? { color } : {}}>{value}</p>
</div>
);
}
function MarketDetails({ market }) {
if (!market) return <p className="text-gray-500 text-sm text-center py-8">No market data</p>;
const rows = [
['Yes Bid / Ask', `${market.yesBid || '-'}¢ / ${market.yesAsk || '-'}¢`],
['No Bid / Ask', `${market.noBid || '-'}¢ / ${market.noAsk || '-'}¢`],
['Last Price', `${market.lastPrice || '-'}¢`],
['Volume 24h', `$${(market.volume24h || 0).toLocaleString()}`],
['Open Interest', (market.openInterest || 0).toLocaleString()],
['Status', market.status || 'unknown'],
['Closes', market.closeTime ? new Date(market.closeTime).toLocaleTimeString() : '-'],
];
return (
<div className="bg-white/5 rounded-xl border border-white/5 overflow-hidden">
{rows.map(([k, v], i) => (
<div key={k} className={`flex justify-between px-4 py-3 ${i < rows.length - 1 ? 'border-b border-white/5' : ''}`}>
<span className="text-sm text-gray-400">{k}</span>
<span className="text-sm font-medium">{v}</span>
</div>
))}
</div>
);
}
function StrategiesView({ strategies }) {
if (!strategies.length) {
return <p className="text-gray-500 text-sm text-center py-8">No strategies loaded</p>;
}
return (
<div className="space-y-3">
{strategies.map((s, i) => (
<div key={i} className="bg-white/5 rounded-xl p-4 border border-white/5">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${s.enabled && !s.paused ? 'bg-green-400' : 'bg-red-500'}`} />
<span className="font-bold text-sm capitalize">{s.name}</span>
</div>
<span className="text-xs px-2 py-0.5 rounded-full bg-white/10 text-gray-400">{s.mode}</span>
</div>
<div className="space-y-1 text-xs text-gray-400">
{s.config && Object.entries(s.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>
))}
{s.consecutiveLosses !== undefined && (
<div className="flex justify-between mt-1 pt-1 border-t border-white/5">
<span>Consecutive Losses</span>
<span className={s.consecutiveLosses > 0 ? 'text-red-400' : 'text-gray-300'}>
{s.consecutiveLosses}
</span>
</div>
)}
{s.currentBetSize !== undefined && (
<div className="flex justify-between">
<span>Next Bet</span>
<span className="text-gray-300">${s.currentBetSize}</span>
</div>
)}
{s.paused && (
<div className="text-red-400 font-medium mt-1"> PAUSED max losses reached</div>
)}
</div>
</div>
))}
</div>
);
}
function TradesView({ trades, openPositions }) {
return (
<div className="space-y-3">
{/* Open Positions */}
{openPositions.length > 0 && (
<div>
<h3 className="text-xs text-gray-500 uppercase tracking-wider mb-2">Open Positions</h3>
{openPositions.map((t, i) => (
<TradeRow key={i} trade={t} isOpen />
))}
</div>
)}
{/* Trade History */}
<div>
<h3 className="text-xs text-gray-500 uppercase tracking-wider mb-2">
History ({trades.length})
</h3>
{trades.length === 0 ? (
<p className="text-gray-600 text-sm text-center py-6">No trades yet. Strategies are watching...</p>
) : (
trades.map((t, i) => <TradeRow key={i} trade={t} />)
)}
</div>
</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>
</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 capitalize">{trade.strategy}</span>
<span className="text-[10px] text-gray-600">
{trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString() : ''}
</span>
</div>
{trade.reason && (
<p className="text-[10px] text-gray-600 mt-1 truncate">{trade.reason}</p>
)}
</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`;
}