From f34ae9eb00756c52e57d04669275ff3d72355337 Mon Sep 17 00:00:00 2001 From: k Date: Tue, 26 May 2026 01:05:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(macro-vibes):=20rename=20BTC=20Signal=20?= =?UTF-8?q?=E2=86=92=20Macro=20Vibes;=20add=20MacroPanel=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/[locale]/DashboardClient.tsx | 16 +- .../analytics/AnalyticsPageClient.tsx | 32 +- app/[locale]/archive/page.tsx | 31 +- app/[locale]/btc/page.tsx | 71 -- app/[locale]/case-studies/page.tsx | 12 +- app/[locale]/globals.css | 762 +++++++++++++++++- app/[locale]/glossary/page.tsx | 14 +- app/[locale]/kol/KolPageClient.tsx | 14 +- .../MacroVibesPageClient.tsx} | 79 +- app/[locale]/macro/page.tsx | 76 ++ app/[locale]/methodology/page.tsx | 12 +- app/[locale]/page.tsx | 4 +- app/[locale]/posts/page.tsx | 10 +- app/[locale]/privacy/page.tsx | 3 +- app/[locale]/settings/SettingsClient.tsx | 13 +- app/[locale]/trades/TradesPageClient.tsx | 6 +- app/[locale]/trump/TrumpPageClient.tsx | 33 +- app/[locale]/trump/page.tsx | 3 +- app/api/proxy/{ => [...path]}/route.ts | 0 app/layout.tsx | 21 +- app/opengraph-image.tsx | 2 +- app/page.tsx | 6 +- app/sitemap.ts | 2 +- components/btc/MacroPanel.tsx | 631 +++++++++++++++ components/nav/Navbar.tsx | 2 +- components/signals/SystemControl.tsx | 49 +- components/ui/InfoTip.tsx | 64 ++ components/ui/PageHint.tsx | 29 + lib/api.ts | 24 + messages/en.json | 2 +- messages/zh.json | 2 +- package.json | 3 +- public/llms.txt | 10 +- 33 files changed, 1810 insertions(+), 228 deletions(-) delete mode 100644 app/[locale]/btc/page.tsx rename app/[locale]/{btc/BtcPageClient.tsx => macro/MacroVibesPageClient.tsx} (82%) create mode 100644 app/[locale]/macro/page.tsx rename app/api/proxy/{ => [...path]}/route.ts (100%) create mode 100644 components/btc/MacroPanel.tsx create mode 100644 components/ui/InfoTip.tsx create mode 100644 components/ui/PageHint.tsx diff --git a/app/[locale]/DashboardClient.tsx b/app/[locale]/DashboardClient.tsx index c7e5eab..9a37c18 100644 --- a/app/[locale]/DashboardClient.tsx +++ b/app/[locale]/DashboardClient.tsx @@ -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) {

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

-

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

+ + 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. +
Live feed - {`${totalPosts} posts tracked`}
@@ -279,7 +281,7 @@ export default function DashboardClient({ initialPosts }: Props) { signal. Always sits ABOVE everything so it's never missed. */} {btcReversalAlert && ( - ⚡ BTC bottom-reversal signal + ⚡ Macro Vibes · Bottom trigger @@ -301,7 +303,7 @@ export default function DashboardClient({ initialPosts }: Props) { - Open BTC signal → + Open Macro Vibes →
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() {

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

-

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

+ + Did the bot actually make money? Win rate, drawdown, average trade, + and AI signal accuracy across the time window you pick on the right. +
{(['7d', '30d', '90d', 'All'] as const).map(p => ( @@ -154,8 +165,11 @@ export default function AnalyticsPageClient() {
{metrics.map(m => ( -
-
{m.k}
+
+
+ {m.k} + {m.tip && } +
{m.v}
{m.sub}
diff --git a/app/[locale]/archive/page.tsx b/app/[locale]/archive/page.tsx index ee19927..ec15563 100644 --- a/app/[locale]/archive/page.tsx +++ b/app/[locale]/archive/page.tsx @@ -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() {

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.'} + + 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. +
diff --git a/app/[locale]/btc/page.tsx b/app/[locale]/btc/page.tsx deleted file mode 100644 index 6a79579..0000000 --- a/app/[locale]/btc/page.tsx +++ /dev/null @@ -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 { - 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 index c9f2715..e436609 100644 --- a/app/[locale]/case-studies/page.tsx +++ b/app/[locale]/case-studies/page.tsx @@ -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 { + const { locale } = await params const copy = getCopy(locale) return { @@ -302,7 +303,8 @@ const SIGNAL_COLOR: Record = { 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 diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css index fc8db7d..429b0c0 100644 --- a/app/[locale]/globals.css +++ b/app/[locale]/globals.css @@ -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 One sentence about the page.. */ +.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 1–3 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; } diff --git a/app/[locale]/glossary/page.tsx b/app/[locale]/glossary/page.tsx index d24cf24..0a470fb 100644 --- a/app/[locale]/glossary/page.tsx +++ b/app/[locale]/glossary/page.tsx @@ -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 Alpha’s 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 Alpha’s 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 { + const { locale } = await params const copy = getCopy(locale) return { @@ -319,7 +320,8 @@ const CATEGORY_COLOR: Record = { '技术术语': '#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))) diff --git a/app/[locale]/kol/KolPageClient.tsx b/app/[locale]/kol/KolPageClient.tsx index ffbc63c..184a41a 100644 --- a/app/[locale]/kol/KolPageClient.tsx +++ b/app/[locale]/kol/KolPageClient.tsx @@ -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({

{isZh ? 'KOL 信号' : 'KOL Signals'}

-

- {`Track long-form KOL theses and extracted asset calls · ${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. +
-
- 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. -
- (initialPosts ?? []) @@ -70,24 +73,20 @@ export default function BtcSignalPage({
-

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

-

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

+

{isZh ? '宏观氛围' : 'Macro Vibes'}

+ {/* 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. */} + + Macro vibes for crypto. Read what's about to happen before price prints it. +
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. -
- )} -
@@ -107,20 +106,34 @@ export default function BtcSignalPage({
+ {/* 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' && ( -
- <> - 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. - +
+ Fires when ≥2 of 3 classic bottom signals agree:{' '} + AHR999 < 0.45 · price ≤ 200-week MA + {' '}· Pi Cycle Bottom. Long only, 2–4 fires per cycle. + Holds up to 18 months with a trailing-stop exit — no take-profit.
)} + {/* 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' && } + {tab === 'funding' && ( +
+ Mean-reversion against crowded perp positioning. When 30-day cumulative + funding crosses ±3% AND the most recent cycles start + cooling off, the scanner bets against the side that's been paying for + weeks. Hourly check. +
+ )} + + {/* Old plain-text per-tab description removed — the same content + now lives in the section-hint block above the tab content. */} + {tab === 'funding' && ( = 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." />
@@ -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 (
{label}
+ letterSpacing: '.06em', marginBottom: 4, + display: 'flex', alignItems: 'center' }}> + {label} + {hint && } +
{value}
{sub &&
{sub}
} diff --git a/app/[locale]/macro/page.tsx b/app/[locale]/macro/page.tsx new file mode 100644 index 0000000..4d5f65b --- /dev/null +++ b/app/[locale]/macro/page.tsx @@ -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 { + 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 ( + + ) +} diff --git a/app/[locale]/methodology/page.tsx b/app/[locale]/methodology/page.tsx index aef65ea..337d708 100644 --- a/app/[locale]/methodology/page.tsx +++ b/app/[locale]/methodology/page.tsx @@ -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 { + 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 diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 2546068..ec787af 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -18,8 +18,8 @@ export async function generateMetadata({ params }: OverviewPageProps): Promise }) { - redirect(`/${params.locale}/trump`) + const { locale } = await params + redirect(`/${locale}/trump`) } diff --git a/app/[locale]/privacy/page.tsx b/app/[locale]/privacy/page.tsx index f2266c2..40e8a88 100644 --- a/app/[locale]/privacy/page.tsx +++ b/app/[locale]/privacy/page.tsx @@ -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 diff --git a/app/[locale]/settings/SettingsClient.tsx b/app/[locale]/settings/SettingsClient.tsx index 3b02a59..8978b42 100644 --- a/app/[locale]/settings/SettingsClient.tsx +++ b/app/[locale]/settings/SettingsClient.tsx @@ -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 ( -
+
+
+
+

{isZh ? '设置' : 'Settings'}

+ + Everything that controls your account — subscription, Hyperliquid + API key, bot risk parameters, and Telegram alerts. + +
+
+ {/* Account card — quick "who am I logged in as" */}

{isZh ? '交易执行' : 'Trades'}

-

{isZh ? '当前持仓、执行历史和按信号来源拆分的盈亏。' : 'Open positions · execution history · P&L by source'}

+ + What the bot actually did with your money — currently-open positions + on top, closed-trade history with realized P&L below. +
diff --git a/app/[locale]/trump/TrumpPageClient.tsx b/app/[locale]/trump/TrumpPageClient.tsx index acf7df6..866aa4f 100644 --- a/app/[locale]/trump/TrumpPageClient.tsx +++ b/app/[locale]/trump/TrumpPageClient.tsx @@ -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

{isZh ? '① Trump 信号' : '① Trump Signal'}

-

- {`Event-driven scalp · ${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. +
Live
-
- 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. -
-
{([ - { 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 => ( ))}
-
+
+ + Sentiment + + {SENTIMENTS.map(f => (