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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user