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:
k
2026-05-30 01:03:15 +08:00
parent e78a61bd6e
commit 594d9817ba
8 changed files with 72 additions and 25 deletions
+3 -3
View File
@@ -303,9 +303,9 @@ export default function DashboardClient({ initialPosts }: Props) {
<div>
<h1 className="page-title">{isZh ? '信号总览' : 'Signal monitor'}</h1>
<PageHint count={`${actionablePosts} actionable · ${totalPosts} tracked`}>
All four signal pipelines in one place Trump posts, BTC macro
bottom, funding-rate extremes, and KOL talks-vs-trades alongside
your auto-trader's live state.
Four signals that move crypto before the crowd sees them Trump
posts, BTC macro bottoms, funding extremes, and what KOLs do vs
say. Tracked live, in public, every call timestamped.
</PageHint>
</div>
<div className="row gap-s">
+46 -6
View File
@@ -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>
+1 -1
View File
@@ -58,7 +58,7 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
color: 'var(--ink-3)',
marginTop: 48,
}}>
<span>© {new Date().getFullYear()} Trump Alpha</span>
<span>© {new Date().getFullYear()} Trump Alpha a research project by Endorphin</span>
<Link href={href('/methodology')} style={{ color: 'var(--ink-3)', textDecoration: 'none' }}>{t('methodology')}</Link>
<Link href={href('/glossary')} style={{ color: 'var(--ink-3)', textDecoration: 'none' }}>{t('glossary')}</Link>
<Link href={href('/case-studies')} style={{ color: 'var(--ink-3)', textDecoration: 'none' }}>{t('caseStudies')}</Link>
+1 -1
View File
@@ -223,7 +223,7 @@ SHORT_INTENT + SHORT_ACTION => ALIGNMENT`,
heroTitle: 'Signal Methodology',
heroSubtitle: 'How each engine works, from raw input to trading decision',
intro:
'Trump Alpha is not one monolithic strategy. It is a stack of six independent engines, each with its own data source, trigger logic, and failure mode. This page documents the live production logic rather than a simplified marketing summary.',
'Endorphin runs Trump Alpha as a research project, not a marketing funnel. It is not one monolithic strategy — it is a stack of six independent engines, each with its own data source, trigger logic, and failure mode. This page documents the live production logic, not a simplified pitch.',
sections: [
{
tag: 'Signal 1 · Real-time event',
+4 -4
View File
@@ -15,11 +15,11 @@ export async function generateMetadata({ params }: OverviewPageProps): Promise<M
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
const title = isZh
? 'Trump Alpha 信号总览'
: 'Trump Alpha Signal Monitor'
? 'Trump Alpha — 实时加密信号研究台'
: 'Trump Alpha — Live Crypto Signal Desk'
const description = isZh
? '实时查看 Trump Truth Social 信号、Macro Vibes 宏观氛围、KOL 观点和 talks-vs-trades 分歧,一页掌握核心加密信号。'
: 'Track Trump Truth Social signals, Macro Vibes (crypto macro regime), KOL calls, and talks-vs-trades divergence in one live crypto dashboard.'
? 'Endorphin 研究台追踪四个先于市场共识的信号:Trump 帖子、BTC 宏观底部、资金费率极值,以及 KOL 言行偏离。每个信号公开且带时间戳。'
: 'Endorphin tracks four signals that move crypto before the crowd: Trump posts, BTC macro bottoms, funding-rate extremes, and what KOLs do vs say. Public and timestamped.'
const path = `${siteUrl}/${locale}`
return {
+14 -7
View File
@@ -4,8 +4,8 @@ import Script from 'next/script'
import './[locale]/globals.css'
const siteTitle = 'Trump Alpha'
const siteTagline = 'Four crypto alpha feeds, one dashboard'
const siteDescription = 'Real-time Trump Truth Social signals, BTC macro-bottom confluence, KOL Substack/podcast signals, and talks-vs-trades divergence — all scored by AI in one live dashboard.'
const siteTagline = 'The crypto signals that move price before the crowd'
const siteDescription = "Endorphin is a crypto research desk tracking four signals that move markets before consensus catches up — Trump's posts, BTC macro bottoms, funding-rate extremes, and what KOLs do versus what they say. Every signal is public and timestamped."
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
export const metadata: Metadata = {
@@ -42,9 +42,9 @@ export const metadata: Metadata = {
'funding rate reversal',
'Bitcoin cycle bottom',
],
authors: [{ name: 'Trump Alpha' }],
creator: 'Trump Alpha',
publisher: 'Trump Alpha',
authors: [{ name: 'Endorphin' }],
creator: 'Endorphin',
publisher: 'Endorphin',
category: 'Finance',
robots: {
index: true,
@@ -137,6 +137,8 @@ const jsonLd = {
applicationSubCategory: 'Cryptocurrency Trading Tool',
operatingSystem: 'Web',
description: siteDescription,
publisher: { '@id': `${_base}/#org` },
author: { '@id': `${_base}/#org` },
datePublished: '2025-01-01',
dateModified: _today,
inLanguage: ['en'],
@@ -168,10 +170,15 @@ const jsonLd = {
{
'@type': 'Organization',
'@id': `${_base}/#org`,
name: 'Trump Alpha',
name: 'Endorphin',
alternateName: ['Endorphin Research', 'Endorphin Desk'],
url: _base,
logo: `${_base}/icon`,
description: 'AI-powered crypto intelligence platform tracking Trump posts, on-chain signals, and KOL sentiment.',
description: 'Endorphin is a crypto research desk. Trump Alpha is its public signal project — four market-moving signals tracked live and timestamped.',
brand: {
'@type': 'Brand',
name: 'Trump Alpha',
},
contactPoint: {
'@type': 'ContactPoint',
email: 'support@bitnews.day',