From 32e1fa31f75179cdbf0f5c73bbd69789594f01db Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Mon, 16 Mar 2026 11:31:15 -0700 Subject: [PATCH] Feat: Full live trading dashboard with toggles --- app/dash/page.js | 358 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 350 insertions(+), 8 deletions(-) diff --git a/app/dash/page.js b/app/dash/page.js index d48c29f..f7e8cb0 100644 --- a/app/dash/page.js +++ b/app/dash/page.js @@ -1,18 +1,360 @@ 'use client'; +import { useState, useEffect } from 'react'; const GREEN = '#16A34A'; +const RED = '#DC2626'; +const BLUE = '#2563EB'; +const AMBER = '#D97706'; export default function LiveDashboard() { + const [data, setData] = useState(null); + const [trades, setTrades] = useState([]); + const [loading, setLoading] = useState(true); + const [toggling, setToggling] = useState(null); + const [activeTrade, setActiveTrade] = useState(null); + + useEffect(() => { + const fetchState = async () => { + try { + const res = await fetch('/api/live-state'); + const json = await res.json(); + setData(json); + setLoading(false); + } catch (e) { + console.error('State fetch error:', e); + } + }; + fetchState(); + const interval = setInterval(fetchState, 2000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const fetchTrades = async () => { + try { + const res = await fetch('/api/live-trades'); + const json = await res.json(); + setTrades(json.trades || []); + } catch (e) { + console.error('Trades fetch error:', e); + } + }; + fetchTrades(); + const interval = setInterval(fetchTrades, 10000); + return () => clearInterval(interval); + }, []); + + const sendCommand = async (action, strategy = null) => { + setToggling(strategy || action); + try { + await fetch('/api/live-toggle', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action, strategy }) + }); + } catch (e) { + console.error('Command error:', e); + } + setTimeout(() => setToggling(null), 1500); + }; + + if (loading) { + return ( +
+
Loading Live Trading...
+
+ ); + } + + const market = data?.market; + const live = data?.live || {}; + const strategies = data?.strategies || []; + const enabledSet = new Set(live.enabledStrategies || []); + return ( -
-
-

Live Trading

-

Coming soon, Meowster.

-

Find a profitable paper strategy first.

- - โ† Back to Paper Trading - +
+ {/* Header */} +
+
+
+

Kalbot

+ LIVE +
+
+ + ๐Ÿ“ Paper + +
+ + {data?.lastUpdate ? 'Live' : 'Offline'} +
+
+
+
+ +
+ {/* Kill Switch + Balance */} +
+
+
+

Kalshi Balance

+

+ {live.balance != null ? `$${live.balance.toFixed(2)}` : 'โ€”'} +

+ {live.portfolioValue != null && ( +

Portfolio: ${live.portfolioValue.toFixed(2)}

+ )} +
+ +
+ +
+ = 0 ? '+' : ''}$${live.totalPnL?.toFixed(2) || '0.00'}`} color={live.totalPnL >= 0 ? GREEN : RED} dark /> + = 50 ? GREEN : RED} dark /> + + 0 ? AMBER : null} dark /> +
+ + {live.paused && ( +
+ โš ๏ธ TRADING PAUSED โ€” No new orders will be placed +
+ )} + +
+ Per-trade cap: ${live.maxPerTrade?.toFixed(2) || '5.00'} + Daily limit: ${live.maxDailyLoss?.toFixed(2) || '20.00'} +
+
+ + {/* Market Card */} + + + {/* Strategy Toggles */} +
+
+

โšก Strategy Controls

+
+ {strategies.map((s, i) => { + const isEnabled = enabledSet.has(s.name); + const isToggling = toggling === s.name; + return ( +
+
+ +
+

{s.name}

+ {s.config && ( +

+ ${s.config.betSize || 1}/trade โ€ข {s.config.cooldownMs ? `${(s.config.cooldownMs/1000).toFixed(0)}s cd` : ''} +

+ )} +
+
+ +
+ ); + })} + {strategies.length === 0 && ( +

No strategies loaded yet.

+ )} +
+ + {/* Open Orders */} + {live.openOrders?.length > 0 && ( +
+

+ Open Orders ({live.openOrders.length}) +

+ {live.openOrders.map((o, i) => ( + + ))} +
+ )} + + {/* Kalshi Positions */} + {live.positions?.length > 0 && ( +
+

+ Kalshi Positions ({live.positions.length}) +

+ {live.positions.map((p, i) => ( +
+
+ {p.market_ticker || p.ticker} + {p.position_fp || p.position || 0} contracts +
+ {p.realized_pnl_dollars && ( +

+ Realized PnL: = 0 ? 'text-green-400' : 'text-red-400'}> + ${Number(p.realized_pnl_dollars).toFixed(2)} + +

+ )} +
+ ))} +
+ )} + + {/* Trade History */} +
+

+ Trade History ({trades.length}) +

+ {trades.length === 0 ? ( +

No settled trades yet. Enable a strategy to start.

+ ) : ( + trades.map((t, i) => ) + )} +
+
+ + {/* Footer */} +
+
+ Worker: {formatUptime(data?.workerUptime)} + {data?.lastUpdate ? new Date(data.lastUpdate).toLocaleTimeString() : 'never'} +
); } + +function MarketCard({ 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 LiveOrderRow({ order, isOpen }) { + const won = order.result && order.side?.toLowerCase() === order.result?.toLowerCase(); + const isNeutral = order.result === 'cancelled' || order.result === 'expired'; + const pnlVal = order.pnl != null ? (typeof order.pnl === 'number' && Math.abs(order.pnl) > 50 ? order.pnl / 100 : order.pnl) : null; + const pnlColor = pnlVal == null ? 'text-gray-600' : pnlVal > 0 ? 'text-green-400' : pnlVal < 0 ? 'text-red-400' : 'text-gray-400'; + + return ( +
+
+
+ {isOpen ? ( + + ) : ( + {isNeutral ? 'โž–' : won ? 'โœ…' : 'โŒ'} + )} + {order.side} + @ {order.priceCents || order.price}ยข + {order.contracts || 1}x +
+ + {pnlVal != null ? `${pnlVal >= 0 ? '+' : ''}$${pnlVal.toFixed(2)}` : 'open'} + +
+
+ {order.reason} + {order.strategy} +
+ {order.result && !isOpen && ( +
+ Result: {order.result} + + {order.settleTime ? new Date(order.settleTime).toLocaleTimeString() : order.createdAt ? new Date(order.createdAt).toLocaleTimeString() : ''} + +
+ )} +
+ ); +} + +function StatBox({ label, value, color, dark }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +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`; +}