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
+8 -3
View File
@@ -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 (
<tr key={t.id}>
<td>
@@ -748,8 +753,8 @@ export default function TradesPage() {
</div>
</td>
<td><span className={`side-pill ${t.side}`}>{t.side==='long'?' LONG':' SHORT'}</span></td>
<td className="mono">${t.entry_price.toLocaleString()}</td>
<td className="mono">${t.exit_price.toLocaleString()}</td>
<td className="mono">{t.entry_price ? '$' + t.entry_price.toLocaleString() : ''}</td>
<td className="mono">{t.exit_price != null ? '$' + t.exit_price.toLocaleString() : ''}</td>
<td className="mono" style={{ color:'var(--ink-2)' }}>{fmtHold(t.hold_seconds)}</td>
<td style={{ maxWidth:260 }}>
{tp ? (