fix: dashboard live price + 24h % + chart tick + null-pnl safety

Five bugs in the same blast radius (places we render a price/percent).

DashboardClient.tsx
  • Hero price read lastCandle.close (REST candle, frozen until user
    changes asset/tf). WebSocket livePrices was set but never read.
    Now: livePrice ?? lastCandle.close.
  • "+21.84% · 4H" was (last_close - first_open) / first_open over
    the entire candle window. limit=200 + tf=4H = 33 days shown as
    "4H". Find candle closest to now-24h, label "24h" consistently.

ChartPanel.tsx
  • Chart only re-rendered on candles prop change. Live WS ticks
    ignored. Added useEffect calling series.update() with
    livePrices[asset] on every tick.

analytics/page.tsx
  • reduce(sum + trade.pnl_usd) when pnl_usd is null → NaN poison.
  • Math.max/min over null cast to 0 → bogus "best/worst trade".
  • Win-rate denominator counted null-pnl trades as losses.
  • calcDrawdownPct had the same null poisoning.
  Fix: gate aggregations on priced subset only.

trades/page.tsx
  • ROI could divide by 0/null; entry/exit crashed on null.toLocaleString().
  Added guards.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-08 19:57:28 +08:00
parent 040e1df685
commit 01be8e790b
4 changed files with 76 additions and 23 deletions
+22 -1
View File
@@ -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<HTMLDivElement>(null)
const chartRef = useRef<unknown>(null)
const seriesRef = useRef<unknown>(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 (
<div
ref={containerRef}