diff --git a/app/[locale]/DashboardClient.tsx b/app/[locale]/DashboardClient.tsx index a48d19c..e94c925 100644 --- a/app/[locale]/DashboardClient.tsx +++ b/app/[locale]/DashboardClient.tsx @@ -132,7 +132,7 @@ function SelectHint() { // ── Main dashboard ───────────────────────────────────────────────────────────── export default function DashboardClient({ initialPosts, initialPerformance }: Props) { - const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setBotReadiness, setHlApiKeySet, isSubscribed, hlApiKeySet, botReadiness } = useDashboardStore() + const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setBotReadiness, setHlApiKeySet, isSubscribed, hlApiKeySet, botReadiness, livePrices } = useDashboardStore() function setTimeframe(tf: string) { _setTimeframe(tf as '5m' | '15m' | '1H' | '4H' | '1D' | '1W') } const { address, isConnected } = useAccount() const [posts, setPosts] = useState(initialPosts) @@ -178,10 +178,28 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr const recentPosts = posts.slice(0, 8) const lastCandle = candles[candles.length - 1] - const firstCandle = candles[0] - const priceChange = lastCandle && firstCandle - ? ((lastCandle.close - firstCandle.open) / firstCandle.open) * 100 - : 0 + // Live price beats the most recent candle close — the candle is only + // refreshed when the user changes asset/timeframe, so without WS the number + // is frozen at page load. + const livePrice = livePrices[asset] + const displayPrice = livePrice ?? lastCandle?.close ?? null + + // 24-hour change. Picking by candle index is wrong (200×4H = 33 days). + // Find the candle whose `time` is closest to (now − 24h) and use its open. + const now = lastCandle ? lastCandle.time : Math.floor(Date.now() / 1000) + const target24h = now - 24 * 3600 + let baseline24h: typeof lastCandle | undefined + if (candles.length) { + baseline24h = candles[0] + for (const c of candles) { + if (c.time <= target24h) baseline24h = c + else break + } + } + const priceChange = + displayPrice != null && baseline24h + ? ((displayPrice - baseline24h.open) / baseline24h.open) * 100 + : 0 const totalPosts = posts.length const todayKey = new Date().toISOString().slice(0, 10) @@ -211,10 +229,10 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
BTC
-
{lastCandle ? '$' + Math.round(lastCandle.close).toLocaleString() : '—'}
+
{displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} - {timeframe} + 24h
@@ -243,9 +261,9 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
Price · {asset}
- {lastCandle ? '$' + Math.round(lastCandle.close).toLocaleString() : '—'} + {displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
- = 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · {timeframe} + = 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · 24h
diff --git a/app/[locale]/analytics/page.tsx b/app/[locale]/analytics/page.tsx index e83c4df..c4cb8a6 100644 --- a/app/[locale]/analytics/page.tsx +++ b/app/[locale]/analytics/page.tsx @@ -31,15 +31,17 @@ function inPeriod(iso: string, period: Period) { } function calcDrawdownPct(trades: BotTrade[]) { - const ordered = [...trades].sort( - (a, b) => new Date(a.closed_at).getTime() - new Date(b.closed_at).getTime() - ) + // Skip externally-closed trades (pnl_usd null) — including them as `+ null` + // turns equity into NaN and the whole drawdown chart goes blank. + const ordered = [...trades] + .filter((t) => t.pnl_usd !== null && t.pnl_usd !== undefined) + .sort((a, b) => new Date(a.closed_at).getTime() - new Date(b.closed_at).getTime()) let equity = 0 let peak = 0 let maxDrawdownPct = 0 for (const trade of ordered) { - equity += trade.pnl_usd + equity += trade.pnl_usd as number peak = Math.max(peak, equity) if (peak > 0) { maxDrawdownPct = Math.max(maxDrawdownPct, ((peak - equity) / peak) * 100) @@ -65,12 +67,19 @@ export default function AnalyticsPage() { }, []) const filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period)) - const totalPnl = filteredTrades.reduce((sum, trade) => sum + trade.pnl_usd, 0) - const wins = filteredTrades.filter((trade) => trade.pnl_usd > 0).length - const winRate = filteredTrades.length ? (wins / filteredTrades.length) * 100 : 0 - const bestTrade = filteredTrades.length ? Math.max(...filteredTrades.map(t => t.pnl_usd)) : 0 - const worstTrade = filteredTrades.length ? Math.min(...filteredTrades.map(t => t.pnl_usd)) : 0 - const avgTrade = filteredTrades.length ? filteredTrades.reduce((s, t) => s + t.pnl_usd, 0) / filteredTrades.length : 0 + // Trades closed externally on Hyperliquid have pnl_usd === null. Including + // them silently in reduce/Math.max/Math.min produces NaN ("$NaN P&L") and + // also pollutes win_rate. Aggregate only on the priced subset. + const pricedTrades = filteredTrades.filter( + (t) => t.pnl_usd !== null && t.pnl_usd !== undefined, + ) + const pnls = pricedTrades.map((t) => t.pnl_usd as number) + const totalPnl = pnls.reduce((s, p) => s + p, 0) + const wins = pnls.filter((p) => p > 0).length + const winRate = pricedTrades.length ? (wins / pricedTrades.length) * 100 : 0 + const bestTrade = pnls.length ? Math.max(...pnls) : 0 + const worstTrade = pnls.length ? Math.min(...pnls) : 0 + const avgTrade = pnls.length ? totalPnl / pnls.length : 0 const avgHold = filteredTrades.length ? filteredTrades.reduce((sum, trade) => sum + trade.hold_seconds, 0) / filteredTrades.length : 0 const maxDrawdown = period === '30d' && perf ? perf.max_drawdown_pct : calcDrawdownPct(filteredTrades) const summaryPnl = period === '30d' && perf && trades.length >= filteredTrades.length diff --git a/app/[locale]/trades/page.tsx b/app/[locale]/trades/page.tsx index c5c2131..98bd3ba 100644 --- a/app/[locale]/trades/page.tsx +++ b/app/[locale]/trades/page.tsx @@ -738,7 +738,12 @@ export default function TradesPage() { )} {filtered.map(t => { const tp = posts.find(p => p.id === t.trigger_post_id) - const roi = ((t.exit_price - t.entry_price) / t.entry_price) * 100 * (t.side === 'long' ? 1 : -1) + // Guard: entry_price may be 0 on a corrupt row; exit_price may + // be null for externally-closed trades. Both → NaN otherwise. + const roi = + t.entry_price && t.exit_price != null + ? ((t.exit_price - t.entry_price) / t.entry_price) * 100 * (t.side === 'long' ? 1 : -1) + : 0 return ( @@ -748,8 +753,8 @@ export default function TradesPage() {
{t.side==='long'?'↗ LONG':'↘ SHORT'} - ${t.entry_price.toLocaleString()} - ${t.exit_price.toLocaleString()} + {t.entry_price ? '$' + t.entry_price.toLocaleString() : '—'} + {t.exit_price != null ? '$' + t.exit_price.toLocaleString() : '—'} {fmtHold(t.hold_seconds)} {tp ? ( diff --git a/components/dashboard/ChartPanel.tsx b/components/dashboard/ChartPanel.tsx index 6786232..8be7308 100644 --- a/components/dashboard/ChartPanel.tsx +++ b/components/dashboard/ChartPanel.tsx @@ -13,7 +13,7 @@ interface ChartPanelProps { } export default function ChartPanel({ posts = [], candles = [], externalSelectedId, onSelectPost, onSelectDayPosts }: ChartPanelProps) { - const { timeframe } = useDashboardStore() + const { timeframe, asset, livePrices } = useDashboardStore() const containerRef = useRef(null) const chartRef = useRef(null) const seriesRef = useRef(null) @@ -243,6 +243,27 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI useEffect(() => { fittedRef.current = false }, [timeframe]) + // Live-tick the rightmost candle so the chart feels alive between REST polls. + // lightweight-charts' `series.update()` either appends a new bar (newer time) + // or in-place mutates the bar at that timestamp. We always keep `time` == + // the last candle's bucket so the bar grows in place; high/low expand if + // the live tick exceeds them. + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const series = seriesRef.current as any + if (!series) return + const live = livePrices[asset] + if (live == null || !candles.length) return + const last = candles[candles.length - 1] + series.update({ + time: last.time as number, + open: last.open, + high: Math.max(last.high, live), + low: Math.min(last.low, live), + close: live, + }) + }, [livePrices, asset, candles]) + return (