feat(macro-vibes): rename BTC Signal → Macro Vibes; add MacroPanel UI

Module rename across page H1, navbar tab, URL (/en/btc → /en/macro),
all metadata/JSON-LD, sitemap, llms.txt, opengraph image, and SystemControl
copy. Old /btc route fully removed.

New components/btc/MacroPanel.tsx polls /api/macro/snapshot and lays out
the 8 indicators in four sections (Valuation / Bottom trigger reference /
Market structure / Sentiment & flows / Positioning) with tone-coloured
values, current-band threshold chips, and a single CoinGlass / source
chart link per card. Composite -100..+100 needle pulses on score change.

Also fixes the pinned bottom-reversal alert on the homepage, which still
linked to the now-404 /en/btc — now routes to /en/macro.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-26 01:05:18 +08:00
parent d72323b1c6
commit f34ae9eb00
33 changed files with 1810 additions and 228 deletions
+9 -7
View File
@@ -13,6 +13,7 @@ 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'
import PageHint from '@/components/ui/PageHint'
interface Props {
initialPosts: TrumpPost[]
@@ -265,13 +266,14 @@ export default function DashboardClient({ initialPosts }: Props) {
<div className="page-head">
<div>
<h1 className="page-title">{isZh ? '信号总览' : 'Signal monitor'}</h1>
<p className="page-sub">
{`${actionablePosts} actionable signals · Auto-trader ${botReadiness === 'ready' ? 'ready' : hlApiKeySet ? 'saved' : isSubscribed ? 'subscribed' : 'standby'} · ${hasPriceData ? 'Live market context' : 'Waiting for market data'}`}
</p>
<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.
</PageHint>
</div>
<div className="row gap-s">
<span className="chip"><span className="live-dot" />Live feed</span>
<span className="chip">{`${totalPosts} posts tracked`}</span>
</div>
</div>
@@ -279,7 +281,7 @@ export default function DashboardClient({ initialPosts }: Props) {
signal. Always sits ABOVE everything so it's never missed. */}
{btcReversalAlert && (
<a
href={`/${locale}/btc`}
href={`/${locale}/macro`}
style={{
display: 'block', textDecoration: 'none',
border: '1px solid color-mix(in oklab, var(--up) 45%, var(--line))',
@@ -290,7 +292,7 @@ export default function DashboardClient({ initialPosts }: Props) {
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<span style={{ fontSize: 12, fontWeight: 800, letterSpacing: '.04em',
color: 'var(--up)', textTransform: 'uppercase' }}>
BTC bottom-reversal signal
Macro Vibes · Bottom trigger
</span>
<span style={{ fontSize: 11, fontWeight: 700, padding: '2px 8px',
borderRadius: 999, background: 'var(--up)', color: '#fff' }}>
@@ -301,7 +303,7 @@ export default function DashboardClient({ initialPosts }: Props) {
</span>
<span style={{ marginLeft: 'auto', fontSize: 12, fontWeight: 700,
color: 'var(--up)' }}>
Open BTC signal
Open Macro Vibes
</span>
</div>
<div style={{ fontSize: 13, color: 'var(--ink-2)', marginTop: 6,
+23 -9
View File
@@ -7,6 +7,8 @@ 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'
import PageHint from '@/components/ui/PageHint'
import InfoTip from '@/components/ui/InfoTip'
type Period = '7d' | '30d' | '90d' | 'All'
@@ -115,12 +117,18 @@ export default function AnalyticsPageClient() {
: 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 },
{ k: isZh ? '最大回撤' : 'Max drawdown', v: filteredTrades.length ? maxDrawdown.toFixed(1) + '%' : '—', sub: isZh ? '从高点到低点的最大跌幅' : 'Worst peak-to-trough', down: true,
tip: 'Largest equity drop from a previous high. Tells you the worst losing streak you would have lived through.' },
{ k: isZh ? '总交易数' : 'Total trades', v: String(filteredTrades.length || '—'), sub: isZh ? '已平仓机器人交易' : 'Closed bot executions',
tip: 'How many closed positions in this window. Open trades not counted.' },
{ k: isZh ? '平均持仓时间' : 'Avg hold time', v: filteredTrades.length ? fmtHold(Math.round(avgHold)) : '—', sub: isZh ? '单笔计算' : 'Per trade',
tip: 'Entry → exit duration averaged across every closed trade.' },
{ k: isZh ? '平均单笔盈亏' : 'Avg trade P&L', v: fmtMoney(avgTrade), sub: isZh ? '每笔交易均值' : 'Mean per trade', up: avgTrade > 0,
tip: 'Total P&L ÷ number of trades. Positive = the strategy has edge per trade.' },
{ k: isZh ? '最佳单笔' : 'Best trade', v: fmtMoney(bestTrade), sub: isZh ? '单笔最大盈利' : 'Largest single win', up: true,
tip: 'Biggest realized gain on one trade in this window.' },
{ k: isZh ? '最差单笔' : 'Worst trade', v: fmtMoney(worstTrade), sub: isZh ? '单笔最大亏损' : 'Largest single loss', down: true,
tip: 'Biggest realized loss on one trade. Should be bounded by your stop-loss setting.' },
]
return (
@@ -128,7 +136,10 @@ export default function AnalyticsPageClient() {
<div className="page-head">
<div>
<h1 className="page-title">{isZh ? '分析面板' : 'Analytics'}</h1>
<p className="page-sub">{isZh ? `查看 ${period} 窗口内的信号与交易表现。` : `Deep dive into ${period} of signals and trades.`}</p>
<PageHint>
Did the bot actually make money? Win rate, drawdown, average trade,
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 => (
@@ -154,8 +165,11 @@ export default function AnalyticsPageClient() {
<div className="metric-grid" style={{ marginBottom: 20 }}>
{metrics.map(m => (
<div key={m.k} className="card" style={{ padding: 20 }}>
<div className="tiny" style={{ marginBottom: 8 }}>{m.k}</div>
<div key={m.k} className="card" style={{ padding: 20, overflow: 'visible' }}>
<div className="tiny" style={{ marginBottom: 8, display: 'flex', alignItems: 'center' }}>
{m.k}
{m.tip && <InfoTip text={m.tip} placement="top" width={240} />}
</div>
<div className={`mono ${m.up ? 'delta up' : m.down ? 'delta down' : ''}`} style={{ fontSize: 24, fontWeight: 500, letterSpacing: '-0.01em' }}>{m.v}</div>
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4 }}>{m.sub}</div>
</div>
+13 -18
View File
@@ -5,6 +5,14 @@ import { useLocale } from 'next-intl'
import type { TrumpPost } from '@/types'
import { getPosts } from '@/lib/api'
import PostRow from '@/components/dashboard/PostCards'
import PageHint from '@/components/ui/PageHint'
const LIVE_SOURCES = new Set([
'truth',
'btc_bottom_reversal',
'funding_reversal',
'kol_divergence',
])
/**
* Archive — legacy / test signals (rsi_reversal, sma_reclaim, breakout,
@@ -28,12 +36,6 @@ export default function ArchivePage() {
// 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],
@@ -53,18 +55,11 @@ export default function ArchivePage() {
<div className="page-head">
<div>
<h1 className="page-title">Archive</h1>
<p className="page-sub">{isZh ? '历史 / 测试数据,不属于实时系统。' : 'Legacy / test data — NOT a live system'}</p>
</div>
</div>
<div style={{
background: 'var(--bg-sunk)', borderRadius: 10, padding: '12px 16px',
marginBottom: 16, borderLeft: '4px solid var(--ink-4)',
}}>
<div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.5 }}>
{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.'}
<PageHint count={`${archivePosts.length} legacy posts`}>
Historical fires from old scanner experiments
(rsi_reversal, sma_reclaim, breakout, test/phase1). Kept only for
inspection the bot doesn't act on these any more.
</PageHint>
</div>
</div>
-71
View File
@@ -1,71 +0,0 @@
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<Metadata> {
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 (
<BtcPageClient
initialPosts={posts}
initialFundingSnapshot={fundingSnapshot}
/>
)
}
+7 -5
View File
@@ -271,11 +271,12 @@ function getCopy(locale: string): CaseStudiesCopy {
}
}
export function generateMetadata({
params: { locale },
export async function generateMetadata({
params,
}: {
params: { locale: string }
}): Metadata {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const copy = getCopy(locale)
return {
@@ -302,7 +303,8 @@ const SIGNAL_COLOR: Record<string, string> = {
DIVERGENCE: '#f5a524',
}
export default function CaseStudiesPage({ params: { locale } }: { params: { locale: string } }) {
export default async function CaseStudiesPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const copy = getCopy(locale)
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
+760 -2
View File
@@ -48,8 +48,8 @@
--shadow-2: 0 4px 16px rgba(20, 18, 14, 0.06), 0 1px 2px rgba(20, 18, 14, 0.03);
--shadow-3: 0 12px 40px rgba(20, 18, 14, 0.08), 0 2px 4px rgba(20, 18, 14, 0.04);
--sans: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
--mono: var(--font-geist-mono), ui-monospace, 'SF Mono', Menlo, monospace;
--sans: 'Geist', ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
--mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;
}
html[data-theme="dark"] {
@@ -331,6 +331,647 @@ html[data-theme="dark"] .asset-switch button.on { background: var(--surface-2);
margin-bottom: 0;
}
/* PageHint — the prominent "what is this page" tagline directly under
.page-title. Stronger contrast + bigger than .page-sub so it actually
gets read (page-sub was so faint it functioned as decoration).
Use via <PageHint>One sentence about the page.</PageHint>. */
.page-hint {
color: var(--ink-2);
font-size: 16px;
line-height: 1.5;
margin: 10px 0 0;
max-width: 64ch;
}
.page-hint-count {
color: var(--ink-4);
font-size: 13px;
margin-left: 10px;
padding-left: 10px;
border-left: 1px solid var(--line);
}
/* SectionHint — always-visible explanation block. Pairs with a section/tab
to tell the user "what does this view actually do" in 13 sentences.
More detailed than PageHint, less detail-y than an InfoTip. Spans the
full content width on purpose — narrow boxes inside a wide page looked
like accidentally-floating callouts. */
.section-hint {
background: var(--bg-sunk);
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 14px;
color: var(--ink-2);
font-size: 14px;
line-height: 1.55;
}
.section-hint strong {
color: var(--ink);
font-weight: 600;
}
/* InfoTip — small "?" pill that reveals a one-line tooltip on hover/focus.
No JS, pure CSS. Pattern mimics the (i) dots in Linear/Hyperliquid:
subtle until needed, never blocks the underlying content. */
.infotip {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
margin-left: 6px;
cursor: help;
outline: none;
/* The icon should sit on the text baseline without nudging line-height. */
line-height: 1;
}
.infotip-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--bg-sunk);
color: var(--ink-3);
font-size: 10px;
font-weight: 700;
border: 1px solid var(--line);
transition: background 0.12s, color 0.12s, border-color 0.12s;
user-select: none;
}
.infotip:hover .infotip-icon,
.infotip:focus-visible .infotip-icon {
background: var(--ink);
color: var(--bg);
border-color: var(--ink);
}
.infotip-bubble {
position: absolute;
z-index: 50;
width: var(--tip-w, 240px);
padding: 8px 10px;
/* Always-dark bubble — common pattern (Linear/Stripe/GitHub) — but needs
a visible border in dark mode so it doesn't blend into the page bg. */
background: #0f0d0a;
color: #f5f2ea;
border: 1px solid transparent;
border-radius: 6px;
font-size: 12px;
font-weight: 400;
line-height: 1.5;
letter-spacing: 0;
text-transform: none;
text-align: left;
box-shadow: 0 6px 24px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.2);
pointer-events: none;
opacity: 0;
transform: translateY(2px);
transition: opacity 0.12s, transform 0.12s;
white-space: normal;
}
/* Dark mode: page bg is ~oklch(15%), bubble is #0f0d0a — visually identical.
Lighten the bubble slightly and add a 1px ring so the shape stays defined. */
html[data-theme="dark"] .infotip-bubble {
background: oklch(28% 0.008 85);
border-color: oklch(38% 0.01 85);
box-shadow: 0 6px 24px rgba(0,0,0,0.6), 0 1px 2px rgba(0,0,0,0.4);
}
/* MacroPanel cell — subtle hover so the grid feels alive without becoming
a giant heat-map of click affordances. Just a tiny background lift. */
.macro-cell:hover {
background: var(--bg-sunk) !important;
}
.macro-panel {
padding: 0;
margin-bottom: 12px;
overflow: hidden;
}
.macro-panel-head {
padding: 20px 22px 18px;
border-bottom: 1px solid var(--line);
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.macro-panel-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 700;
}
.macro-panel-dot {
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--up);
display: inline-block;
}
.macro-panel-subtitle {
margin-top: 6px;
font-size: 12px;
line-height: 1.45;
color: var(--ink-3);
}
.macro-panel-meta {
text-align: right;
font-size: 11px;
line-height: 1.45;
color: var(--ink-4);
flex-shrink: 0;
}
.macro-section {
border-top: 1px solid var(--line);
}
.macro-section-title {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 22px 12px;
/* Was 10px / ink-3 / weight 700 — too quiet, users couldn't see the
section structure. Bumped to 12px / ink-2 / weight 800. Still uppercase
+ tracked so it reads as a label, not a heading that competes with the
metric values. */
font-size: 12px;
color: var(--ink-2);
letter-spacing: 0.10em;
text-transform: uppercase;
font-weight: 800;
background: var(--bg-sunk);
border-bottom: 1px solid var(--line);
}
/* Tiny dot anchor on the left — visual signal that "this is a section
header" without using a heavy colored bar. */
.macro-section-title::before {
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--ink-3);
flex-shrink: 0;
}
.macro-grid {
display: grid;
gap: 14px;
padding: 14px;
}
.macro-grid.one {
grid-template-columns: minmax(0, 1fr);
}
.macro-grid.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.macro-metric-card,
.macro-guide-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 12px;
padding: 18px;
min-width: 0;
min-height: 248px;
display: flex;
flex-direction: column;
}
.macro-metric-card {
box-shadow: var(--shadow-1);
}
.macro-metric-card.tone-up {
border-color: color-mix(in oklab, var(--up) 28%, var(--line));
}
.macro-metric-card.tone-down {
border-color: color-mix(in oklab, var(--down) 28%, var(--line));
}
/* Stat card layout — label STACKED ABOVE the big number, not side-by-side.
Previous side-by-side layout pushed the title to the far left and the
value to the far right with too much air between them on wide screens.
The eye now reads top→bottom: label → value → summary → chips → button. */
.macro-metric-head {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.macro-metric-title {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
/* Slightly larger + bolder so "AHR999" doesn't disappear next to the
big number below. Treated as a label, not a heading. */
font-size: 15px;
font-weight: 650;
color: var(--ink-2);
}
.macro-rank {
font-size: 11px;
color: var(--ink-4);
letter-spacing: 0.08em;
font-weight: 700;
flex-shrink: 0;
}
.macro-metric-value {
/* Same size as before but sits directly under the label now, so the
eye-distance between "AHR999" and "0.4663" is ~6px instead of the
full card width. */
font-size: 34px;
line-height: 1;
letter-spacing: -0.03em;
font-weight: 800;
color: var(--ink);
text-align: left;
flex-shrink: 0;
}
.macro-guide-title {
font-size: 20px;
line-height: 1.15;
letter-spacing: -0.02em;
font-weight: 700;
color: var(--ink);
}
.macro-summary {
margin-top: 12px;
font-size: 14px;
line-height: 1.55;
color: var(--ink-2);
min-height: 66px;
}
.macro-thresholds {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
/* THRESHOLD CHIPS — "you are here" treatment
─────────────────────────────────────────────────────────
Design goal: the chip that the CURRENT value falls into must be the
single most-eye-catching element in the cell. Everything else fades back.
Two states:
base (inactive) — flat, outlined, muted gray. Tone is suppressed
so a "down (red)" inactive chip doesn't visually
outshout an active "neutral" chip. Stripping
tone here is intentional.
.active — filled pill with the tone colour, larger, raised
on shadow, scale 1.05. The eye can't miss it.
Hover the row → all chips bump to 0.8 opacity so you can read the
other bands' rules without losing the active one. */
.macro-threshold-chip {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 5px 12px;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
font-size: 11.5px;
line-height: 1.35;
color: var(--ink-4);
font-weight: 500;
opacity: 0.45;
transition: opacity 160ms, transform 180ms cubic-bezier(.2,.8,.2,1),
box-shadow 180ms, background 180ms, color 180ms;
}
.macro-thresholds:hover .macro-threshold-chip { opacity: 0.8; }
/* When INACTIVE, suppress tone colors. They'd compete with the active chip
for attention. Tone returns the moment the chip becomes .active below. */
.macro-threshold-chip:not(.active) {
color: var(--ink-4) !important;
background: transparent !important;
border-color: var(--line) !important;
}
/* ACTIVE — the band the current value falls into. */
.macro-threshold-chip.active {
opacity: 1;
font-weight: 700;
font-size: 12px;
padding: 6px 14px;
transform: scale(1.06);
letter-spacing: 0.01em;
}
.macro-threshold-chip.up.active {
background: var(--up);
color: #fff;
border-color: var(--up);
box-shadow:
0 6px 18px color-mix(in oklab, var(--up) 35%, transparent),
0 1px 0 rgba(255,255,255,0.25) inset;
}
.macro-threshold-chip.down.active {
background: var(--down);
color: #fff;
border-color: var(--down);
box-shadow:
0 6px 18px color-mix(in oklab, var(--down) 35%, transparent),
0 1px 0 rgba(255,255,255,0.25) inset;
}
.macro-threshold-chip.neutral.active {
/* Amber = "caution" signal — between safe and danger. Lets the user
know "you are here, no extreme yet" without the alarming red. */
background: var(--amber);
color: oklch(20% 0.04 75);
border-color: var(--amber);
box-shadow:
0 6px 18px color-mix(in oklab, var(--amber) 35%, transparent),
0 1px 0 rgba(255,255,255,0.4) inset;
}
/* Dark mode tweaks — the soft white-shimmer inset doesn't read on dark cards,
so swap to a subtle darker outline. */
html[data-theme="dark"] .macro-threshold-chip.up.active,
html[data-theme="dark"] .macro-threshold-chip.down.active,
html[data-theme="dark"] .macro-threshold-chip.neutral.active {
box-shadow:
0 8px 24px color-mix(in oklab, currentColor 30%, transparent),
0 0 0 1px rgba(0,0,0,0.25) inset;
}
/* Section variants — reference cards (rules-only, no live number) get a
muted treatment so users instantly see they're explanatory, not live.
We REPLACE the dot anchor with a 📐 emoji (overriding the .macro-section-
title::before above) and dim the text color. */
.macro-section.reference .macro-section-title {
color: var(--ink-3);
}
.macro-section.reference .macro-section-title::before {
content: '📐';
width: auto;
height: auto;
background: none;
border-radius: 0;
font-size: 13px;
line-height: 1;
filter: grayscale(0.3);
}
.macro-guide-card {
background: color-mix(in oklab, var(--bg-sunk) 60%, var(--surface));
border-style: dashed;
}
.macro-guide-card .macro-guide-title::before {
content: 'RULES ';
font-size: 9px;
font-weight: 800;
letter-spacing: 0.12em;
padding: 2px 6px;
border-radius: 4px;
background: color-mix(in oklab, var(--ink) 8%, transparent);
color: var(--ink-3);
margin-right: 8px;
vertical-align: middle;
}
/* Action row — single small button, aligned to the bottom-right of the card.
Was previously a full-width dual-button group; pinning a compact button to
the corner keeps the card's eye-flow on the value + thresholds, with the
"go look at history" affordance present but quiet. */
.macro-actions {
display: flex;
justify-content: flex-end;
margin-top: auto;
padding-top: 14px;
}
.macro-action-btn {
display: inline-flex;
align-items: center;
gap: 6px;
/* Compact natural width — fits "Open chart CoinGlass ↗" comfortably */
padding: 7px 12px;
border-radius: 8px;
border: 1px solid var(--line);
background: color-mix(in oklab, var(--ink) 4%, var(--surface));
color: var(--ink);
text-decoration: none;
transition: background 120ms, border-color 120ms, transform 120ms, box-shadow 120ms;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.4);
}
.macro-action-btn:hover {
background: color-mix(in oklab, var(--ink) 7%, var(--surface));
border-color: color-mix(in oklab, var(--ink) 12%, var(--line));
transform: translateY(-1px);
box-shadow: var(--shadow-1);
}
.macro-action-btn.chart {
background: color-mix(in oklab, var(--amber) 10%, var(--surface));
border-color: color-mix(in oklab, var(--amber) 18%, var(--line));
}
/* The kicker ("OPEN CHART" uppercase eyebrow) used to sit ABOVE the label
in a 2-row layout. With the new compact button it lives inline next to
the label as a tiny prefix tag — same words, less vertical space. */
.macro-action-copy {
display: inline-flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.macro-action-kicker {
font-size: 9.5px;
line-height: 1;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-4);
font-weight: 700;
}
.macro-action-label {
font-size: 13px;
line-height: 1;
color: var(--ink);
font-weight: 600;
white-space: nowrap;
}
.macro-action-arrow {
font-size: 13px;
line-height: 1;
color: var(--ink-3);
flex-shrink: 0;
}
.macro-disclosure {
margin-top: 14px;
border-top: 1px dashed var(--line);
padding-top: 12px;
}
.macro-disclosure summary {
list-style: none;
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
}
.macro-disclosure summary::-webkit-details-marker {
display: none;
}
.macro-disclosure-body {
margin-top: 8px;
font-size: 12px;
line-height: 1.55;
color: var(--ink-3);
}
.macro-composite {
border-top: 1px solid var(--line);
padding: 20px 22px 22px;
background: var(--bg-sunk);
}
.macro-composite-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.macro-composite-label {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-3);
font-weight: 700;
}
.macro-composite-right {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.macro-composite-value {
font-size: 32px;
line-height: 0.95;
font-weight: 800;
letter-spacing: -0.03em;
}
.macro-regime-pill {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid var(--line);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
}
.macro-composite-track {
position: relative;
height: 12px;
border-radius: 999px;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.08);
}
.macro-composite-needle {
position: absolute;
top: -4px;
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid var(--ink);
background: var(--surface);
box-shadow: var(--shadow-2);
transition: left 0.4s cubic-bezier(.2,.8,.2,1);
/* React re-mounts this element via `key={composite_score}` whenever the
score changes, which re-triggers the pulse animation below — a brief
attention cue for users tracking how the macro tilt shifts day to day. */
animation: macro-needle-pulse 0.9s ease-out 1;
}
@keyframes macro-needle-pulse {
0% { transform: scale(0.85); box-shadow: 0 0 0 0 currentColor; }
40% { transform: scale(1.18); box-shadow: 0 0 0 8px color-mix(in oklab, currentColor 18%, transparent); }
100% { transform: scale(1); box-shadow: var(--shadow-2); }
}
.macro-composite-scale {
display: flex;
justify-content: space-between;
gap: 8px;
margin-top: 8px;
font-size: 10px;
letter-spacing: 0.04em;
color: var(--ink-4);
text-transform: uppercase;
}
.infotip:hover .infotip-bubble,
.infotip:focus-visible .infotip-bubble {
opacity: 1;
transform: translateY(0);
}
/* Placement variants (default = top) */
.infotip-top .infotip-bubble {
bottom: calc(100% + 8px);
left: 50%; transform: translate(-50%, 2px);
}
.infotip-top:hover .infotip-bubble,
.infotip-top:focus-visible .infotip-bubble {
transform: translate(-50%, 0);
}
.infotip-bottom .infotip-bubble {
top: calc(100% + 8px);
left: 50%; transform: translate(-50%, -2px);
}
.infotip-bottom:hover .infotip-bubble,
.infotip-bottom:focus-visible .infotip-bubble {
transform: translate(-50%, 0);
}
.infotip-left .infotip-bubble {
right: calc(100% + 8px);
top: 50%; transform: translate(2px, -50%);
}
.infotip-left:hover .infotip-bubble,
.infotip-left:focus-visible .infotip-bubble {
transform: translate(0, -50%);
}
.infotip-right .infotip-bubble {
left: calc(100% + 8px);
top: 50%; transform: translate(-2px, -50%);
}
.infotip-right:hover .infotip-bubble,
.infotip-right:focus-visible .infotip-bubble {
transform: translate(0, -50%);
}
/* ============================================================
Cards + primitives
============================================================ */
@@ -1273,6 +1914,116 @@ html[data-theme="dark"] .ai-reasoning-card {
grid-template-columns: 1fr;
}
.macro-panel-head,
.macro-composite-head {
flex-direction: column;
align-items: flex-start;
}
.macro-panel-meta {
text-align: left;
}
.macro-grid.two {
grid-template-columns: 1fr;
}
.macro-grid {
padding: 12px;
gap: 12px;
}
.macro-metric-card,
.macro-guide-card {
padding: 16px;
min-height: 0;
}
.macro-metric-head {
/* Desktop is now also flex-direction: column. Just override the gap
for mobile to keep label-to-value spacing comfortable on small fonts. */
gap: 8px;
}
.macro-metric-value {
font-size: 30px;
}
.macro-guide-title {
font-size: 18px;
}
.macro-summary {
font-size: 13px;
line-height: 1.5;
min-height: 0;
}
.macro-threshold-chip {
font-size: 11.5px;
/* Bump tap target on mobile — iOS HIG asks for ≥44pt, this is the
inline-flex bubble so we go a bit smaller but still comfortable. */
min-height: 36px;
padding: 7px 12px;
}
.macro-threshold-chip.active {
font-size: 12px;
padding: 8px 14px;
/* Stronger scale on mobile — small screens lose subtle differences. */
transform: scale(1.08);
}
.macro-action-btn {
/* Stays compact on mobile too — bottom-right corner pill, not full-width. */
padding: 8px 12px;
}
/* Composite scale labels — "BEAR / NEUTRAL / BULL" letters crowd at narrow
widths. Shorten them. */
.macro-composite-scale {
font-size: 9.5px;
letter-spacing: 0.02em;
}
/* InfoTip — boost tap area and clamp bubble inside viewport */
.infotip {
/* Pad outside the visible icon to enlarge the hit zone without making
the icon itself look bloated. */
padding: 8px;
margin: -8px -2px -8px 4px;
}
.infotip-icon {
width: 18px;
height: 18px;
font-size: 11px;
}
.infotip-bubble {
/* On small viewports a 280px-wide bubble can hang off the edge when
the tip sits near the right margin. Clamp to viewport width minus
a 16px breathing room each side. */
max-width: calc(100vw - 32px) !important;
}
/* Touch-only browsers fire :active on tap. Combined with :hover, this
ensures bubble shows on first tap (not the doubled-tap that some
CSS-hover-only tooltips require). */
.infotip:active .infotip-bubble {
opacity: 1;
transform: none;
}
.macro-action-label {
font-size: 12px;
white-space: normal;
}
.macro-composite {
padding: 18px 16px 20px;
}
.macro-composite-right {
justify-content: flex-start;
}
.row.between {
align-items: flex-start;
}
@@ -1325,6 +2076,13 @@ html[data-theme="dark"] .ai-reasoning-card {
.modal, .dialog { width: calc(100vw - 24px) !important; max-width: calc(100vw - 24px) !important; }
}
/* Ultra-narrow screens — drop the regime descriptive words on the composite
scale so it doesn't wrap. Keep just the numeric anchors. The .word spans
inside each label are set in MacroPanel.tsx so this hide works cleanly. */
@media (max-width: 420px) {
.macro-composite-scale .word { display: none; }
}
@media (max-width: 380px) {
.nav { padding: 8px 12px; }
.page { padding: 16px 12px 48px; }
+8 -6
View File
@@ -241,7 +241,7 @@ function getCopy(locale: string): GlossaryCopy {
abbr: 'Unspent Transaction Output',
category: 'Bitcoin term',
definition:
'UTXO is the fundamental accounting unit of the Bitcoin network. Trump Alphas live BTC signal no longer relies directly on UTXO-spend behavior, but the concept still matters for interpreting classic on-chain research.',
'UTXO is the fundamental accounting unit of the Bitcoin network. Trump Alphas live Macro Vibes module no longer relies directly on UTXO-spend behavior, but the concept still matters for interpreting classic on-chain research.',
},
{
term: 'Perpetual Future',
@@ -277,11 +277,12 @@ function getCopy(locale: string): GlossaryCopy {
}
}
export function generateMetadata({
params: { locale },
export async function generateMetadata({
params,
}: {
params: { locale: string }
}): Metadata {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const copy = getCopy(locale)
return {
@@ -319,7 +320,8 @@ const CATEGORY_COLOR: Record<string, string> = {
'技术术语': '#94a3b8',
}
export default function GlossaryPage({ params: { locale } }: { params: { locale: string } }) {
export default async function GlossaryPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const copy = getCopy(locale)
const categories = Array.from(new Set(copy.terms.map(t => t.category)))
+7 -7
View File
@@ -9,6 +9,7 @@ import type {
} from '@/types'
import { getKolPosts, getKolPost, getKolDigest, getKolChanges, getKolDivergence } from '@/lib/api'
import { swrFetch } from '@/lib/cache'
import PageHint from '@/components/ui/PageHint'
/**
* KOL feed — independent module. Lists analyzed posts from tracked KOLs
@@ -802,16 +803,15 @@ export default function KolPage({
<div className="page-head">
<div>
<h1 className="page-title">{isZh ? 'KOL 信号' : 'KOL Signals'}</h1>
<p className="page-sub">
{`Track long-form KOL theses and extracted asset calls · ${posts.length} posts`}
</p>
<PageHint count={`${posts.length} posts`}>
19 crypto KOLs' essays and podcasts ingested daily, AI extracts every
ticker call, then cross-checked against the same KOLs' on-chain
wallets. Catches "talks vs trades" when their wallet says the
opposite of their tweets.
</PageHint>
</div>
</div>
<div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.5, marginBottom: 16 }}>
This module ingests long-form KOL writing and show notes, extracts explicit asset views with AI, then cross-checks those views against wallet behavior. Open any row to inspect the original text and supporting quote.
</div>
<DigestWidget
isZh={isZh}
initialDigest={initialDigest}
@@ -7,6 +7,9 @@ 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'
import InfoTip from '@/components/ui/InfoTip'
import PageHint from '@/components/ui/PageHint'
import MacroPanel from '@/components/btc/MacroPanel'
/**
* System 2 BTC bottom-reversal. Its own dedicated page.
@@ -17,15 +20,15 @@ const SENTIMENTS = ['all', 'bullish', 'bearish', 'neutral'] as const
type SentimentFilter = (typeof SENTIMENTS)[number]
type BtcTab = 'bottom' | 'funding'
interface BtcSignalPageProps {
interface MacroVibesPageProps {
initialPosts?: TrumpPost[] | null
initialFundingSnapshot?: FundingSnapshot | null
}
export default function BtcSignalPage({
export default function MacroVibesPage({
initialPosts = null,
initialFundingSnapshot = null,
}: BtcSignalPageProps) {
}: MacroVibesPageProps) {
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<TrumpPost[]>(initialPosts ?? [])
@@ -70,24 +73,20 @@ export default function BtcSignalPage({
<div className="page">
<div className="page-head">
<div>
<h1 className="page-title">{isZh ? '② BTC 信号' : '② BTC Signal'}</h1>
<p className="page-sub">
{tab === 'bottom'
? `Macro cycle bottom · ${btcPosts.length} signals`
: `Funding-rate extreme reversal · ${btcPosts.length} signals`}
</p>
<h1 className="page-title">{isZh ? '宏观氛围' : 'Macro Vibes'}</h1>
{/* PageHint: brand tagline for the page same across both tabs so
the "what is this page" answer doesn't shift under the user when
they click between Bottom / Funding. Tab-specific notes live in
the section-hint card right below the tab bar. */}
<PageHint>
Macro vibes for crypto. Read what&apos;s about to happen before price prints it.
</PageHint>
</div>
<span className="chip"><span className="live-dot" />Live</span>
</div>
<SystemControl system="btc" />
{tab === 'bottom' && (
<div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.6, marginBottom: 12 }}>
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.
</div>
)}
<div className="nav-tabs" style={{
background: 'var(--bg-sunk)', marginTop: 16, marginBottom: 12,
}}>
@@ -107,20 +106,34 @@ export default function BtcSignalPage({
</button>
</div>
{/* Always-visible per-tab explanation. Was hidden behind a hover
tooltip on the tab button users couldn't tell what they were
about to look at until they explicitly hovered. */}
{tab === 'bottom' && (
<div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.5, marginBottom: 12 }}>
<>
Fires when <strong>2 of 3</strong> classic bottom signals agree:{' '}
<strong>AHR999&nbsp;&lt;&nbsp;0.45</strong> · <strong>price 200-week
MA</strong> · <strong>Pi Cycle Bottom</strong>. Long only, low frequency
(24 fires per cycle). Scans daily 00:45&nbsp;UTC. Designed to ride the
full cycle: holds up to <strong>18&nbsp;months</strong>, trailing-stop
exit, no take-profit. Keep leverage 2× amplification comes from
pyramiding, not from cranking leverage.
</>
<div className="section-hint">
Fires when <strong>2 of 3</strong> classic bottom signals agree:{' '}
<strong>AHR999&nbsp;&lt;&nbsp;0.45</strong> · <strong>price 200-week MA</strong>
{' '}· <strong>Pi Cycle Bottom</strong>. Long only, 24 fires per cycle.
Holds up to 18 months with a trailing-stop exit no take-profit.
</div>
)}
{/* Macro indicator panel only relevant on the Macro Bottom tab where
users are reasoning about the broader risk regime. The Funding tab
has its own live funding panel below. */}
{tab === 'bottom' && <MacroPanel />}
{tab === 'funding' && (
<div className="section-hint">
Mean-reversion against crowded perp positioning. When 30-day cumulative
funding crosses <strong>±3%</strong> AND the most recent cycles start
cooling off, the scanner bets against the side that's been paying for
weeks. Hourly check.
</div>
)}
{/* Old plain-text per-tab description removed the same content
now lives in the section-hint block above the tab content. */}
{tab === 'funding' && (
<FundingPanel
isZh={isZh}
@@ -262,16 +275,19 @@ function FundingPanel({
<StatBox
label="Latest cycle"
value={`${snap.latest_rate_pct?.toFixed(4)}%`}
hint="The most recent funding payment, in % per cycle. Positive = longs pay shorts."
/>
<StatBox
label="Last 24h avg"
value={`${snap.last_24h_avg_pct?.toFixed(4)}%`}
hint="Average of the last 24 hours of funding cycles. Smooths out one-off prints."
/>
<StatBox
label="30d cumulative"
value={`${cum >= 0 ? '+' : ''}${cum.toFixed(2)}%`}
color={cumColor}
sub={`extreme @ ±${thr}%`}
hint="Sum of every funding payment over 30 days. Crosses ±3% = one side has been paying for weeks — overcrowded."
/>
<StatBox
label="Signal"
@@ -281,6 +297,7 @@ function FundingPanel({
? '⏳ Watching'
: '✓ Quiet'}
color={snap.signal_fired ? 'var(--down)' : extreme ? 'var(--amber, #f59e0b)' : 'var(--up)'}
hint="Quiet = normal range. Watching = extreme funding detected, waiting for mean-revert + price confirm. Fired = trade triggered."
/>
</div>
@@ -310,16 +327,22 @@ function FundingPanel({
)
}
function StatBox({ label, value, color, sub }: {
label: string; value: string; color?: string; sub?: string
function StatBox({ label, value, color, sub, hint }: {
label: string; value: string; color?: string; sub?: string; hint?: string
}) {
return (
<div style={{
padding: 10, background: 'var(--bg-sunk)', borderRadius: 6,
border: '1px solid var(--line)',
// The InfoTip bubble overflows the stat box; let it escape.
overflow: 'visible',
}}>
<div style={{ fontSize: 10, color: 'var(--ink-4)', textTransform: 'uppercase',
letterSpacing: '.06em', marginBottom: 4 }}>{label}</div>
letterSpacing: '.06em', marginBottom: 4,
display: 'flex', alignItems: 'center' }}>
{label}
{hint && <InfoTip text={hint} placement="top" width={220} />}
</div>
<div style={{ fontSize: 16, fontWeight: 700, color: color || 'var(--ink)',
fontVariantNumeric: 'tabular-nums' }}>{value}</div>
{sub && <div style={{ fontSize: 10, color: 'var(--ink-4)', marginTop: 2 }}>{sub}</div>}
+76
View File
@@ -0,0 +1,76 @@
import { getFundingSnapshot, getPosts } from '@/lib/api'
import type { Metadata } from 'next'
import MacroVibesPageClient from './MacroVibesPageClient'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://trumpsignal.com'
export const revalidate = 30
interface MacroVibesPageProps {
params: Promise<{ locale: string }>
}
export async function generateMetadata({
params,
}: MacroVibesPageProps): Promise<Metadata> {
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 ? '宏观氛围 — 加密市场宏观读数' : 'Macro Vibes — read crypto macro before price prints',
description: isZh
? 'Trump Alpha 的宏观氛围面板:AHR999、Fear & Greed、BTC 主导率、ETH/BTC、稳定币供应、ETF 资金流、永续 OI、Altcoin Season Index 八大公开宏观指标合一,外加 BTC 周期底部 + 资金费率反转两条触发器。'
: 'Macro vibes for crypto. Eight public macro signals in one view — AHR999, Fear & Greed, BTC dominance, ETH/BTC, stablecoin supply, ETF flows, perp OI, Altcoin Season Index — plus BTC bottom and funding-rate triggers.',
keywords: isZh
? [
'宏观氛围',
'加密宏观',
'AHR999',
'Fear and Greed',
'BTC 主导率',
'ETH/BTC',
'ETF 资金流',
'稳定币供应',
'资金费率反转',
'Altcoin Season Index',
]
: [
'crypto macro',
'macro vibes',
'AHR999',
'Fear and Greed Index',
'BTC dominance',
'ETH/BTC ratio',
'stablecoin supply',
'Bitcoin ETF flow',
'funding rate reversal',
'Altcoin Season Index',
],
openGraph: {
title: isZh ? '宏观氛围 | Trump Alpha' : 'Macro Vibes | Trump Alpha',
description: isZh
? '加密宏观一屏看完:八大宏观指标 + BTC 周期底部 + 资金费率反转触发器。'
: 'Macro vibes for crypto. Read what is about to happen before price prints it.',
},
alternates: {
canonical: `${siteUrl}/${locale}/macro`,
languages: {
en: `${siteUrl}/en/macro`,
'x-default': `${siteUrl}/en/macro`,
},
},
}
}
export default async function MacroVibesPage() {
const [posts, fundingSnapshot] = await Promise.all([
getPosts(500, 1).catch(() => null),
getFundingSnapshot().catch(() => null),
])
return (
<MacroVibesPageClient
initialPosts={posts}
initialFundingSnapshot={fundingSnapshot}
/>
)
}
+7 -5
View File
@@ -367,11 +367,12 @@ SHORT_INTENT + SHORT_ACTION => ALIGNMENT`,
}
}
export function generateMetadata({
params: { locale },
export async function generateMetadata({
params,
}: {
params: { locale: string }
}): Metadata {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const copy = getCopy(locale)
return {
@@ -431,7 +432,8 @@ function Formula({ children }: { children: React.ReactNode }) {
)
}
export default function MethodologyPage({ params: { locale } }: { params: { locale: string } }) {
export default async function MethodologyPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const copy = getCopy(locale)
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
+2 -2
View File
@@ -18,8 +18,8 @@ export async function generateMetadata({ params }: OverviewPageProps): Promise<M
? 'Trump Alpha 信号总览'
: 'Trump Alpha Signal Monitor'
const description = isZh
? '实时查看 Trump Truth Social 信号、BTC 底部反转、KOL 观点和 talks-vs-trades 分歧,一页掌握核心加密信号。'
: 'Track Trump Truth Social signals, BTC bottom-reversal setups, KOL calls, and talks-vs-trades divergence in one live crypto dashboard.'
? '实时查看 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.'
const path = `${siteUrl}/${locale}`
return {
+7 -3
View File
@@ -4,11 +4,15 @@ import { redirect } from 'next/navigation'
* Legacy route. The old "Signals" hub was replaced by two top-level nav
* tabs (Trump / BTC). Anything still pointing here (old links, sitemap,
* bookmarks) lands on the Trump system page.
*
* NOTE: Next.js 15+ made `params` async. Without `await` we'd interpolate
* the still-pending Promise as `undefined` and ship users to /undefined/trump.
*/
export default function LegacySignalsRedirect({
export default async function LegacySignalsRedirect({
params,
}: {
params: { locale: string }
params: Promise<{ locale: string }>
}) {
redirect(`/${params.locale}/trump`)
const { locale } = await params
redirect(`/${locale}/trump`)
}
+2 -1
View File
@@ -1,7 +1,8 @@
import Link from 'next/link'
import { getLocale } from 'next-intl/server'
export default async function PrivacyPage({ params: { locale } }: { params: { locale: string } }) {
export default async function PrivacyPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const intlLocale = await getLocale()
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
+12 -1
View File
@@ -7,6 +7,7 @@ import { useLocale } from 'next-intl'
import { useAccount } from 'wagmi'
import BotConfigPanel from '@/components/trades/BotConfigPanel'
import TelegramCard from '@/components/telegram/TelegramCard'
import PageHint from '@/components/ui/PageHint'
/**
* Settings page — the home for all configuration.
@@ -30,7 +31,17 @@ export default function SettingsClient() {
const href = (path: string) => `/${locale}${path}`
return (
<div>
<div className="page">
<div className="page-head">
<div>
<h1 className="page-title">{isZh ? '设置' : 'Settings'}</h1>
<PageHint>
Everything that controls your account subscription, Hyperliquid
API key, bot risk parameters, and Telegram alerts.
</PageHint>
</div>
</div>
{/* Account card — quick "who am I logged in as" */}
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.06em',
+5 -1
View File
@@ -11,6 +11,7 @@ import { useDashboardStore } from '@/store/dashboard'
import { getCachedViewEnvelope, getOrCreateViewEnvelope } from '@/lib/signedRequest'
import TradeTable from '@/components/trades/TradeTable'
import OpenPositions from '@/components/positions/OpenPositions'
import PageHint from '@/components/ui/PageHint'
export default function TradesPageClient() {
const intlLocale = useLocale()
@@ -71,7 +72,10 @@ export default function TradesPageClient() {
<div className="page-head">
<div>
<h1 className="page-title">{isZh ? '交易执行' : 'Trades'}</h1>
<p className="page-sub">{isZh ? '当前持仓、执行历史和按信号来源拆分的盈亏。' : 'Open positions · execution history · P&L by source'}</p>
<PageHint>
What the bot actually did with your money currently-open positions
on top, closed-trade history with realized P&amp;L below.
</PageHint>
</div>
</div>
+19 -14
View File
@@ -7,6 +7,8 @@ import { getPosts } from '@/lib/api'
import { swrFetch } from '@/lib/cache'
import PostRow from '@/components/dashboard/PostCards'
import SystemControl from '@/components/signals/SystemControl'
import PageHint from '@/components/ui/PageHint'
import InfoTip from '@/components/ui/InfoTip'
/**
* System 1 — Trump (event-driven scalp). Its own dedicated page.
@@ -80,17 +82,14 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
<div className="page-head">
<div>
<h1 className="page-title">{isZh ? '① Trump 信号' : '① Trump Signal'}</h1>
<p className="page-sub">
{`Event-driven scalp · ${sigCounts.actionable} actionable · ${trumpPosts.length} posts`}
</p>
<PageHint count={`${sigCounts.actionable} actionable / ${trumpPosts.length} posts`}>
Watches Trump's Truth Social posts in real time, AI-scores each one,
and only fires a trade when the conviction is high enough to move price.
</PageHint>
</div>
<span className="chip"><span className="live-dot" />Live</span>
</div>
<div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.5, marginBottom: 12 }}>
This engine watches Trump Truth Social in real time, classifies each post, and only opens trades on high-confidence market-moving events. It is built for tight risk, short holds, and a 12-hour cooldown between entries.
</div>
<SystemControl system="trump" />
<div style={{
@@ -99,24 +98,30 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
}}>
<div className="nav-tabs" style={{ background: 'var(--bg-sunk)' }}>
{([
{ key: 'all' },
{ key: 'actionable' },
{ key: 'buy' },
{ key: 'short' },
{ key: 'all', hint: 'Every post the scraper has captured.' },
{ key: 'actionable', hint: 'Only posts the AI flagged as BUY or SHORT.' },
{ key: 'buy', hint: 'Posts where the AI verdict is long BTC.' },
{ key: 'short', hint: 'Posts where the AI verdict is short BTC.' },
] as const).map(f => (
<button
key={f.key}
className={`nav-tab ${sigFilter === f.key ? 'active' : ''}`}
onClick={() => setSigFilter(f.key)}
>
{f.key === 'actionable' && !isZh ? '🔥 ' : ''}
{f.key === 'actionable' && isZh ? '🔥 ' : ''}
{f.key === 'actionable' ? '🔥 ' : ''}
{signalLabels[f.key]}
<span style={{ color: 'var(--ink-4)', marginLeft: 4 }}>{sigCounts[f.key]}</span>
</button>
))}
</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: 11, color: 'var(--ink-4)', marginRight: 4 }}>
Sentiment
<InfoTip
text="Filters by the post's overall tone. Sentiment trade signal a bearish post can still be a BUY if the market reads it as priced-in."
placement="left"
/>
</span>
{SENTIMENTS.map(f => (
<button
key={f}
+1 -2
View File
@@ -27,7 +27,7 @@ export async function generateMetadata({
'特朗普发帖行情',
'Trump 自动交易',
'Hyperliquid 机器人',
'Trump BTC 信号',
'Trump 加密信号',
]
: [
'Trump crypto signal',
@@ -36,7 +36,6 @@ export async function generateMetadata({
'Trump market signal',
'crypto auto-trader',
'Hyperliquid bot',
'Trump BTC signal',
],
openGraph: {
title: isZh ? 'Trump Truth Social 实时信号 | Trump Alpha' : 'Trump Truth Social Signals | Trump Alpha',