fix(analytics): never mix paper + live P&L; settings live-switch copy
Money-safety/accuracy bug: analytics computed total P&L, win rate, drawdown, and every metric over ALL trades regardless of is_paper. A user who tried paper mode then went live saw simulated and real P&L summed into one number on the page whose entire purpose is 'did the bot make REAL money'. Now: split trades by is_paper. Default to LIVE. A Live/Paper toggle appears only when the wallet has both; otherwise auto-pick whichever exists. Paper view shows a prominent 'SIMULATED — not real-money results' banner. All metrics derive from the selected mode's trades only. Also: BotConfigPanel handleUpgradeToLive copy now states Auto-Trade is forced OFF on the paper→live switch (matches backend subscribe.py safety reset). tsc + next build clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { useLocale } from 'next-intl'
|
||||
import { useAccount, useSignMessage } from 'wagmi'
|
||||
import { getTrades, getSignalAccuracy } from '@/lib/api'
|
||||
@@ -67,6 +67,7 @@ export default function AnalyticsPageClient() {
|
||||
const [trades, setTrades] = useState<BotTrade[]>([])
|
||||
const [accuracy, setAccuracy] = useState<SignalAccuracy | null>(null)
|
||||
const [period, setPeriod] = useState<Period>('30d')
|
||||
const [tradeMode, setTradeMode] = useState<'live' | 'paper'>('live')
|
||||
const [privateLocked, setPrivateLocked] = useState(false)
|
||||
const [unlocking, setUnlocking] = useState(false)
|
||||
const [unlockErr, setUnlockErr] = useState('')
|
||||
@@ -131,7 +132,21 @@ export default function AnalyticsPageClient() {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period))
|
||||
// CRITICAL: never mix paper (simulated) and live (real-money) P&L into one
|
||||
// number. is_paper comes from the backend (hl_order_id == "paper"). Default
|
||||
// to LIVE — this page is the "did the bot make REAL money" view. A Paper/Live
|
||||
// toggle appears only when the wallet has both kinds of trades; otherwise we
|
||||
// auto-pick whichever set exists so a paper-only user still sees their data
|
||||
// (clearly labelled SIMULATED).
|
||||
const liveTrades = useMemo(() => trades.filter(t => !t.is_paper), [trades])
|
||||
const paperTrades = useMemo(() => trades.filter(t => !!t.is_paper), [trades])
|
||||
const hasBoth = liveTrades.length > 0 && paperTrades.length > 0
|
||||
const effectiveMode: 'live' | 'paper' =
|
||||
hasBoth ? tradeMode : (liveTrades.length > 0 ? 'live' : (paperTrades.length > 0 ? 'paper' : 'live'))
|
||||
const modeTrades = effectiveMode === 'paper' ? paperTrades : liveTrades
|
||||
const isPaperView = effectiveMode === 'paper'
|
||||
|
||||
const filteredTrades = modeTrades.filter((trade) => inPeriod(trade.closed_at, period))
|
||||
const pricedTrades = filteredTrades.filter(
|
||||
(t) => t.pnl_usd !== null && t.pnl_usd !== undefined,
|
||||
)
|
||||
@@ -172,13 +187,38 @@ export default function AnalyticsPageClient() {
|
||||
and AI signal accuracy across the time window you pick on the right.
|
||||
</PageHint>
|
||||
</div>
|
||||
<div className="nav-tabs">
|
||||
{(['7d', '30d', '90d', 'All'] as const).map(p => (
|
||||
<button key={p} className={`nav-tab ${period === p ? 'active' : ''}`} onClick={() => setPeriod(p)}>{p}</button>
|
||||
))}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-end' }}>
|
||||
{hasBoth && (
|
||||
<div className="nav-tabs">
|
||||
{(['live', 'paper'] as const).map(m => (
|
||||
<button key={m}
|
||||
className={`nav-tab ${tradeMode === m ? 'active' : ''}`}
|
||||
onClick={() => setTradeMode(m)}>
|
||||
{m === 'live' ? '💰 Live' : '📝 Paper'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="nav-tabs">
|
||||
{(['7d', '30d', '90d', 'All'] as const).map(p => (
|
||||
<button key={p} className={`nav-tab ${period === p ? 'active' : ''}`} onClick={() => setPeriod(p)}>{p}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isPaperView && filteredTrades.length > 0 && (
|
||||
<div className="card" style={{
|
||||
padding: '10px 16px', marginBottom: 16, fontSize: 12, fontWeight: 600,
|
||||
background: 'var(--amber-soft)',
|
||||
borderColor: 'color-mix(in oklab, var(--amber) 30%, var(--line))',
|
||||
color: 'var(--amber-ink, var(--ink))',
|
||||
}}>
|
||||
📝 Showing SIMULATED (paper) performance — these are not real-money results.
|
||||
{hasBoth && ' Switch to 💰 Live above for actual P&L.'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{privateLocked && (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 20 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>Private performance is locked.</div>
|
||||
|
||||
Reference in New Issue
Block a user