diff --git a/app/paper/page.js b/app/paper/page.js new file mode 100644 index 0000000..18cddda --- /dev/null +++ b/app/paper/page.js @@ -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 ( +
+
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`; +}