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
+27 -9
View File
@@ -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<TrumpPost[]>(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
<div className="kpi-row">
<div className="kpi">
<div className="label"><span className="asset-dot btc" style={{ width: 10, height: 10 }} /> BTC</div>
<div className="value">{lastCandle ? '$' + Math.round(lastCandle.close).toLocaleString() : '—'}</div>
<div className="value">{displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}</div>
<div className="foot">
<span className={`delta ${priceChange >= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'}</span>
<span>{timeframe}</span>
<span>24h</span>
</div>
</div>
<div className="kpi">
@@ -243,9 +261,9 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
<div className="tiny">Price · {asset}</div>
<div className="row gap-m" style={{ marginTop: 6 }}>
<div className="hero-value mono" style={{ fontSize: 32 }}>
{lastCandle ? '$' + Math.round(lastCandle.close).toLocaleString() : '—'}
{displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
</div>
<span className={`chip ${priceChange >= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · {timeframe}</span>
<span className={`chip ${priceChange >= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · 24h</span>
</div>
</div>
<div className="stack gap-s" style={{ alignItems: 'flex-end' }}>