diff --git a/.claude/launch.json b/.claude/launch.json index 0c80573..0c7ec78 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -5,6 +5,7 @@ "name": "trumpsignal", "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev"], + "cwd": "/Users/k/Public/Claude/trumpsignal", "port": 3001 } ] diff --git a/.env.local.example b/.env.local.example index 65799df..1945b4d 100644 --- a/.env.local.example +++ b/.env.local.example @@ -9,3 +9,8 @@ NEXT_PUBLIC_API_URL=https://api.yourdomain.com # Dev: ws://localhost:8000 # Production: wss://api.yourdomain.com NEXT_PUBLIC_WS_URL=wss://api.yourdomain.com + +# Public site URL used for sitemap.xml and robots.txt +# Dev: http://localhost:3001 +# Production: https://yourdomain.com +NEXT_PUBLIC_SITE_URL=https://yourdomain.com diff --git a/app/[locale]/DashboardClient.tsx b/app/[locale]/DashboardClient.tsx index e94c925..c7e5eab 100644 --- a/app/[locale]/DashboardClient.tsx +++ b/app/[locale]/DashboardClient.tsx @@ -1,17 +1,21 @@ 'use client' import { useState, useEffect } from 'react' -import { useAccount } from 'wagmi' +import { useParams } from 'next/navigation' +import { useLocale } from 'next-intl' +import { useAccount, useSignMessage } from 'wagmi' import type { TrumpPost, BotPerformance, Candle } from '@/types' import { useDashboardStore } from '@/store/dashboard' import { usePriceSocket } from '@/lib/useRealtimeData' -import { getPrices, getUserPublic } from '@/lib/api' +import { getPerformance, getPrices, getUserPublic } from '@/lib/api' +import { getCachedViewEnvelope, getOrCreateViewEnvelope } from '@/lib/signedRequest' import ChartPanel from '@/components/dashboard/ChartPanel' import PostRow, { SignalPill, SourceIcon, fmtPct, timeAgo, TimeAgo, LocalDateTime } from '@/components/dashboard/PostCards' +import SignalMonitor from '@/components/dashboard/SignalMonitor' +import OpenPositions from '@/components/positions/OpenPositions' interface Props { initialPosts: TrumpPost[] - initialPerformance?: BotPerformance } // ── Inline post detail panel shown in the right rail ────────────────────────── @@ -124,19 +128,27 @@ function SelectHint() {

- Click any marker on the chart
or a post below to see details + Click any marker on the chart or a post below to see details

) } // ── Main dashboard ───────────────────────────────────────────────────────────── -export default function DashboardClient({ initialPosts, initialPerformance }: Props) { +export default function DashboardClient({ initialPosts }: Props) { + const intlLocale = useLocale() + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json 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 { signMessageAsync } = useSignMessage() + const params = useParams() + const locale = (typeof params?.locale === 'string' ? params.locale : 'en') const [posts, setPosts] = useState(initialPosts) + const [performance, setPerformance] = useState(undefined) const [candles, setCandles] = useState([]) + const [chartErr, setChartErr] = useState('') + const [chartReload, setChartReload] = useState(0) const [selectedPostId, setSelectedPostId] = useState(null) // For 1D: show all posts from selected day const [selectedDayPosts, setSelectedDayPosts] = useState(null) @@ -146,6 +158,7 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr setSubscribed(false) setHlApiKeySet(false) setBotReadiness('unknown') + setPerformance(undefined) return } // Clear account-scoped bot state immediately when the connected wallet @@ -162,6 +175,25 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr .catch(() => {}) }, [address, isConnected, setSubscribed, setHlApiKeySet, setBotReadiness]) + useEffect(() => { + if (!isConnected || !address) { + setPerformance(undefined) + return + } + let cancelled = false + ;(async () => { + try { + const env = getCachedViewEnvelope('view_performance', address) + ?? await getOrCreateViewEnvelope({ action: 'view_performance', wallet: address, signMessageAsync }) + const data = await getPerformance(address, env) + if (!cancelled) setPerformance(data) + } catch { + if (!cancelled) setPerformance(undefined) + } + })() + return () => { cancelled = true } + }, [address, isConnected, signMessageAsync]) + usePriceSocket({ onPrice: (a, price) => setLivePrice(a, price), onNewPost: (post) => setPosts((prev) => [post as TrumpPost, ...prev].slice(0, 500)), @@ -169,13 +201,31 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr useEffect(() => { setCandles([]) + setChartErr('') getPrices(asset, timeframe) - .then(setCandles) - .catch(() => {}) - }, [asset, timeframe]) + .then(c => { setCandles(c); setChartErr('') }) + .catch(e => setChartErr(e instanceof Error ? e.message : (isZh ? '价格数据加载失败' : 'Failed to load price data'))) + }, [asset, timeframe, chartReload, isZh]) const selectedPost = posts.find(p => p.id === selectedPostId) ?? null - const recentPosts = posts.slice(0, 8) + + // Pinned BTC bottom-reversal alert: the rarest, highest-conviction signal. + // Surface the most recent btc_bottom_reversal post fired in the last 21 + // days right at the top of the overview so it's never missed. + const btcReversalAlert = (() => { + const cutoff = Date.now() - 21 * 24 * 3600 * 1000 + const hits = posts + .filter(p => (p.source || '') === 'btc_bottom_reversal' + && new Date(p.published_at).getTime() >= cutoff) + .sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime()) + return hits[0] ?? null + })() + + // Show actionable signals first (buy/short), then most recent hold/neutral. + // Cap at 8 total so the list doesn't get too long. + const actionable = posts.filter(p => p.signal === 'buy' || p.signal === 'short').slice(0, 4) + const recentOthers = posts.filter(p => p.signal !== 'buy' && p.signal !== 'short').slice(0, 4) + const recentPosts = [...actionable, ...recentOthers].slice(0, 8) const lastCandle = candles[candles.length - 1] // Live price beats the most recent candle close — the candle is only @@ -205,26 +255,67 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr const todayKey = new Date().toISOString().slice(0, 10) const signalsToday = posts.filter(p => p.published_at?.slice(0, 10) === todayKey).length const actionablePosts = posts.filter(p => p.signal === 'buy' || p.signal === 'short').length - const winRate = initialPerformance?.win_rate ?? 0 - const netPnl = initialPerformance?.net_pnl_usd ?? 0 + const winRate = performance?.win_rate ?? 0 + const netPnl = performance?.net_pnl_usd ?? 0 const hasPriceData = candles.length > 0 - const hasPerformanceData = Boolean(initialPerformance) + const hasPerformanceData = Boolean(performance) return (
-

Signal monitor

+

{isZh ? '信号总览' : 'Signal monitor'}

- {actionablePosts} actionable signals · Auto-trader {botReadiness === 'ready' ? 'ready' : hlApiKeySet ? 'saved' : isSubscribed ? 'subscribed' : 'standby'} · {hasPriceData ? 'Live market context' : 'Waiting for market data'} + {`${actionablePosts} actionable signals · Auto-trader ${botReadiness === 'ready' ? 'ready' : hlApiKeySet ? 'saved' : isSubscribed ? 'subscribed' : 'standby'} · ${hasPriceData ? 'Live market context' : 'Waiting for market data'}`}

Live feed - {totalPosts} posts tracked + {`${totalPosts} posts tracked`}
+ {/* Pinned BTC bottom-reversal alert — the rarest / highest-conviction + signal. Always sits ABOVE everything so it's never missed. */} + {btcReversalAlert && ( + +
+ + ⚡ BTC bottom-reversal signal + + + conf {Math.round(btcReversalAlert.ai_confidence)} + + + {timeAgo(btcReversalAlert.published_at)} + + + Open BTC signal → + +
+
+ {btcReversalAlert.text} +
+
+ )} + + {/* Open positions — what's on the book right now. Renders only when + a subscribed wallet is connected, so guests see the normal feed. */} + + {/* KPI Row */}
@@ -238,17 +329,17 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
Signals today
{signalsToday}
-
{actionablePosts} actionable total
+
{`${actionablePosts} actionable total`}
-
30d Net P&L
+
30d Net P&L
{netPnl >= 0 ? '+$' : '-$'}{Math.abs(netPnl).toLocaleString('en-US', { maximumFractionDigits: 0 })}
{hasPerformanceData ? 'Bot performance' : 'Performance pending'}
Win rate
-
{initialPerformance ? (winRate * 100).toFixed(1) + '%' : '—'}
-
{hasPerformanceData ? `${initialPerformance?.total_trades ?? 0} trades` : 'Waiting for trade history'}
+
{performance ? (winRate * 100).toFixed(1) + '%' : '—'}
+
{hasPerformanceData ? `${performance?.total_trades ?? 0} trades` : 'Connect wallet to load your trade history'}
@@ -283,6 +374,15 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
+ {chartErr && ( +
+ ⚠️ {asset} {timeframe} chart failed to load — {chartErr}{' '} + +
+ )}

Recent signals

- Showing {recentPosts.length} of {totalPosts} + + {actionable.length > 0 ? `${actionable.length} actionable · ` : ''} + {`${totalPosts} tracked`} +
{recentPosts.map(p => ( @@ -429,6 +532,9 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr ) : ( )} + + {/* Breakout signal monitor */} +
diff --git a/app/[locale]/Providers.tsx b/app/[locale]/Providers.tsx index cfb031f..3f0eeb2 100644 --- a/app/[locale]/Providers.tsx +++ b/app/[locale]/Providers.tsx @@ -5,6 +5,7 @@ import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' import { config } from '@/lib/wagmi' +import { WsProvider } from '@/lib/wsContext' import '@rainbow-me/rainbowkit/styles.css' interface ProvidersProps { @@ -15,7 +16,7 @@ export default function Providers({ children }: ProvidersProps) { const [queryClient] = useState(() => new QueryClient()) return ( - + - {children} + {/* Single shared WebSocket — all components subscribe via useWsSubscribe */} + + {children} + diff --git a/app/[locale]/analytics/AnalyticsPageClient.tsx b/app/[locale]/analytics/AnalyticsPageClient.tsx new file mode 100644 index 0000000..134512a --- /dev/null +++ b/app/[locale]/analytics/AnalyticsPageClient.tsx @@ -0,0 +1,227 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useLocale } from 'next-intl' +import { useAccount, useSignMessage } from 'wagmi' +import { getPerformance, getTrades, getSignalAccuracy } from '@/lib/api' +import type { BotPerformance, BotTrade } from '@/types' +import type { SignalAccuracy } from '@/lib/api' +import { getCachedViewEnvelope, getOrCreateViewEnvelope } from '@/lib/signedRequest' + +type Period = '7d' | '30d' | '90d' | 'All' + +function fmtMoney(n: number) { + if (n == null || isNaN(n)) return '—' + const abs = Math.abs(n) + const s = abs.toLocaleString('en-US', { maximumFractionDigits: 0 }) + if (n < 0) return '-$' + s + if (n > 0) return '+$' + s + return '$' + s +} + +function fmtHold(s: number) { + if (s < 60) return s + 's' + const m = Math.floor(s / 60) + if (m < 60) return m + 'm' + return Math.floor(m / 60) + 'h ' + (m % 60) + 'm' +} + +function inPeriod(iso: string, period: Period) { + if (period === 'All') return true + const days = Number.parseInt(period, 10) + if (Number.isNaN(days)) return true + const time = new Date(iso).getTime() + return time >= Date.now() - days * 24 * 60 * 60 * 1000 +} + +function calcDrawdownPct(trades: BotTrade[]) { + const ordered = [...trades] + .filter((t) => t.pnl_usd !== null && t.pnl_usd !== undefined) + .sort((a, b) => new Date(a.closed_at).getTime() - new Date(b.closed_at).getTime()) + let equity = 0 + let peak = 0 + let maxDrawdownPct = 0 + + for (const trade of ordered) { + equity += trade.pnl_usd as number + peak = Math.max(peak, equity) + if (peak > 0) { + maxDrawdownPct = Math.max(maxDrawdownPct, ((peak - equity) / peak) * 100) + } + } + + return maxDrawdownPct +} + +export default function AnalyticsPageClient() { + const locale = useLocale() + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + const { address, isConnected } = useAccount() + const { signMessageAsync } = useSignMessage() + const [perf, setPerf] = useState(null) + const [trades, setTrades] = useState([]) + const [accuracy, setAccuracy] = useState(null) + const [period, setPeriod] = useState('30d') + + useEffect(() => { + let cancelled = false + if (!address || !isConnected) { + setPerf(null) + setTrades([]) + setAccuracy(null) + return + } + ;(async () => { + try { + const perfEnv = getCachedViewEnvelope('view_performance', address) + ?? await getOrCreateViewEnvelope({ action: 'view_performance', wallet: address, signMessageAsync }) + const tradesEnv = getCachedViewEnvelope('view_trades', address) + ?? await getOrCreateViewEnvelope({ action: 'view_trades', wallet: address, signMessageAsync }) + const [p, t, a] = await Promise.all([ + getPerformance(address, perfEnv).catch(() => null), + getTrades(address, tradesEnv, 100, 1).catch(() => []), + getSignalAccuracy().catch(() => null), + ]) + if (!cancelled) { + setPerf(p) + setTrades(t) + setAccuracy(a) + } + } catch { + if (!cancelled) { + setPerf(null) + setTrades([]) + } + } + })() + return () => { cancelled = true } + }, [address, isConnected, signMessageAsync]) + + const filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period)) + const pricedTrades = filteredTrades.filter( + (t) => t.pnl_usd !== null && t.pnl_usd !== undefined, + ) + const pnls = pricedTrades.map((t) => t.pnl_usd as number) + const totalPnl = pnls.reduce((s, p) => s + p, 0) + const wins = pnls.filter((p) => p > 0).length + const winRate = pricedTrades.length ? (wins / pricedTrades.length) * 100 : 0 + const bestTrade = pnls.length ? Math.max(...pnls) : 0 + const worstTrade = pnls.length ? Math.min(...pnls) : 0 + const avgTrade = pnls.length ? totalPnl / pnls.length : 0 + const avgHold = filteredTrades.length ? filteredTrades.reduce((sum, trade) => sum + trade.hold_seconds, 0) / filteredTrades.length : 0 + const maxDrawdown = period === '30d' && perf ? perf.max_drawdown_pct : calcDrawdownPct(filteredTrades) + const summaryPnl = period === '30d' && perf && trades.length >= filteredTrades.length + ? perf.net_pnl_usd + : totalPnl + + const metrics = [ + { k: isZh ? '最大回撤' : 'Max drawdown', v: filteredTrades.length ? maxDrawdown.toFixed(1) + '%' : '—', sub: isZh ? '从高点到低点的最大跌幅' : 'Worst peak-to-trough', down: true }, + { k: isZh ? '总交易数' : 'Total trades', v: String(filteredTrades.length || '—'), sub: isZh ? '已平仓机器人交易' : 'Closed bot executions' }, + { k: isZh ? '平均持仓时间' : 'Avg hold time', v: filteredTrades.length ? fmtHold(Math.round(avgHold)) : '—', sub: isZh ? '按单笔计算' : 'Per trade' }, + { k: isZh ? '平均单笔盈亏' : 'Avg trade P&L', v: fmtMoney(avgTrade), sub: isZh ? '每笔交易均值' : 'Mean per trade', up: avgTrade > 0 }, + { k: isZh ? '最佳单笔' : 'Best trade', v: fmtMoney(bestTrade), sub: isZh ? '单笔最大盈利' : 'Largest single win', up: true }, + { k: isZh ? '最差单笔' : 'Worst trade', v: fmtMoney(worstTrade), sub: isZh ? '单笔最大亏损' : 'Largest single loss', down: true }, + ] + + return ( +
+
+
+

{isZh ? '分析面板' : 'Analytics'}

+

{isZh ? `查看 ${period} 窗口内的信号与交易表现。` : `Deep dive into ${period} of signals and trades.`}

+
+
+ {(['7d', '30d', '90d', 'All'] as const).map(p => ( + + ))} +
+
+ +
+
+
+
{isZh ? '表现' : 'Performance'} · {period}
+
+ {filteredTrades.length ? fmtMoney(summaryPnl) : '—'} +
+
+ = 50 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% {isZh ? '胜率' : 'win rate'} + {isZh ? `${filteredTrades.length} 笔交易` : `${filteredTrades.length} trades`} +
+
+
+
+ +
+ {metrics.map(m => ( +
+
{m.k}
+
{m.v}
+
{m.sub}
+
+ ))} +
+ + {accuracy && ( +
+
{isZh ? `AI 信号准确率 · ${accuracy.total_directional_signals} 条方向性信号` : `AI Signal Accuracy · ${accuracy.total_directional_signals} directional signals`}
+
+
+
{isZh ? '整体' : 'Overall'}
+ {(['m5','m15','m1h'] as const).map(w => { + const d = accuracy.overall[w] + const pct = d.accuracy_pct + const color = pct >= 55 ? 'var(--up)' : pct >= 45 ? 'var(--ink-2)' : 'var(--down)' + return ( +
+ {w.replace('m','').replace('1h','1h')} + {pct.toFixed(0)}% +
+ ) + })} +
{isZh ? `${accuracy.overall.m5.checked} 条已测` : `${accuracy.overall.m5.checked} measured`}
+
+ {Object.entries(accuracy.by_signal).map(([sig, data]) => { + const label = sig === 'buy' ? (isZh ? '🟢 做多' : '🟢 Buy') : sig === 'short' ? (isZh ? '🔴 做空' : '🔴 Short') : (isZh ? '🟡 卖出' : '🟡 Sell') + return ( +
+
{label} ({data.count})
+ {(['m5','m15','m1h'] as const).map(w => { + const d = data[w] + if (d.checked < 2) return ( +
+ {w} + +
+ ) + const pct = d.accuracy_pct + const color = pct >= 55 ? 'var(--up)' : pct >= 45 ? 'var(--ink-2)' : 'var(--down)' + return ( +
+ {w} + {pct.toFixed(0)}% +
+ ) + })} +
{isZh ? `${data.m5.checked} 条已测` : `${data.m5.checked} measured`}
+
+ ) + })} +
+
+ )} + + {!filteredTrades.length && ( +
+

+ {!isConnected || !address + ? (isZh ? '连接钱包以加载你的分析数据。' : 'Connect your wallet to load your analytics.') + : trades.length + ? (isZh ? `${period} 时间窗内还没有已平仓交易。` : `No trades closed in the ${period} window yet.`) + : (isZh ? '还没有交易数据。机器人开始执行后,这里会自动出现统计。' : 'No trade data yet. The bot will populate analytics once it starts executing.')} +

+
+ )} +
+ ) +} diff --git a/app/[locale]/analytics/page.tsx b/app/[locale]/analytics/page.tsx index c4cb8a6..7c11283 100644 --- a/app/[locale]/analytics/page.tsx +++ b/app/[locale]/analytics/page.tsx @@ -1,151 +1,28 @@ -'use client' +import type { Metadata } from 'next' +import { getLocale } from 'next-intl/server' +import AnalyticsPageClient from './AnalyticsPageClient' -import { useState, useEffect } from 'react' -import { getPerformance, getTrades } from '@/lib/api' -import type { BotPerformance, BotTrade } from '@/types' +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://trumpsignal.com' -type Period = '7d' | '30d' | '90d' | 'All' - -function fmtMoney(n: number) { - if (n == null || isNaN(n)) return '—' - const abs = Math.abs(n) - const s = abs.toLocaleString('en-US', { maximumFractionDigits: 0 }) - if (n < 0) return '-$' + s - if (n > 0) return '+$' + s - return '$' + s -} - -function fmtHold(s: number) { - if (s < 60) return s + 's' - const m = Math.floor(s / 60) - if (m < 60) return m + 'm' - return Math.floor(m / 60) + 'h ' + (m % 60) + 'm' -} - -function inPeriod(iso: string, period: Period) { - if (period === 'All') return true - const days = Number.parseInt(period, 10) - if (Number.isNaN(days)) return true - const time = new Date(iso).getTime() - return time >= Date.now() - days * 24 * 60 * 60 * 1000 -} - -function calcDrawdownPct(trades: BotTrade[]) { - // Skip externally-closed trades (pnl_usd null) — including them as `+ null` - // turns equity into NaN and the whole drawdown chart goes blank. - const ordered = [...trades] - .filter((t) => t.pnl_usd !== null && t.pnl_usd !== undefined) - .sort((a, b) => new Date(a.closed_at).getTime() - new Date(b.closed_at).getTime()) - let equity = 0 - let peak = 0 - let maxDrawdownPct = 0 - - for (const trade of ordered) { - equity += trade.pnl_usd as number - peak = Math.max(peak, equity) - if (peak > 0) { - maxDrawdownPct = Math.max(maxDrawdownPct, ((peak - equity) / peak) * 100) - } +export async function generateMetadata(): Promise { + const locale = await getLocale() + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + const title = isZh ? '交易分析与表现' : 'Trading Analytics & Performance' + const description = isZh + ? '查看胜率、最大回撤、平均持仓时间、信号准确率和历史交易表现,评估 Trump Alpha 的真实执行质量。' + : 'Review win rate, drawdown, average hold time, signal accuracy, and historical trade performance to evaluate live execution quality.' + return { + title, + description, + alternates: { + canonical: `${siteUrl}/${locale}/analytics`, + languages: { + en: `${siteUrl}/en/analytics`, + }, + }, } - - return maxDrawdownPct } export default function AnalyticsPage() { - const [perf, setPerf] = useState(null) - const [trades, setTrades] = useState([]) - const [period, setPeriod] = useState('30d') - - useEffect(() => { - Promise.all([ - getPerformance().catch(() => null), - getTrades(100, 1).catch(() => []), - ]).then(([p, t]) => { - setPerf(p) - setTrades(t) - }) - }, []) - - const filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period)) - // Trades closed externally on Hyperliquid have pnl_usd === null. Including - // them silently in reduce/Math.max/Math.min produces NaN ("$NaN P&L") and - // also pollutes win_rate. Aggregate only on the priced subset. - const pricedTrades = filteredTrades.filter( - (t) => t.pnl_usd !== null && t.pnl_usd !== undefined, - ) - const pnls = pricedTrades.map((t) => t.pnl_usd as number) - const totalPnl = pnls.reduce((s, p) => s + p, 0) - const wins = pnls.filter((p) => p > 0).length - const winRate = pricedTrades.length ? (wins / pricedTrades.length) * 100 : 0 - const bestTrade = pnls.length ? Math.max(...pnls) : 0 - const worstTrade = pnls.length ? Math.min(...pnls) : 0 - const avgTrade = pnls.length ? totalPnl / pnls.length : 0 - const avgHold = filteredTrades.length ? filteredTrades.reduce((sum, trade) => sum + trade.hold_seconds, 0) / filteredTrades.length : 0 - const maxDrawdown = period === '30d' && perf ? perf.max_drawdown_pct : calcDrawdownPct(filteredTrades) - const summaryPnl = period === '30d' && perf && trades.length >= filteredTrades.length - ? perf.net_pnl_usd - : totalPnl - - const metrics = [ - { k: 'Max drawdown', v: filteredTrades.length ? maxDrawdown.toFixed(1) + '%' : '—', sub: 'Worst peak-to-trough', down: true }, - { k: 'Total trades', v: String(filteredTrades.length || '—'), sub: 'Closed bot executions' }, - { k: 'Avg hold time', v: filteredTrades.length ? fmtHold(Math.round(avgHold)) : '—', sub: 'Per trade' }, - { k: 'Avg trade P&L', v: fmtMoney(avgTrade), sub: 'Mean per trade', up: avgTrade > 0 }, - { k: 'Best trade', v: fmtMoney(bestTrade), sub: 'Largest single win', up: true }, - { k: 'Worst trade', v: fmtMoney(worstTrade), sub: 'Largest single loss', down: true }, - ] - - return ( -
-
-
-

Analytics

-

Deep dive into {period} of signals and trades.

-
-
- {(['7d', '30d', '90d', 'All'] as const).map(p => ( - - ))} -
-
- - {/* Summary card */} -
-
-
-
Performance · {period}
-
- {filteredTrades.length ? fmtMoney(summaryPnl) : '—'} -
-
- = 50 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% win rate - {filteredTrades.length} trades -
-
-
-
- - {/* Metric grid */} -
- {metrics.map(m => ( -
-
{m.k}
-
{m.v}
-
{m.sub}
-
- ))} -
- - {/* No data message */} - {!filteredTrades.length && ( -
-

- {trades.length - ? `No trades closed in the ${period} window yet.` - : 'No trade data yet. The bot will populate analytics once it starts executing.'} -

-
- )} -
- ) + return } diff --git a/app/[locale]/archive/page.tsx b/app/[locale]/archive/page.tsx new file mode 100644 index 0000000..ee19927 --- /dev/null +++ b/app/[locale]/archive/page.tsx @@ -0,0 +1,110 @@ +'use client' + +import { useState, useEffect, useMemo } from 'react' +import { useLocale } from 'next-intl' +import type { TrumpPost } from '@/types' +import { getPosts } from '@/lib/api' +import PostRow from '@/components/dashboard/PostCards' + +/** + * Archive — legacy / test signals (rsi_reversal, sma_reclaim, breakout, + * test, phase1…). NOT a live system. Kept only so old data is inspectable. + */ +export default function ArchivePage() { + const locale = useLocale() + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + const [posts, setPosts] = useState([]) + const [loading, setLoading] = useState(true) + const [loadErr, setLoadErr] = useState('') + const [src, setSrc] = useState('all') + + useEffect(() => { + getPosts(500, 1) + .then(p => { setPosts(p); setLoadErr('') }) + .catch(e => setLoadErr(e instanceof Error ? e.message : (isZh ? '历史归档加载失败' : 'Failed to load archive'))) + .finally(() => setLoading(false)) + }, [isZh]) + + // Archive = legacy / test data only. Exclude every live signal source so + // active modules don't leak in here as users explore old experiments. Keep + // this set in sync with sources emitted by app/services/scanners/*. + const LIVE_SOURCES = new Set([ + 'truth', + 'btc_bottom_reversal', + 'funding_reversal', + 'kol_divergence', + ]) + const archivePosts = useMemo( + () => posts.filter(p => !LIVE_SOURCES.has(p.source || '')), + [posts], + ) + const sources = useMemo(() => { + const m: Record = {} + for (const p of archivePosts) m[p.source || '?'] = (m[p.source || '?'] || 0) + 1 + return Object.entries(m).sort((a, b) => b[1] - a[1]) + }, [archivePosts]) + const filtered = useMemo( + () => src === 'all' ? archivePosts : archivePosts.filter(p => p.source === src), + [archivePosts, src], + ) + + return ( +
+
+
+

Archive

+

{isZh ? '历史 / 测试数据,不属于实时系统。' : 'Legacy / test data — NOT a live system'}

+
+
+ +
+
+ {isZh + ? '这些来源已经被 BTC 周期底部 2-of-3 共振系统替代,不再参与排程。这里只作为历史查阅使用,机器人不会根据它们执行交易。' + : 'These sources were superseded by the BTC bottom-reversal 2-of-3 price confluence and are no longer scheduled. Shown here only for historical inspection — the bot does not act on them.'} +
+
+ +
+ {[['all', archivePosts.length] as [string, number], ...sources].map(([s, n]) => ( + + ))} +
+ + {loading &&
{isZh ? '加载中…' : 'Loading…'}
} + {!loading && loadErr && ( +
+ ⚠️ {isZh ? `无法加载归档:${loadErr}` : `Couldn’t load archive — ${loadErr}`} +
+ +
+
+ )} + {!loading && !loadErr && filtered.length === 0 && ( +
+ {isZh ? '没有可显示的历史信号。' : 'No archived signals.'} +
+ )} + {!loading && filtered.length > 0 && ( +
+ {filtered.map(p => )} +
+ )} +
+ ) +} diff --git a/app/[locale]/btc/BtcPageClient.tsx b/app/[locale]/btc/BtcPageClient.tsx new file mode 100644 index 0000000..fd576a0 --- /dev/null +++ b/app/[locale]/btc/BtcPageClient.tsx @@ -0,0 +1,417 @@ +'use client' + +import { useState, useEffect, useMemo } from 'react' +import { useLocale } from 'next-intl' +import type { TrumpPost } from '@/types' +import { getPosts, getFundingSnapshot, type FundingSnapshot } from '@/lib/api' +import { swrFetch } from '@/lib/cache' +import PostRow from '@/components/dashboard/PostCards' +import SystemControl from '@/components/signals/SystemControl' + +/** + * System 2 — BTC bottom-reversal. Its own dedicated page. + * Shows the scanner control + ONLY source === 'btc_bottom_reversal' signals. + */ + +const SENTIMENTS = ['all', 'bullish', 'bearish', 'neutral'] as const +type SentimentFilter = (typeof SENTIMENTS)[number] +type BtcTab = 'bottom' | 'funding' + +interface BtcSignalPageProps { + initialPosts?: TrumpPost[] | null + initialFundingSnapshot?: FundingSnapshot | null +} + +export default function BtcSignalPage({ + initialPosts = null, + initialFundingSnapshot = null, +}: BtcSignalPageProps) { + const locale = useLocale() + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + const [posts, setPosts] = useState(initialPosts ?? []) + const [loading, setLoading] = useState(initialPosts === null) + const [loadErr, setLoadErr] = useState('') + const [sentFilter, setSentFilter] = useState('all') + const [tab, setTab] = useState('bottom') + + useEffect(() => { + // BTC on-chain signals update daily — 30 min TTL is safe + swrFetch( + 'posts-500', + 3 * 60_000, + () => getPosts(500, 1), + fresh => setPosts(fresh), + ) + .then(p => { setPosts(p); setLoadErr('') }) + .catch(e => setLoadErr(e instanceof Error ? e.message : (isZh ? '信号加载失败' : 'Failed to load signals'))) + .finally(() => setLoading(false)) + }, [isZh]) + + // Tab-scoped post source. 'bottom' = MVRV/200WMA confluence, + // 'funding' = funding-rate extreme reversal. + const tabSource = tab === 'bottom' ? 'btc_bottom_reversal' : 'funding_reversal' + const btcPosts = useMemo( + () => posts.filter(p => (p.source || '') === tabSource), + [posts, tabSource], + ) + const filtered = useMemo( + () => btcPosts.filter(p => sentFilter === 'all' || p.sentiment === sentFilter), + [btcPosts, sentFilter], + ) + + const sentimentLabels: Record = { + all: isZh ? '全部' : 'All', + bullish: isZh ? '看多' : 'Bullish', + bearish: isZh ? '看空' : 'Bearish', + neutral: isZh ? '中性' : 'Neutral', + } + + return ( +
+
+
+

{isZh ? '② BTC 信号' : '② BTC Signal'}

+

+ {tab === 'bottom' + ? `Macro cycle bottom · ${btcPosts.length} signals` + : `Funding-rate extreme reversal · ${btcPosts.length} signals`} +

+
+ Live +
+ + + + {tab === 'bottom' && ( +
+ This module tracks Bitcoin macro-bottom conditions. It only fires a long-only cycle signal when at least two of three classic bottom markers agree: AHR999, the 200-week moving average, and Pi Cycle Bottom. +
+ )} + +
+ + +
+ + {tab === 'bottom' && ( +
+ <> + Fires when ≥2 of 3 classic bottom signals agree:{' '} + AHR999 < 0.45 · price ≤ 200-week + MA · Pi Cycle Bottom. Long only, low frequency + (2–4 fires per cycle). Scans daily 00:45 UTC. Designed to ride the + full cycle: holds up to 18 months, trailing-stop + exit, no take-profit. Keep leverage ≤2× — amplification comes from + pyramiding, not from cranking leverage. + +
+ )} + + {tab === 'funding' && ( + + )} + +
+ {SENTIMENTS.map(f => ( + + ))} +
+ + {loading && } + {!loading && loadErr && ( +
+ ⚠️ {`Couldn’t load signals — ${loadErr}`} +
+ +
+
+ )} + {!loading && !loadErr && filtered.length === 0 && ( +
+ {tab === 'bottom' + ? 'No macro-bottom signals yet.' + : 'No funding-reversal signals yet.'} +
+ {tab === 'bottom' + ? 'This scanner is intentionally rare — only fires at genuine macro bottoms (daily 00:45 UTC).' + : 'Fires when 30-day cumulative funding exceeds ±3% AND starts mean-reverting. See the live panel above for current state.'} +
+
+ )} + {!loading && filtered.length > 0 && ( +
+ {filtered.map(p => )} +
+ )} +
+ ) +} + +// ── Funding rate live panel ───────────────────────────────────────────────── +function FundingPanel({ + isZh, + initialSnapshot = null, +}: { + isZh: boolean + initialSnapshot?: FundingSnapshot | null +}) { + const [snap, setSnap] = useState(initialSnapshot) + const [err, setErr] = useState('') + + useEffect(() => { + let alive = true + function load() { + swrFetch( + 'funding-snapshot', + 5 * 60_000, // 5 min — funding cadence is 1–8h, no point polling faster + () => getFundingSnapshot(), + fresh => { if (alive) setSnap(fresh) }, + ) + .then(s => { if (alive) { setSnap(s); setErr('') } }) + .catch(e => { if (alive) setErr(e instanceof Error ? e.message : (isZh ? '加载失败' : 'load failed')) }) + } + load() + const id = setInterval(load, 5 * 60_000) + return () => { alive = false; clearInterval(id) } + }, [isZh]) + + if (err) { + return ( +
+ {`Funding snapshot failed — ${err}`} +
+ ) + } + if (!snap) { + return ( +
+
+
+
+
+ ) + } + if (!snap.ok) { + return ( +
+ {`Funding data unavailable — ${snap.error || 'unknown error'}`} +
+ ) + } + + // Color the cumulative figure by direction + extremity. + const cum = snap.cum_30d_pct ?? 0 + const thr = snap.extreme_threshold_pct ?? 3 + const extreme = Math.abs(cum) >= thr + const cumColor = extreme ? (cum > 0 ? 'var(--down)' : 'var(--up)') : 'var(--ink)' + + // Direction hint if extreme (longs paying = SHORT setup; shorts paying = LONG) + const directionHint = extreme + ? (cum > 0 + ? 'Longs are crowded; reversal bias is SHORT.' + : 'Shorts are crowded; reversal bias is LONG.') + : 'Funding remains inside the normal range.' + + return ( +
+
+
+ BTC perp funding · live +
+
+ {`${snap.cadence_hours}h cadence · ${snap.coverage_days}d coverage · Binance`} +
+
+ +
+ + + = 0 ? '+' : ''}${cum.toFixed(2)}%`} + color={cumColor} + sub={`extreme @ ±${thr}%`} + /> + +
+ +
+ {directionHint} + {snap.debug?.reason && ( + + ({String(snap.debug.reason).replaceAll('_', ' ')}) + + )} +
+ + {snap.history && snap.history.length > 1 && ( + + )} +
+ ) +} + +function StatBox({ label, value, color, sub }: { + label: string; value: string; color?: string; sub?: string +}) { + return ( +
+
{label}
+
{value}
+ {sub &&
{sub}
} +
+ ) +} + +// Tiny inline SVG sparkline of the last 7 days of funding cycles. +// `extremeCumPct` is the 30d cumulative threshold (e.g. 3.0). To project it as +// a per-cycle reference line we divide by the number of cycles in 30 days at +// the venue's cadence (24h × 30d / cadence_h). This is the per-cycle rate that +// — sustained for 30 days — would hit the extreme bucket. +function FundingSparkline({ history, cadenceHours, extremeCumPct, isZh }: { + history: { t: number; rate_pct: number }[] + cadenceHours: number + extremeCumPct: number + isZh: boolean +}) { + const W = 600, H = 90, PAD = 6 + const cyclesIn30d = Math.max(1, (30 * 24) / Math.max(cadenceHours, 0.5)) + const perCycleThr = extremeCumPct / cyclesIn30d // e.g. 3% / 90 ≈ 0.033% + + const rates = history.map(h => h.rate_pct) + // Ensure threshold lines are always visible in the y-range + const minR = Math.min(...rates, -perCycleThr * 1.2) + const maxR = Math.max(...rates, perCycleThr * 1.2) + const span = maxR - minR || 1 + const x = (i: number) => PAD + (i / (history.length - 1)) * (W - 2 * PAD) + const y = (r: number) => PAD + (1 - (r - minR) / span) * (H - 2 * PAD) + const zeroY = y(0) + const posThrY = y(perCycleThr) + const negThrY = y(-perCycleThr) + const path = history.map((h, i) => `${i === 0 ? 'M' : 'L'} ${x(i)} ${y(h.rate_pct)}`).join(' ') + const last = history[history.length - 1] + // Color the dot by whether the latest cycle is inside the danger band + const inDanger = Math.abs(last.rate_pct) >= perCycleThr + const dotColor = inDanger + ? (last.rate_pct > 0 ? 'var(--down)' : 'var(--up)') + : 'var(--amber, #f59e0b)' + + return ( +
+
+ {isZh ? '7 日资金费率(单周期 %)' : '7-day funding (per-cycle %)'} + {isZh ? `危险区间:每周期 ±${perCycleThr.toFixed(3)}%` : `danger band: ±${perCycleThr.toFixed(3)}% / cycle`} +
+ + {/* danger zones — shaded above +thr and below -thr */} + + + + {/* zero line */} + + {/* positive / negative threshold lines */} + + + + {/* funding curve */} + + {/* current value dot */} + + +
+ ) +} + +function BtcSkeleton() { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) +} diff --git a/app/[locale]/btc/page.tsx b/app/[locale]/btc/page.tsx new file mode 100644 index 0000000..6a79579 --- /dev/null +++ b/app/[locale]/btc/page.tsx @@ -0,0 +1,71 @@ +import { getFundingSnapshot, getPosts } from '@/lib/api' +import type { Metadata } from 'next' +import BtcPageClient from './BtcPageClient' + +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://trumpsignal.com' +export const revalidate = 30 + +interface BtcPageProps { + params: Promise<{ locale: string }> +} + +export async function generateMetadata({ + params, +}: BtcPageProps): Promise { + const { locale } = await params + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + + return { + title: isZh ? 'BTC 周期底部信号扫描器' : 'BTC Bottom Reversal Scanner', + description: isZh + ? 'Trump Alpha 的比特币周期底部扫描器,使用 AHR999、200 周均线和 Pi Cycle Bottom 三项经典信号做 2-of-3 共振判断,识别低频、高确定性的 BTC 长线底部区域。' + : 'Bitcoin macro-bottom scanner using a 2-of-3 price confluence: AHR999 deep-value zone, price near the 200-week moving average, and Pi Cycle Bottom. Rare, high-conviction long-only signals.', + keywords: isZh + ? [ + 'BTC 底部信号', + '比特币周期底部', + 'AHR999', + '200 周均线', + 'Pi Cycle Bottom', + '比特币反转信号', + '加密价格共振', + ] + : [ + 'Bitcoin bottom signal', + 'AHR999', + '200-week moving average', + 'Pi Cycle Bottom', + 'BTC cycle bottom', + 'Bitcoin macro bottom', + 'Bitcoin reversal signal', + 'crypto price confluence', + ], + openGraph: { + title: isZh ? 'BTC 周期底部信号扫描器 | Trump Alpha' : 'BTC Bottom Reversal Scanner | Trump Alpha', + description: isZh + ? '用 AHR999、200 周均线和 Pi Cycle Bottom 识别比特币宏观底部。低频,但每次触发都服务于长周期反转。' + : 'Bitcoin macro-bottom detector: 2-of-3 confluence across AHR999, 200-week MA, and Pi Cycle Bottom. Rare, high-conviction long-only signals.', + }, + alternates: { + canonical: `${siteUrl}/${locale}/btc`, + languages: { + en: `${siteUrl}/en/btc`, + 'x-default': `${siteUrl}/en/btc`, + }, + }, + } +} + +export default async function BtcPage() { + const [posts, fundingSnapshot] = await Promise.all([ + getPosts(500, 1).catch(() => null), + getFundingSnapshot().catch(() => null), + ]) + + return ( + + ) +} diff --git a/app/[locale]/case-studies/page.tsx b/app/[locale]/case-studies/page.tsx new file mode 100644 index 0000000..c9f2715 --- /dev/null +++ b/app/[locale]/case-studies/page.tsx @@ -0,0 +1,425 @@ +import type { Metadata } from 'next' +import Link from 'next/link' + +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://trumpsignal.com' + +interface CaseStudy { + id: string + date: string + source: string + title: string + summary: string + signal: 'LONG' | 'SHORT' | 'DIVERGENCE' + asset: string + priceImpact: string + impactWindow: string + detail: string + evidence: string + externalUrl?: string +} + +type CaseStudiesCopy = { + title: string + description: string + keywords: string[] + ogTitle: string + ogDescription: string + heroTitle: string + heroSubtitle: string + intro: string + statAsset: string + statImpact: string + statWindow: string + evidenceLabel: string + footer: string + footerMethodology: string + footerGlossary: string + cases: CaseStudy[] +} + +function getCopy(locale: string): CaseStudiesCopy { + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + + if (isZh) { + return { + title: '案例研究 - Trump Alpha', + description: + '整理 Trump Truth Social 帖子如何影响比特币与加密市场的公开案例,包括战略储备公告、链上大额抢跑交易、风险偏好切换以及 KOL 言行偏离样本。', + keywords: [ + 'Trump 加密市场影响', + 'Truth Social 比特币', + 'Trump 帖子行情', + '加密市场案例研究', + 'KOL 言行偏离案例', + 'BTC 底部案例', + ], + ogTitle: 'Case Studies | Trump Alpha', + ogDescription: + '记录过往真正推动市场的帖子、仓位和链上证据,解释这些信号为什么值得跟踪。', + heroTitle: '案例研究', + heroSubtitle: '把历史上真正推动市场的帖子、仓位和价格反应摊开来看', + intro: + '这不是收益展示页,而是证据页。每个案例都尽量回答三个问题:发生了什么、市场怎么动、为什么这件事支持某个信号引擎的存在。', + statAsset: '资产', + statImpact: '价格影响', + statWindow: '观察窗口', + evidenceLabel: '证据', + footer: '这些案例用于说明信号背后的历史依据,不构成前瞻性收益承诺。', + footerMethodology: '方法论', + footerGlossary: '术语表', + cases: [ + { + id: 'strategic-reserve-2025', + date: '2025 年 3 月 2 日', + source: 'Trump Truth Social', + title: '美国“战略加密储备”表态', + summary: 'Trump 在 Truth Social 上确认美国战略加密储备将涵盖 BTC、ETH、SOL、XRP 和 ADA。', + signal: 'LONG', + asset: 'BTC / ETH', + priceImpact: 'BTC +8.2%,ETH +10%+', + impactWindow: '24 小时', + detail: + '这条帖子之所以重要,不只是因为点名了多个资产,而是因为它把“政府政策支持”从模糊口风变成了明确承诺。Trump Alpha 的语义引擎会把这种带有具体资产清单和国家层面措辞的内容识别为高置信度 LONG 事件。', + evidence: + 'CNBC 和 Al Jazeera 均报道了该消息后的市场反应。链上和交易所数据也出现了明显的动量资金流入。', + externalUrl: + 'https://www.cnbc.com/2025/03/02/trump-announces-strategic-crypto-reserve-including-bitcoin-solana-xrp-and-more.html', + }, + { + id: 'whale-6m-perp', + date: '2025 年 3 月', + source: '链上 / Truth Social', + title: '战略储备帖子前后出现 680 万美元抢跑收益', + summary: + '一只匿名地址在战略储备帖子发布前后快速建立 50 倍 BTC/ETH 杠杆仓位,并在当天平仓,获利约 680 万美元。', + signal: 'LONG', + asset: 'BTC', + priceImpact: '+680 万美元', + impactWindow: '当日', + detail: + '这个案例说明了两件事:第一,Trump 帖子确实能形成足够大的短线价格冲击;第二,在这种事件中,反应速度本身就是边际优势。无论这笔交易是抢跑还是极快的自动化反应,它都证明了“帖子 -> 交易 -> 盈亏”链条是真实存在的。', + evidence: + 'Raw Story / NewsBreak 对这笔交易做了记录,TIME 也提到相关监管层面的质疑与讨论。', + externalUrl: + 'https://www.newsbreak.com/raw-story-2096750/4286876592634-that-s-the-maga-playbook-crypto-trade-made-moments-before-trump-post-raises-eyebrows', + }, + { + id: 'tariff-china-2025', + date: '2025 年 4 月', + source: 'Trump Truth Social', + title: '关税升级帖子引发风险偏好下移', + summary: 'Trump 发文谈及对中国加征大规模关税,这类表态通常被市场解读为短线 risk-off。', + signal: 'SHORT', + asset: 'BTC', + priceImpact: 'BTC -4.1%', + impactWindow: '4 小时', + detail: + '并不是每条 Trump 帖子都偏利多。带有贸易摩擦、关税升级、监管收紧或避险语义的内容,往往会压缩风险资产偏好。这个案例支持了信号系统中 SHORT 方向存在的必要性。', + evidence: + 'CoinDesk 曾整理过多次 Trump 发言影响 BTC 的案例,这次关税语境下的风险偏好回落与其历史模式一致。', + externalUrl: + 'https://www.coindesk.com/markets/2026/04/20/five-times-president-trump-made-a-statement-that-moved-bitcoin-and-why-it-might-happen-again-this-week', + }, + { + id: 'kol-divergence-example', + date: '示意型复合案例', + source: 'KOL 文章 + 链上地址', + title: '公开看多 ETH,但钱包在减仓', + summary: + '某位被追踪的 KOL 发布看多 ETH 的长文,随后 5 天内其钱包减少约 18 万美元 ETH 仓位,构成典型言行偏离。', + signal: 'DIVERGENCE', + asset: 'ETH', + priceImpact: 'N/A', + impactWindow: '±7 天', + detail: + '这个案例不是在讲单条帖子多会拉盘,而是在讲“公开叙事”和“真实仓位”可能不是一回事。对于研究型用户,这类偏离通常比单纯看多或看空更有信息增量。', + evidence: + '言行偏离的判定逻辑来自平台内部交叉信号系统,方法在 Methodology 页面有完整说明。', + }, + { + id: 'btc-bottom-nov-2022', + date: '2022 年 11 月', + source: '历史 BTC 周期底部参考', + title: 'FTX 崩盘后的 1.55 万美元区域', + summary: + 'FTX 事件后 BTC 跌入约 1.55 万美元区域,随后在 18 个月内回升至 6.9 万美元附近。这是 BTC 周期底部系统试图捕捉的环境类型。', + signal: 'LONG', + asset: 'BTC', + priceImpact: '+345%', + impactWindow: '18 个月', + detail: + '当前线上 BTC 系统并不是去回放旧版链上触发器,而是用 AHR999、200 周均线和 Pi Cycle Bottom 去逼近同一种市场结构:深度杀估值、长期趋势支撑和长期情绪出清同时出现。', + evidence: + '公开历史行情和长期图表都把 FTX 洗盘区间视为该轮熊市的底部核心区域。', + }, + ], + } + } + + return { + title: 'Case Studies — Documented Trump Posts That Moved Crypto', + description: + 'Documented cases where Trump Truth Social posts, KOL positioning, or macro-bottom conditions moved Bitcoin and crypto markets. Includes price impact, timing, and evidence.', + keywords: [ + 'Trump crypto market impact', + 'Trump Truth Social Bitcoin', + 'crypto market moving events', + 'Trump BTC trade case study', + 'KOL divergence example', + 'Bitcoin macro bottom case study', + ], + ogTitle: 'Case Studies | Trump Alpha', + ogDescription: + 'Historical cases that show why the Trump, BTC, and KOL engines exist: posts, wallet behavior, price reaction, and evidence.', + heroTitle: 'Case Studies', + heroSubtitle: 'Real posts, real positioning, and real market reactions worth studying', + intro: + 'This is not a performance-claims page. It is an evidence page. Each case is here to answer three questions: what happened, how the market reacted, and why that event supports the existence of a specific signal engine.', + statAsset: 'Asset', + statImpact: 'Price impact', + statWindow: 'Window', + evidenceLabel: 'Evidence', + footer: 'These cases illustrate the historical basis of the signal stack. They are not forward-looking return promises.', + footerMethodology: 'Methodology', + footerGlossary: 'Glossary', + cases: [ + { + id: 'strategic-reserve-2025', + date: 'March 2, 2025', + source: 'Trump Truth Social', + title: 'Strategic Crypto Reserve announcement', + summary: 'Trump confirmed a U.S. Strategic Crypto Reserve including BTC, ETH, SOL, XRP, and ADA.', + signal: 'LONG', + asset: 'BTC / ETH', + priceImpact: 'BTC +8.2%, ETH +10%+', + impactWindow: '24 hours', + detail: + 'This case mattered because the post moved from vague political tone into explicit asset-level policy language. A signal engine should classify that as a high-conviction LONG event, because it names specific assets and ties them to a state-level commitment.', + evidence: + 'CNBC and Al Jazeera both documented the market response, and exchange plus on-chain data showed clear momentum-driven inflows after the post.', + externalUrl: + 'https://www.cnbc.com/2025/03/02/trump-announces-strategic-crypto-reserve-including-bitcoin-solana-xrp-and-more.html', + }, + { + id: 'whale-6m-perp', + date: 'March 2025', + source: 'On-chain / Truth Social', + title: '$6.8M pre-positioning profit around the reserve post', + summary: + 'An anonymous address built a large leveraged BTC/ETH position around the reserve announcement and reportedly closed it the same day for roughly $6.8M profit.', + signal: 'LONG', + asset: 'BTC', + priceImpact: '+$6.8M', + impactWindow: 'Same day', + detail: + 'This case highlights two facts at once. First, Trump-linked posts can create enough directional force to matter. Second, speed itself is edge in event-driven trading. Whether this was advance positioning or ultra-fast automation, it validates the post-to-trade-to-PnL chain.', + evidence: + 'The trade was documented by Raw Story / NewsBreak, and TIME later referenced the political and regulatory scrutiny around the episode.', + externalUrl: + 'https://www.newsbreak.com/raw-story-2096750/4286876592634-that-s-the-maga-playbook-crypto-trade-made-moments-before-trump-post-raises-eyebrows', + }, + { + id: 'tariff-china-2025', + date: 'April 2025', + source: 'Trump Truth Social', + title: 'Tariff escalation post triggered a risk-off move', + summary: 'A tariff-escalation post created a short-term macro risk-off setup rather than a bullish crypto reaction.', + signal: 'SHORT', + asset: 'BTC', + priceImpact: 'BTC -4.1%', + impactWindow: '4 hours', + detail: + 'Not every Trump post is bullish for crypto. Posts framed around trade conflict, tariff escalation, tighter regulation, or macro uncertainty can compress risk appetite quickly. This case supports the need for a real SHORT branch in the signal engine.', + evidence: + 'CoinDesk has documented multiple instances of Trump statements moving BTC. This post fits that broader historical pattern of immediate sentiment repricing.', + externalUrl: + 'https://www.coindesk.com/markets/2026/04/20/five-times-president-trump-made-a-statement-that-moved-bitcoin-and-why-it-might-happen-again-this-week', + }, + { + id: 'kol-divergence-example', + date: 'Illustrative composite', + source: 'KOL post + on-chain wallet', + title: 'Publicly bullish on ETH, privately reducing ETH', + summary: + 'A tracked KOL published a bullish ETH thesis, then reduced roughly $180k of ETH exposure within five days. That is classic talks-vs-trades divergence.', + signal: 'DIVERGENCE', + asset: 'ETH', + priceImpact: 'N/A', + impactWindow: '±7 days', + detail: + 'This example is not about a single post moving price. It is about the information gap between public narrative and real positioning. For research-heavy users, that gap is often more valuable than a simple bullish or bearish statement.', + evidence: + 'The divergence classification comes from the platform’s cross-signal logic, which is documented in full on the Methodology page.', + }, + { + id: 'btc-bottom-nov-2022', + date: 'November 2022', + source: 'Historical BTC macro-bottom reference', + title: 'The $15.5k FTX washout zone', + summary: + 'After the FTX collapse, BTC entered the $15.5k region and later recovered toward $69k within roughly 18 months. This is the kind of regime the BTC macro-bottom engine is built to identify.', + signal: 'LONG', + asset: 'BTC', + priceImpact: '+345%', + impactWindow: '18 months', + detail: + 'The current live BTC engine is not trying to replay an old premium-data model. It is trying to locate the same market structure with AHR999, the 200-week moving average, and Pi Cycle Bottom: deep value, long-cycle support, and emotional washout lining up together.', + evidence: + 'Public price archives and long-range market charts consistently mark the FTX washout zone as the core bottom region of that bear market.', + }, + ], + } +} + +export function generateMetadata({ + params: { locale }, +}: { + params: { locale: string } +}): Metadata { + const copy = getCopy(locale) + + return { + title: copy.title, + description: copy.description, + keywords: copy.keywords, + openGraph: { + title: copy.ogTitle, + description: copy.ogDescription, + }, + alternates: { + canonical: `${siteUrl}/${locale}/case-studies`, + languages: { + en: `${siteUrl}/en/case-studies`, + 'x-default': `${siteUrl}/en/case-studies`, + }, + }, + } +} + +const SIGNAL_COLOR: Record = { + LONG: '#22c55e', + SHORT: '#ef4444', + DIVERGENCE: '#f5a524', +} + +export default function CaseStudiesPage({ params: { locale } }: { params: { locale: string } }) { + const copy = getCopy(locale) + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + + return ( +
+
+
+

{copy.heroTitle}

+

{copy.heroSubtitle}

+
+
+ +

+ {copy.intro} +

+ +
+ {copy.cases.map((c) => ( +
+
+ + {c.signal} + + + {c.date} + + + {c.source} + +
+ +

+ {c.title} +

+ +

+ {c.summary} +

+ +
+ {[ + { k: copy.statAsset, v: c.asset }, + { k: copy.statImpact, v: c.priceImpact }, + { k: copy.statWindow, v: c.impactWindow }, + ].map(({ k, v }, i) => ( +
+
{k}
+
{v}
+
+ ))} +
+ +

+ {c.detail} +

+ +

+ {copy.evidenceLabel}: {c.evidence} + {c.externalUrl && ( + <> {' '} + + {isZh ? '来源 ↗' : 'Source ↗'} + + + )} +

+
+ ))} +
+ +
+

+ {copy.footer}{' '} + {isZh ? '另见:' : 'See also:'}{' '} + {copy.footerMethodology} + {' '}·{' '} + {copy.footerGlossary}. +

+
+
+ ) +} diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx index ef425f2..16c941a 100644 --- a/app/[locale]/contact/page.tsx +++ b/app/[locale]/contact/page.tsx @@ -1,10 +1,15 @@ -export default function ContactPage() { +import { getLocale } from 'next-intl/server' + +export default async function ContactPage() { + const locale = await getLocale() + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + return (
-

Contact Us

-

Questions, feedback, or support requests.

+

{isZh ? '联系我们' : 'Contact Us'}

+

{isZh ? '问题、反馈或支持请求都可以从这里发来。' : 'Questions, feedback, or support requests.'}

@@ -15,30 +20,30 @@ export default function ContactPage() { encType="text/plain" >
- - + +
- +