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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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 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; }
|
||||
|
||||
@@ -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<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)))
|
||||
|
||||
|
||||
@@ -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'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 < 0.45</strong> · <strong>price ≤ 200-week
|
||||
MA</strong> · <strong>Pi Cycle Bottom</strong>. Long only, low frequency
|
||||
(2–4 fires per cycle). Scans daily 00:45 UTC. Designed to ride the
|
||||
full cycle: holds up to <strong>18 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 < 0.45</strong> · <strong>price ≤ 200-week MA</strong>
|
||||
{' '}· <strong>Pi Cycle Bottom</strong>. Long only, 2–4 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>}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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&L below.
|
||||
</PageHint>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
+6
-15
@@ -1,5 +1,4 @@
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
import { Geist, Geist_Mono } from 'next/font/google'
|
||||
import { getLocale } from 'next-intl/server'
|
||||
import Script from 'next/script'
|
||||
import './[locale]/globals.css'
|
||||
@@ -9,16 +8,6 @@ const siteTagline = 'Four crypto alpha feeds, one dashboard'
|
||||
const siteDescription = 'Real-time Trump Truth Social signals, BTC macro-bottom confluence, KOL Substack/podcast signals, and talks-vs-trades divergence — all scored by AI in one live dashboard.'
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
|
||||
|
||||
const geistSans = Geist({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-geist-sans',
|
||||
})
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-geist-mono',
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: siteUrl ? new URL(siteUrl) : new URL('https://trumpsignal.com'),
|
||||
title: {
|
||||
@@ -32,6 +21,8 @@ export const metadata: Metadata = {
|
||||
'TrumpSignal',
|
||||
'crypto signals',
|
||||
'crypto alpha dashboard',
|
||||
'Macro Vibes',
|
||||
'crypto macro dashboard',
|
||||
'BTC bottom reversal',
|
||||
'Bitcoin macro bottom',
|
||||
'AHR999',
|
||||
@@ -164,10 +155,10 @@ const jsonLd = {
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'What is the BTC bottom reversal signal?',
|
||||
name: 'What is Macro Vibes?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'The BTC bottom reversal module is a long-only macro-bottom scanner. It fires when at least two of three classic bottom signals agree: AHR999 deep-value zone, price near the 200-week moving average, and Pi Cycle Bottom. These signals are intentionally rare and aimed at cycle-level reversals rather than short-term swings.',
|
||||
text: 'Macro Vibes is the macro-regime dashboard for crypto. It surfaces eight free, public macro signals in one view — AHR999, Fear & Greed, BTC dominance, ETH/BTC ratio, total stablecoin supply, US spot Bitcoin ETF daily net flow, BTC perp open interest, and the Altcoin Season Index — plus a long-only BTC bottom-reversal trigger (2-of-3 confluence of AHR999, 200-week MA, and Pi Cycle Bottom) and a BTC perp funding-rate reversal trigger. One screen tells you whether the macro tape is bullish, bearish, or neutral.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -191,7 +182,7 @@ const jsonLd = {
|
||||
name: 'Is Trump Alpha free?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes. All four signal dashboards (Trump, BTC bottom, KOL, talks-vs-trades) are free to read. The optional Hyperliquid auto-trader requires your own Hyperliquid account and API key. Trump Alpha never takes custody of your funds — your API key can open and close positions but cannot withdraw.',
|
||||
text: 'Yes. All four signal dashboards (Trump, Macro Vibes, KOL, talks-vs-trades) are free to read. The optional Hyperliquid auto-trader requires your own Hyperliquid account and API key. Trump Alpha never takes custody of your funds — your API key can open and close positions but cannot withdraw.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -247,7 +238,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
</head>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
<body>
|
||||
{/* Umami analytics — loaded after interactive so it never blocks render */}
|
||||
<Script
|
||||
src="https://stats.bitnews.day/script.js"
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function OGImage() {
|
||||
>
|
||||
{[
|
||||
{ label: 'Trump · Truth Social', color: '#f5a524' },
|
||||
{ label: 'BTC Bottom Reversal', color: '#a78bfa' },
|
||||
{ label: 'Macro Vibes', color: '#a78bfa' },
|
||||
{ label: 'KOL Signal', color: '#22c55e' },
|
||||
{ label: 'Talks vs Trades', color: '#ef4444' },
|
||||
].map((p) => (
|
||||
|
||||
+3
-3
@@ -326,11 +326,11 @@ export default function LandingPage() {
|
||||
/>
|
||||
<PillarCard
|
||||
tag="Daily · macro"
|
||||
title="BTC Bottom Reversal"
|
||||
title="Macro Vibes"
|
||||
desc="2-of-3 confluence across AHR999, the 200-week moving average, and Pi Cycle Bottom. Rare, high-conviction long-only entries."
|
||||
metric="2–4×"
|
||||
metricLabel="signals per cycle"
|
||||
href="/en/btc"
|
||||
href="/en/macro"
|
||||
/>
|
||||
<PillarCard
|
||||
tag="Daily · narrative"
|
||||
@@ -354,7 +354,7 @@ export default function LandingPage() {
|
||||
desc="Funding spikes → crowded trade unwinds. At multi-cycle extremes, mean-reversion is the highest-probability play."
|
||||
metric="±0.1%"
|
||||
metricLabel="threshold trigger"
|
||||
href="/en/btc"
|
||||
href="/en/macro"
|
||||
/>
|
||||
<PillarCard
|
||||
tag="On demand · execution"
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ const routes: Array<{
|
||||
}> = [
|
||||
{ path: '', priority: 1.0, freq: 'hourly' },
|
||||
{ path: '/trump', priority: 0.9, freq: 'hourly' },
|
||||
{ path: '/btc', priority: 0.9, freq: 'daily' },
|
||||
{ path: '/macro', priority: 0.9, freq: 'daily' },
|
||||
{ path: '/kol', priority: 0.9, freq: 'daily' },
|
||||
{ path: '/trades', priority: 0.8, freq: 'daily' },
|
||||
{ path: '/analytics', priority: 0.7, freq: 'daily' },
|
||||
|
||||
Reference in New Issue
Block a user