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