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' },
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { getMacroSnapshot, type MacroSnapshot } from '@/lib/api'
|
||||
import { swrFetch } from '@/lib/cache'
|
||||
import InfoTip from '@/components/ui/InfoTip'
|
||||
|
||||
function fmtUsd(n: number | null | undefined, { compact = false } = {}): string {
|
||||
if (n == null || isNaN(n)) return '—'
|
||||
const abs = Math.abs(n)
|
||||
if (compact) {
|
||||
if (abs >= 1e9) return `${(n / 1e9).toFixed(2)}B`
|
||||
if (abs >= 1e6) return `${(n / 1e6).toFixed(1)}M`
|
||||
if (abs >= 1e3) return `${(n / 1e3).toFixed(0)}K`
|
||||
return n.toFixed(0)
|
||||
}
|
||||
return n.toLocaleString('en-US', { maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
function fmtSignedUsd(n: number | null | undefined): string {
|
||||
if (n == null || isNaN(n)) return '—'
|
||||
const compact = fmtUsd(Math.abs(n), { compact: true })
|
||||
return (n >= 0 ? '+$' : '-$') + compact
|
||||
}
|
||||
|
||||
function fmtPct(n: number | null | undefined, digits = 2): string {
|
||||
if (n == null || isNaN(n)) return '—'
|
||||
return n.toFixed(digits) + '%'
|
||||
}
|
||||
|
||||
type Tone = 'up' | 'down' | 'neutral'
|
||||
|
||||
function toneAhr(v: number | null): Tone {
|
||||
if (v == null) return 'neutral'
|
||||
if (v < 0.45) return 'up'
|
||||
if (v > 1.2) return 'down'
|
||||
return 'neutral'
|
||||
}
|
||||
function toneAltseason(v: number | null): Tone {
|
||||
if (v == null) return 'neutral'
|
||||
if (v >= 75) return 'up'
|
||||
if (v <= 25) return 'down'
|
||||
return 'neutral'
|
||||
}
|
||||
function toneFearGreed(v: number | null): Tone {
|
||||
if (v == null) return 'neutral'
|
||||
if (v <= 25) return 'up'
|
||||
if (v >= 75) return 'down'
|
||||
return 'neutral'
|
||||
}
|
||||
function toneEtfFlow(v: number | null): Tone {
|
||||
if (v == null) return 'neutral'
|
||||
if (v > 50_000_000) return 'up'
|
||||
if (v < -50_000_000) return 'down'
|
||||
return 'neutral'
|
||||
}
|
||||
function toneBtcDominance(v: number | null): Tone {
|
||||
if (v == null) return 'neutral'
|
||||
if (v >= 60 || v <= 40) return 'down'
|
||||
return 'neutral'
|
||||
}
|
||||
function toneEthBtc(v: number | null): Tone {
|
||||
if (v == null) return 'neutral'
|
||||
if (v >= 0.04) return 'up'
|
||||
if (v <= 0.025) return 'down'
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
const REGIME_COLOR: Record<string, string> = {
|
||||
BULL: 'var(--up)',
|
||||
BULLISH: 'var(--up)',
|
||||
NEUTRAL: 'var(--ink-3)',
|
||||
BEARISH: 'var(--down)',
|
||||
BEAR: 'var(--down)',
|
||||
}
|
||||
|
||||
/**
|
||||
* Single "Open chart" button per card. Was two (Data source + Chart) — users
|
||||
* couldn't tell which to click and the dual-link group split attention.
|
||||
* Defaults to a CoinGlass page; when CoinGlass doesn't host the indicator
|
||||
* (Pi Cycle Bottom, Stablecoin Supply), we fall back to the canonical site.
|
||||
*/
|
||||
function SourceButton({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="macro-action-btn chart"
|
||||
>
|
||||
<span className="macro-action-copy">
|
||||
<span className="macro-action-kicker">Open chart</span>
|
||||
<span className="macro-action-label">{children}</span>
|
||||
</span>
|
||||
<span className="macro-action-arrow" aria-hidden="true">↗</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function ThresholdChip({
|
||||
tone = 'neutral',
|
||||
active = false,
|
||||
children,
|
||||
}: {
|
||||
tone?: Tone
|
||||
active?: boolean
|
||||
children: ReactNode
|
||||
}) {
|
||||
const cls = `macro-threshold-chip ${tone}${active ? ' active' : ''}`
|
||||
return <span className={cls}>{children}</span>
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
rank,
|
||||
label,
|
||||
value,
|
||||
tone = 'neutral',
|
||||
hint,
|
||||
summary,
|
||||
thresholds,
|
||||
activeIndex,
|
||||
chartHref,
|
||||
chartLabel = 'CoinGlass',
|
||||
}: {
|
||||
rank: number
|
||||
label: string
|
||||
value: string
|
||||
tone?: Tone
|
||||
hint: string
|
||||
summary: string
|
||||
thresholds: Array<{ label: string; tone?: Tone }>
|
||||
/** Index into `thresholds` of the band the current value falls into.
|
||||
* Renders that chip with the .active class — full opacity + highlight. */
|
||||
activeIndex?: number
|
||||
/** URL of the canonical chart page (usually CoinGlass). One button only —
|
||||
* the dual "Data source + Chart" pattern confused users about which to click. */
|
||||
chartHref: string
|
||||
chartLabel?: string
|
||||
}) {
|
||||
return (
|
||||
<article className={`macro-metric-card tone-${tone}`}>
|
||||
<div className="macro-metric-head">
|
||||
<div className="macro-metric-title">
|
||||
<span className="macro-rank">{String(rank).padStart(2, '0')}</span>
|
||||
<span>{label}</span>
|
||||
<InfoTip text={hint} placement="top" width={280} />
|
||||
</div>
|
||||
<div className="macro-metric-value">{value}</div>
|
||||
</div>
|
||||
|
||||
<div className="macro-summary">{summary}</div>
|
||||
|
||||
<div className="macro-thresholds">
|
||||
{thresholds.map((item, i) => (
|
||||
<ThresholdChip key={item.label} tone={item.tone} active={i === activeIndex}>
|
||||
{item.label}
|
||||
</ThresholdChip>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="macro-actions">
|
||||
<SourceButton href={chartHref}>{chartLabel}</SourceButton>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function GuideCard({
|
||||
title,
|
||||
summary,
|
||||
thresholds,
|
||||
chartHref,
|
||||
chartLabel = 'CoinGlass',
|
||||
}: {
|
||||
title: string
|
||||
summary: string
|
||||
thresholds: Array<{ label: string; tone?: Tone }>
|
||||
chartHref: string
|
||||
chartLabel?: string
|
||||
}) {
|
||||
return (
|
||||
<article className="macro-guide-card">
|
||||
<div className="macro-guide-title">{title}</div>
|
||||
<div className="macro-summary">{summary}</div>
|
||||
<div className="macro-thresholds">
|
||||
{thresholds.map((item) => (
|
||||
<ThresholdChip key={item.label} tone={item.tone}>
|
||||
{item.label}
|
||||
</ThresholdChip>
|
||||
))}
|
||||
</div>
|
||||
<div className="macro-actions">
|
||||
<SourceButton href={chartHref}>{chartLabel}</SourceButton>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
children,
|
||||
variant,
|
||||
}: {
|
||||
title: string
|
||||
children: ReactNode
|
||||
/** "reference" tones the section down + adds a 📐 icon so users see at a
|
||||
* glance that the cards inside are explainers, not live data. */
|
||||
variant?: 'reference'
|
||||
}) {
|
||||
return (
|
||||
<section className={`macro-section${variant ? ' ' + variant : ''}`}>
|
||||
<div className="macro-section-title">{title}</div>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MacroPanel() {
|
||||
const [snap, setSnap] = useState<MacroSnapshot | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
function load() {
|
||||
swrFetch(
|
||||
'macro-snapshot',
|
||||
10 * 60_000,
|
||||
() => getMacroSnapshot(),
|
||||
(fresh) => { if (alive) setSnap(fresh) },
|
||||
)
|
||||
.then((s) => { if (alive) { setSnap(s); setErr('') } })
|
||||
.catch((e) => { if (alive) setErr(e instanceof Error ? e.message : 'load failed') })
|
||||
}
|
||||
load()
|
||||
const id = setInterval(load, 10 * 60_000)
|
||||
return () => { alive = false; clearInterval(id) }
|
||||
}, [])
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
<div className="card" style={{ padding: 14, fontSize: 12, color: 'var(--down)' }}>
|
||||
Couldn't load macro indicators — {err}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!snap) {
|
||||
return (
|
||||
<div className="skeleton-card">
|
||||
<div className="skeleton sk-line sk-w-3q" />
|
||||
<div className="skeleton sk-line sk-w-half" />
|
||||
<div className="skeleton sk-line sk-w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!snap.ok || !snap.indicators) {
|
||||
return (
|
||||
<div className="card" style={{ padding: 14, fontSize: 12, color: 'var(--ink-3)' }}>
|
||||
Macro snapshot not available yet — {snap.error ?? 'cron has not run'}.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ind = snap.indicators
|
||||
|
||||
const ahrSay =
|
||||
ind.ahr999 == null ? 'No data yet.'
|
||||
: ind.ahr999 < 0.45 ? 'Deep-value zone. Historically this is where macro accumulation starts to make sense.'
|
||||
: ind.ahr999 > 1.2 ? 'Expensive regime. This is no longer a bottom setup.'
|
||||
: ind.ahr999 < 0.75 ? 'Reasonable value. Closer to cheap than to overheated.'
|
||||
: 'Neutral-to-rich. No strong valuation edge from this metric alone.'
|
||||
|
||||
const altSay =
|
||||
ind.altcoin_season_index == null ? 'No data yet.'
|
||||
: ind.altcoin_season_index >= 75 ? 'Altseason conditions. Broad risk appetite is favoring alts over BTC.'
|
||||
: ind.altcoin_season_index >= 60 ? 'Alt strength is building, but it is not a full altseason yet.'
|
||||
: ind.altcoin_season_index >= 40 ? 'Mixed tape. Neither BTC nor alts have a decisive lead.'
|
||||
: ind.altcoin_season_index >= 25 ? 'BTC is leading. This is a more defensive market posture.'
|
||||
: 'Bitcoin season. Alts are broadly lagging BTC.'
|
||||
|
||||
const fgSay =
|
||||
ind.fear_greed == null ? 'No data yet.'
|
||||
: ind.fear_greed <= 20 ? 'Extreme fear. Contrarian bottom-hunting conditions.'
|
||||
: ind.fear_greed <= 40 ? 'Fear is present, but not yet a full washout.'
|
||||
: ind.fear_greed <= 60 ? 'Neutral sentiment. Little edge from crowd psychology here.'
|
||||
: ind.fear_greed <= 80 ? 'Greed is building. Be selective about adding exposure.'
|
||||
: 'Extreme greed. Historically a poor place to chase upside.'
|
||||
|
||||
const domSay =
|
||||
ind.btc_dominance_pct == null ? 'No data yet.'
|
||||
: ind.btc_dominance_pct >= 65 ? 'Capital is crowding back into BTC. That usually reads as caution.'
|
||||
: ind.btc_dominance_pct >= 55 ? 'BTC still leads the market. Risk appetite is present, but selective.'
|
||||
: ind.btc_dominance_pct >= 45 ? 'Balanced regime. No strong breadth message from dominance.'
|
||||
: ind.btc_dominance_pct >= 35 ? 'Alt participation is improving. Risk appetite is broadening.'
|
||||
: 'Dominance is very low. That often shows late-cycle froth.'
|
||||
|
||||
const ebrSay =
|
||||
ind.eth_btc_ratio == null ? 'No data yet.'
|
||||
: ind.eth_btc_ratio >= 0.05 ? 'ETH leadership is strong. This usually maps to a cleaner risk-on regime.'
|
||||
: ind.eth_btc_ratio >= 0.04 ? 'ETH is waking up versus BTC. Useful confirmation for broader crypto risk appetite.'
|
||||
: ind.eth_btc_ratio >= 0.03 ? 'Range-bound. ETH/BTC is not giving a decisive leadership signal.'
|
||||
: ind.eth_btc_ratio >= 0.025 ? 'ETH is lagging, which usually means BTC remains the safer bid.'
|
||||
: 'ETH is weak versus BTC. This is a defensive backdrop.'
|
||||
|
||||
const stabSay = ind.stablecoin_supply_usd == null
|
||||
? 'No data yet.'
|
||||
: 'This is a liquidity gauge, not a hard trigger. Watch the direction of supply over time.'
|
||||
|
||||
const etfSay = ind.etf_flow_net_usd_1d == null ? 'No data yet.' : (() => {
|
||||
const v = ind.etf_flow_net_usd_1d ?? 0
|
||||
if (v >= 500_000_000) return 'Huge ETF inflow day. Institutions are pressing the bid.'
|
||||
if (v >= 200_000_000) return 'Strong inflow day. Institutional demand is clearly supportive.'
|
||||
if (v >= 50_000_000) return 'Positive day, but not a major outlier.'
|
||||
if (v > -50_000_000) return 'Roughly flat. No strong institutional message.'
|
||||
if (v > -200_000_000) return 'Moderate outflow day. Some distribution, but not panic.'
|
||||
if (v > -500_000_000) return 'Heavy outflows. Institutions are leaning defensive.'
|
||||
return 'Very large outflows. This is a meaningful risk-off signal.'
|
||||
})()
|
||||
|
||||
const oiSay = ind.btc_open_interest_usd == null
|
||||
? 'No data yet.'
|
||||
: 'Use this with price, not by itself. Rising OI confirms commitment; falling OI shows deleveraging.'
|
||||
|
||||
// ── Active threshold band per indicator ─────────────────────────────────
|
||||
// Each function returns the index into the indicator's `thresholds` array
|
||||
// that the CURRENT value falls into. Renders that chip with the .active
|
||||
// CSS class so it stands out from the muted siblings.
|
||||
// Indexes MUST stay in lock-step with the threshold array order below.
|
||||
const ahrActive = ind.ahr999 == null ? -1
|
||||
: ind.ahr999 < 0.45 ? 0
|
||||
: ind.ahr999 < 0.75 ? 1
|
||||
: ind.ahr999 <= 1.2 ? 2
|
||||
: 3
|
||||
const altActive = ind.altcoin_season_index == null ? -1
|
||||
: ind.altcoin_season_index < 25 ? 0
|
||||
: ind.altcoin_season_index < 60 ? 1
|
||||
: ind.altcoin_season_index < 75 ? 2
|
||||
: 3
|
||||
const fgActive = ind.fear_greed == null ? -1
|
||||
: ind.fear_greed < 25 ? 0
|
||||
: ind.fear_greed < 45 ? 1
|
||||
: ind.fear_greed < 60 ? 2
|
||||
: ind.fear_greed < 75 ? 3
|
||||
: 4
|
||||
const domActive = ind.btc_dominance_pct == null ? -1
|
||||
: ind.btc_dominance_pct < 45 ? 0
|
||||
: ind.btc_dominance_pct < 55 ? 1
|
||||
: ind.btc_dominance_pct < 65 ? 2
|
||||
: 3
|
||||
const ebrActive = ind.eth_btc_ratio == null ? -1
|
||||
: ind.eth_btc_ratio < 0.025 ? 0
|
||||
: ind.eth_btc_ratio < 0.04 ? 1
|
||||
: 2
|
||||
const etfActive = ind.etf_flow_net_usd_1d == null ? -1
|
||||
: ind.etf_flow_net_usd_1d > 200_000_000 ? 0
|
||||
: ind.etf_flow_net_usd_1d < -200_000_000 ? 2
|
||||
: 1
|
||||
const stabActive = ind.stablecoin_supply_usd == null ? -1 : 1 // we can't infer trend → land mid
|
||||
|
||||
const compositeColor =
|
||||
snap.composite_score == null ? 'var(--ink-3)'
|
||||
: snap.composite_score > 3 ? 'var(--up)'
|
||||
: snap.composite_score < -3 ? 'var(--down)'
|
||||
: 'var(--ink-3)'
|
||||
|
||||
const compositePct =
|
||||
snap.composite_score == null ? 50
|
||||
: Math.max(0, Math.min(100, (snap.composite_score + 100) / 2))
|
||||
|
||||
const trackGradient =
|
||||
'linear-gradient(to right, ' +
|
||||
'color-mix(in oklab, var(--down) 70%, transparent) 0%, ' +
|
||||
'color-mix(in oklab, var(--down) 25%, transparent) 35%, ' +
|
||||
'var(--bg-sunk) 50%, ' +
|
||||
'color-mix(in oklab, var(--up) 25%, transparent) 65%, ' +
|
||||
'color-mix(in oklab, var(--up) 70%, transparent) 100%)'
|
||||
|
||||
return (
|
||||
<div className="card macro-panel">
|
||||
<div className="macro-panel-head">
|
||||
<div>
|
||||
<div className="macro-panel-title">
|
||||
<span className="macro-panel-dot" />
|
||||
Macro indicators
|
||||
</div>
|
||||
<div className="macro-panel-subtitle">
|
||||
Daily snapshot for BTC macro context. Designed for scanning first, details second.
|
||||
</div>
|
||||
</div>
|
||||
<div className="macro-panel-meta">
|
||||
<div>{snap.date}</div>
|
||||
<div>free public sources</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Valuation">
|
||||
<div className="macro-grid one">
|
||||
<MetricCard
|
||||
rank={1}
|
||||
label="AHR999"
|
||||
value={ind.ahr999?.toFixed(4) ?? '—'}
|
||||
tone={toneAhr(ind.ahr999)}
|
||||
hint="BTC valuation index: price vs 200d MA × price vs fitted long-run growth curve. < 0.45 = buy/accumulation zone, 0.45–0.75 = fair value leaning cheap, 0.75–1.2 = neutral / no edge, > 1.2 = expensive."
|
||||
summary={ahrSay}
|
||||
activeIndex={ahrActive}
|
||||
thresholds={[
|
||||
{ label: '< 0.45 buy zone', tone: 'up' },
|
||||
{ label: '0.45–0.75 fair value', tone: 'neutral' },
|
||||
{ label: '0.75–1.2 no edge', tone: 'neutral' },
|
||||
{ label: '> 1.2 expensive', tone: 'down' },
|
||||
]}
|
||||
chartHref="https://www.coinglass.com/en/pro/i/ahr999"
|
||||
chartLabel="CoinGlass"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Bottom trigger reference" variant="reference">
|
||||
<div className="macro-grid two">
|
||||
<GuideCard
|
||||
title="200-week Moving Average"
|
||||
summary="A structural support check. In this system, price near the 200WMA is one of the three bottom votes."
|
||||
thresholds={[
|
||||
{ label: '≤ 1.05× 200WMA = vote ON', tone: 'up' },
|
||||
{ label: 'Below 200WMA = strongest value zone', tone: 'up' },
|
||||
{ label: '1.05×–1.20× above = recovering', tone: 'neutral' },
|
||||
{ label: 'Far above = no bottom vote', tone: 'down' },
|
||||
]}
|
||||
chartHref="https://www.coinglass.com/pro/i/200WMA"
|
||||
chartLabel="CoinGlass"
|
||||
/>
|
||||
<GuideCard
|
||||
title="Pi Cycle Bottom"
|
||||
summary="A binary bottom condition. It is either confirmed or not confirmed; there is no graded middle reading in the live code."
|
||||
thresholds={[
|
||||
{ label: '150EMA ≤ 471SMA × 0.745 = vote ON', tone: 'up' },
|
||||
{ label: '150EMA above line = not confirmed', tone: 'down' },
|
||||
]}
|
||||
// CoinGlass only hosts Pi Cycle TOP; for Pi Cycle BOTTOM the
|
||||
// canonical chart lives on LookIntoBitcoin.
|
||||
chartHref="https://www.lookintobitcoin.com/charts/pi-cycle-bottom-indicator/"
|
||||
chartLabel="LookIntoBitcoin"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Market structure">
|
||||
<div className="macro-grid two">
|
||||
<MetricCard
|
||||
rank={2}
|
||||
label="Altcoin Season"
|
||||
value={ind.altcoin_season_index == null ? '—' : ind.altcoin_season_index.toFixed(0) + ' / 100'}
|
||||
tone={toneAltseason(ind.altcoin_season_index)}
|
||||
hint="% of top-50 alts that beat BTC over the last 30 days. < 25 = Bitcoin season, 25–60 = mixed, 60–75 = alt strength building, ≥ 75 = altseason."
|
||||
summary={altSay}
|
||||
activeIndex={altActive}
|
||||
thresholds={[
|
||||
{ label: '< 25 Bitcoin season', tone: 'down' },
|
||||
{ label: '25–60 mixed', tone: 'neutral' },
|
||||
{ label: '60–75 alt strength', tone: 'up' },
|
||||
{ label: '≥ 75 altseason', tone: 'up' },
|
||||
]}
|
||||
chartHref="https://www.coinglass.com/en/pro/i/alt-coin-season"
|
||||
chartLabel="CoinGlass"
|
||||
/>
|
||||
<MetricCard
|
||||
rank={3}
|
||||
label="BTC Dominance"
|
||||
value={fmtPct(ind.btc_dominance_pct)}
|
||||
tone={toneBtcDominance(ind.btc_dominance_pct)}
|
||||
hint="BTC's share of total crypto market cap. < 45% = alt rotation, 45–55% = balanced, 55–65% = BTC-led / defensive, > 65% = flight to safety."
|
||||
summary={domSay}
|
||||
activeIndex={domActive}
|
||||
thresholds={[
|
||||
{ label: '< 45% alt rotation', tone: 'up' },
|
||||
{ label: '45–55% balanced', tone: 'neutral' },
|
||||
{ label: '55–65% BTC-led', tone: 'neutral' },
|
||||
{ label: '> 65% safety trade', tone: 'down' },
|
||||
]}
|
||||
chartHref="https://www.coinglass.com/pro/i/bitcoin-dominance"
|
||||
chartLabel="CoinGlass"
|
||||
/>
|
||||
<MetricCard
|
||||
rank={4}
|
||||
label="ETH / BTC"
|
||||
value={ind.eth_btc_ratio == null ? '—' : ind.eth_btc_ratio.toFixed(5)}
|
||||
tone={toneEthBtc(ind.eth_btc_ratio)}
|
||||
hint="Relative strength of ETH vs BTC. < 0.025 = defensive / BTC-led, 0.025–0.04 = range, > 0.04 = ETH and alts gaining leadership."
|
||||
summary={ebrSay}
|
||||
activeIndex={ebrActive}
|
||||
thresholds={[
|
||||
{ label: '< 0.025 defensive', tone: 'down' },
|
||||
{ label: '0.025–0.04 range', tone: 'neutral' },
|
||||
{ label: '> 0.04 ETH leadership', tone: 'up' },
|
||||
]}
|
||||
chartHref="https://www.coinglass.com/currencies/ETHBTC"
|
||||
chartLabel="CoinGlass"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Sentiment & flows">
|
||||
<div className="macro-grid two">
|
||||
<MetricCard
|
||||
rank={5}
|
||||
label="Fear & Greed"
|
||||
value={ind.fear_greed == null ? '—' : String(ind.fear_greed)}
|
||||
tone={toneFearGreed(ind.fear_greed)}
|
||||
hint="Sentiment composite from alternative.me. CONTRARIAN read — extreme fear is bullish, extreme greed is bearish."
|
||||
summary={fgSay}
|
||||
activeIndex={fgActive}
|
||||
thresholds={[
|
||||
{ label: '< 25 → BUY (extreme fear)', tone: 'up' },
|
||||
{ label: '25–45 fear', tone: 'neutral' },
|
||||
{ label: '45–60 neutral', tone: 'neutral' },
|
||||
{ label: '60–75 greed', tone: 'neutral' },
|
||||
{ label: '> 75 → SELL (extreme greed)', tone: 'down' },
|
||||
]}
|
||||
chartHref="https://www.coinglass.com/pro/i/FearGreedIndex?s=09"
|
||||
chartLabel="CoinGlass"
|
||||
/>
|
||||
<MetricCard
|
||||
rank={6}
|
||||
label="Stablecoin Supply"
|
||||
value={ind.stablecoin_supply_usd == null ? '—' : '$' + fmtUsd(ind.stablecoin_supply_usd, { compact: true })}
|
||||
summary={stabSay}
|
||||
hint="Sum of all USDT + USDC + DAI + others in circulation. Rising = dry powder accumulating; falling = capital exiting crypto."
|
||||
activeIndex={stabActive}
|
||||
thresholds={[
|
||||
{ label: 'Rising = liquidity building', tone: 'up' },
|
||||
{ label: 'Flat = neutral', tone: 'neutral' },
|
||||
{ label: 'Falling = capital leaving', tone: 'down' },
|
||||
]}
|
||||
// CoinGlass doesn't expose an all-issuer stablecoin supply view
|
||||
// cleanly; DeFiLlama is the canonical aggregator we pull from.
|
||||
chartHref="https://defillama.com/stablecoins"
|
||||
chartLabel="DeFiLlama"
|
||||
/>
|
||||
<MetricCard
|
||||
rank={7}
|
||||
label="ETF Net Flow (1d)"
|
||||
value={fmtSignedUsd(ind.etf_flow_net_usd_1d)}
|
||||
tone={toneEtfFlow(ind.etf_flow_net_usd_1d)}
|
||||
hint="Yesterday's total net flow into/out of US spot BTC ETFs. > +$200M = strong bid, −$50M to +$50M = noise, < −$200M = heavy outflow."
|
||||
summary={etfSay}
|
||||
activeIndex={etfActive}
|
||||
thresholds={[
|
||||
{ label: '> +$200M strong bid', tone: 'up' },
|
||||
{ label: '±$50M noise band', tone: 'neutral' },
|
||||
{ label: '< −$200M heavy outflow', tone: 'down' },
|
||||
]}
|
||||
chartHref="https://www.coinglass.com/etf"
|
||||
/>
|
||||
<MetricCard
|
||||
rank={8}
|
||||
label="BTC Open Interest"
|
||||
value={ind.btc_open_interest_usd == null ? '—' : '$' + fmtUsd(ind.btc_open_interest_usd, { compact: true })}
|
||||
summary={oiSay}
|
||||
hint="Total notional in BTC futures. No universal absolute threshold: use OI with price. Rising OI + rising price = trend confirmation; rising OI + falling price = trapped longs."
|
||||
thresholds={[
|
||||
{ label: 'OI up + price up = confirmation', tone: 'up' },
|
||||
{ label: 'OI up + price down = stress', tone: 'down' },
|
||||
{ label: 'OI down after flush = reset', tone: 'neutral' },
|
||||
]}
|
||||
chartHref="https://www.tradingview.com/symbols/BTC_OI/"
|
||||
chartLabel="TradingView"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="macro-composite">
|
||||
<div className="macro-composite-head">
|
||||
<div>
|
||||
<div className="macro-composite-label">Composite read</div>
|
||||
<div className="macro-panel-subtitle">
|
||||
Advisory blend of the metrics above. Useful for context, not for blind execution.
|
||||
</div>
|
||||
</div>
|
||||
<div className="macro-composite-right">
|
||||
<span className="macro-composite-value" style={{ color: compositeColor }}>
|
||||
{snap.composite_score == null
|
||||
? '—'
|
||||
: (snap.composite_score > 0 ? '+' : '') + snap.composite_score.toFixed(1)}
|
||||
</span>
|
||||
{snap.regime_label ? (
|
||||
<span
|
||||
className="macro-regime-pill"
|
||||
style={{
|
||||
color: REGIME_COLOR[snap.regime_label] || 'var(--ink-3)',
|
||||
borderColor: `color-mix(in oklab, ${REGIME_COLOR[snap.regime_label] || 'var(--ink-3)'} 32%, transparent)`,
|
||||
background: `color-mix(in oklab, ${REGIME_COLOR[snap.regime_label] || 'var(--ink-3)'} 12%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{snap.regime_label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="macro-composite-track" style={{ background: trackGradient }}>
|
||||
{snap.composite_score != null ? (
|
||||
<div
|
||||
// Keying by score forces a remount → the @keyframes pulse on
|
||||
// .macro-composite-needle re-fires every time the score moves.
|
||||
key={snap.composite_score}
|
||||
className="macro-composite-needle"
|
||||
style={{
|
||||
left: `calc(${compositePct}% - 10px)`,
|
||||
borderColor: compositeColor,
|
||||
color: compositeColor,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="macro-composite-scale">
|
||||
{/* .word spans are hidden on screens ≤420px so the scale becomes
|
||||
just "-100", "0", "+100" — fits comfortably on any phone. */}
|
||||
<span>−100 <span className="word">bear</span></span>
|
||||
<span>0 <span className="word">neutral</span></span>
|
||||
<span>+100 <span className="word">bull</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export default function Navbar() {
|
||||
const tabs = [
|
||||
{ key: '/', label: t('tabs.overview') },
|
||||
{ key: '/trump', label: t('tabs.trump') },
|
||||
{ key: '/btc', label: t('tabs.btc') },
|
||||
{ key: '/macro', label: t('tabs.vibes') },
|
||||
{ key: '/kol', label: t('tabs.kol') },
|
||||
{ key: '/trades', label: t('tabs.trades') },
|
||||
{ key: '/analytics', label: t('tabs.analytics') },
|
||||
|
||||
@@ -26,7 +26,7 @@ import { confirmSign } from '@/components/wallet/SignConfirmSheet'
|
||||
|
||||
const SYS = {
|
||||
trump: { idx: '①', name: 'Trump', accent: '#b45309', soft: 'rgba(180,83,9,0.08)' },
|
||||
btc: { idx: '②', name: 'BTC', accent: '#16a34a', soft: 'rgba(22,163,74,0.08)' },
|
||||
btc: { idx: '②', name: 'Macro Vibes', accent: '#16a34a', soft: 'rgba(22,163,74,0.08)' },
|
||||
} as const
|
||||
|
||||
const ROW: React.CSSProperties = {
|
||||
@@ -122,22 +122,37 @@ export default function SystemControl({ system }: { system: 'trump' | 'btc' }) {
|
||||
} finally { setBusy(false) }
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16, fontSize: 13, color: 'var(--ink-3)' }}>
|
||||
{isZh
|
||||
? `连接右上角的钱包后,才能操作 ${s.name} 模块。`
|
||||
: `Connect a wallet (top-right) to operate the ${s.name} system.`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// SSR / pre-hydration: render nothing to avoid hydration mismatch with
|
||||
// wagmi state that resolves client-side.
|
||||
if (!mounted) return null
|
||||
|
||||
// Wallet not connected: a previous version showed a giant full-width
|
||||
// "Connect a wallet..." card here, which was redundant with the navbar's
|
||||
// Connect button. The opposite extreme — rendering null — silently hid
|
||||
// the existence of Auto-Trade for unconnected users. Compromise: a SLIM
|
||||
// inline strip that simply announces the feature without pushing the
|
||||
// page around. Once connected, the full control panel renders below.
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16, fontSize: 13, color: 'var(--ink-3)' }}>
|
||||
{isZh
|
||||
? `连接右上角的钱包后,才能操作 ${s.name} 模块。`
|
||||
: `Connect a wallet (top-right) to operate the ${s.name} system.`}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap',
|
||||
padding: '10px 14px', marginBottom: 16,
|
||||
background: 'var(--bg-sunk)',
|
||||
border: '1px dashed var(--line)',
|
||||
borderRadius: 8,
|
||||
fontSize: 12, color: 'var(--ink-3)',
|
||||
}}>
|
||||
<span style={{
|
||||
width: 6, height: 6, borderRadius: '50%',
|
||||
background: 'var(--ink-4)', flexShrink: 0,
|
||||
}} />
|
||||
<span style={{ color: 'var(--ink-2)', fontWeight: 600 }}>
|
||||
{s.name} Auto-Trade
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span style={{ color: 'var(--ink-4)' }}>
|
||||
OFF — connect wallet (top-right) to enable
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -156,7 +171,7 @@ export default function SystemControl({ system }: { system: 'trump' | 'btc' }) {
|
||||
} else if (!autoOn) {
|
||||
netMsg = isZh
|
||||
? 'Auto-Trade 当前为关闭(全局): Trump 和 BTC 信号仍会显示,但不会自动开仓。'
|
||||
: 'Auto-Trade is OFF (global) — Trump AND BTC signals are shown in the feed but NOT traded.'
|
||||
: 'Auto-Trade is OFF (global) — Trump AND Macro Vibes signals are shown in the feed but NOT traded.'
|
||||
} else {
|
||||
netOk = true
|
||||
netMsg = isZh
|
||||
@@ -210,8 +225,8 @@ export default function SystemControl({ system }: { system: 'trump' | 'btc' }) {
|
||||
<>
|
||||
<strong>One switch for both systems.</strong> OFF: signals scanned
|
||||
& shown in the feed, nothing traded. ON: a qualifying Trump OR
|
||||
BTC signal auto-opens a trade. Stop-loss / staged de-risk always
|
||||
protect open positions either way.
|
||||
Macro Vibes signal auto-opens a trade. Stop-loss / staged de-risk
|
||||
always protect open positions either way.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Inline "?"" hint icon with a hover tooltip.
|
||||
*
|
||||
* Use this for any UI element whose meaning isn't obvious from its label —
|
||||
* stat boxes, tab names, jargon-y settings. Goal is the same as the (i)
|
||||
* dots in Linear / Hyperliquid / Coinglass: small, ignorable, one hover
|
||||
* away from a one-line plain-English explanation.
|
||||
*
|
||||
* Design rules:
|
||||
* * `text` MUST be one short sentence — never paste long docs in here.
|
||||
* If you need a paragraph, you're explaining the wrong thing.
|
||||
* * The icon is intentionally subtle (12 px circle, ink-4 colour). It
|
||||
* should disappear visually until the user actively scans for help.
|
||||
* * The tooltip uses CSS hover only — no JS positioning, no portals.
|
||||
* Trade-off: it's clipped by the nearest `overflow:hidden` ancestor.
|
||||
* Pages that need tooltips inside a `<table>` cell should add
|
||||
* `overflow: visible` to that row/td.
|
||||
*
|
||||
* Usage:
|
||||
* <span>Latest cycle <InfoTip text="The single most recent funding payment, in %." /></span>
|
||||
*
|
||||
* <InfoTip
|
||||
* text="Last 24h of cycles, averaged. Smooths the noise of one print."
|
||||
* placement="bottom" // default 'top'
|
||||
* />
|
||||
*/
|
||||
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
type Placement = 'top' | 'bottom' | 'left' | 'right'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
placement?: Placement
|
||||
/** Tip max-width in px. Default 240 — fits ~2 short lines. */
|
||||
width?: number
|
||||
/** Tweak the icon's vertical alignment for fussy contexts. */
|
||||
iconStyle?: CSSProperties
|
||||
}
|
||||
|
||||
export default function InfoTip({
|
||||
text,
|
||||
placement = 'top',
|
||||
width = 240,
|
||||
iconStyle,
|
||||
}: Props) {
|
||||
return (
|
||||
<span
|
||||
className={`infotip infotip-${placement}`}
|
||||
// role=button is a slight overstatement but it lets screen-reader users
|
||||
// know there's a thing to tab to. The actual content is in data-tip and
|
||||
// also as a regular <span> child for non-CSS-tooltip fallback.
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={text}
|
||||
style={{ ['--tip-w' as string]: `${width}px`, ...iconStyle }}
|
||||
>
|
||||
<span aria-hidden="true" className="infotip-icon">?</span>
|
||||
<span className="infotip-bubble" role="tooltip">{text}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* One-line "what is this page" tagline that sits right under the page title.
|
||||
*
|
||||
* Replaces ad-hoc <p className="page-sub"> spots that ended up as ghost-gray
|
||||
* unreadable status lines ("Macro cycle bottom · 2 signals"). The new contract:
|
||||
*
|
||||
* * ALWAYS one short sentence — game-tutorial style. Tells the user the
|
||||
* "what" in plain English, not the "how" (the how lives in InfoTip's).
|
||||
* * Slightly bigger + higher contrast than page-sub. Meant to be read,
|
||||
* not glanced past.
|
||||
* * Optional `count` slot for the dim stat ("2 signals", "30 posts").
|
||||
* Renders muted after a separator so the tagline itself stays clean.
|
||||
*/
|
||||
export default function PageHint({
|
||||
children,
|
||||
count,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
count?: string
|
||||
}) {
|
||||
return (
|
||||
<p className="page-hint">
|
||||
{children}
|
||||
{count && (
|
||||
<span className="page-hint-count">{count}</span>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
+24
@@ -466,6 +466,30 @@ export async function getFundingSnapshot(): Promise<FundingSnapshot> {
|
||||
return fetchJson<FundingSnapshot>('/funding/snapshot')
|
||||
}
|
||||
|
||||
// ── Macro snapshot (BTC page Macro Bottom tab) ──────────────────────────────
|
||||
export interface MacroSnapshot {
|
||||
ok: boolean
|
||||
error?: string
|
||||
date?: string
|
||||
captured_at?: string
|
||||
indicators?: {
|
||||
ahr999: number | null
|
||||
altcoin_season_index: number | null
|
||||
fear_greed: number | null
|
||||
fear_greed_label: string | null
|
||||
btc_dominance_pct: number | null
|
||||
eth_btc_ratio: number | null
|
||||
stablecoin_supply_usd: number | null
|
||||
etf_flow_net_usd_1d: number | null
|
||||
btc_open_interest_usd: number | null
|
||||
}
|
||||
composite_score?: number | null
|
||||
regime_label?: string | null
|
||||
}
|
||||
export async function getMacroSnapshot(): Promise<MacroSnapshot> {
|
||||
return fetchJson<MacroSnapshot>('/macro/snapshot')
|
||||
}
|
||||
|
||||
// ── Telegram alerts ─────────────────────────────────────────────────────────
|
||||
export interface TelegramStatus {
|
||||
configured: boolean
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"tabs": {
|
||||
"overview": "Overview",
|
||||
"trump": "Trump",
|
||||
"btc": "BTC",
|
||||
"vibes": "Vibes",
|
||||
"kol": "KOL",
|
||||
"trades": "Trades",
|
||||
"analytics": "Analytics",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"tabs": {
|
||||
"overview": "总览",
|
||||
"trump": "Trump",
|
||||
"btc": "BTC",
|
||||
"vibes": "Vibes",
|
||||
"kol": "KOL",
|
||||
"trades": "交易",
|
||||
"analytics": "分析",
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3001",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"verify": "npm run lint && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rainbow-me/rainbowkit": "^2.1.3",
|
||||
|
||||
+5
-5
@@ -1,20 +1,20 @@
|
||||
# Trump Alpha
|
||||
|
||||
> AI-powered crypto intelligence platform. Tracks four uncorrelated signal sources: Trump Truth Social sentiment, on-chain Bitcoin bottom reversal, KOL Substack/podcast signals, and talks-vs-trades divergence.
|
||||
> AI-powered crypto intelligence platform. Four uncorrelated signal sources: Trump Truth Social sentiment, Macro Vibes (crypto macro regime dashboard), KOL Substack/podcast signals, and talks-vs-trades divergence.
|
||||
|
||||
## What this site does
|
||||
|
||||
Trump Alpha aggregates six independent crypto signal engines into one live dashboard:
|
||||
Trump Alpha aggregates four signal engines and one macro context view into one live dashboard:
|
||||
|
||||
1. **Trump Truth Social Signal** — Scrapes Trump's posts within 15 seconds of publish. AI classifies each as LONG (bullish crypto), SHORT (bearish), or NOISE (off-topic). Optional auto-trade on Hyperliquid with isolated margin, automatic TP/SL, and six user-defined safety limits.
|
||||
|
||||
2. **BTC Bottom Reversal** — Price-confluence scanner: fires a long-only buy signal when at least 2 of 3 classic macro-bottom signals agree — AHR999 < 0.45 (deep-value zone), price ≤ 200-week moving average × 1.05, Pi Cycle Bottom (150-day EMA ≤ 471-day SMA × 0.745). No API keys, no on-chain data, no Glassnode dependency. Occurs 2–4 times per market cycle. Position is invalidated when AHR999 climbs back above 1.2.
|
||||
2. **Macro Vibes** — The crypto macro-regime dashboard. Surfaces eight free public macro signals in one view (AHR999, Fear & Greed Index, BTC dominance, ETH/BTC ratio, total stablecoin supply, US spot Bitcoin ETF daily net flow, BTC perpetual open interest, Altcoin Season Index) plus two trade triggers: a long-only BTC bottom-reversal (2-of-3 confluence across AHR999 < 0.45, price ≤ 200-week MA × 1.05, and Pi Cycle Bottom) and a BTC perpetual funding-rate reversal. Read what's about to happen before price prints it.
|
||||
|
||||
3. **KOL Signal** — 19 crypto KOL feeds including Arthur Hayes, Delphi Digital, Dragonfly Capital, Bankless, Empire, Unchained, 0xResearch, Lightspeed, Pomp, The Defiant, Reflexivity Research (Will Clemente), Bell Curve (Multicoin), The Scoop (The Block), TFTC (Marty Bent). Posts and podcast episodes ingested daily via RSS. AI extracts ticker, direction (buy/sell/bullish/bearish), and conviction (0–1 float).
|
||||
|
||||
4. **Talks-vs-Trades Divergence** — Cross-references KOL public posts against their on-chain Ethereum wallet changes within a ±7-day window. Divergence fires when a KOL is publicly bullish but their wallet is selling (or vice versa). On-chain action is treated as ground truth. This is the platform's highest-conviction signal category.
|
||||
|
||||
5. **Funding Rate Reversal** — Monitors perpetual funding rates across major exchanges. Extreme positive or negative funding historically precedes mean-reversion. Signals fire when rates cross multi-cycle thresholds.
|
||||
5. **Funding Rate Reversal** — Lives inside Macro Vibes. Monitors BTC perpetual funding. Extreme positive or negative funding historically precedes mean-reversion; signals fire when rates cross multi-cycle thresholds AND the most recent cycles start cooling off.
|
||||
|
||||
6. **Hyperliquid Auto-Trader** — Optional execution layer for the Trump pillar. Non-custodial: uses a trade-only API key (cannot withdraw). Configurable leverage, position size, TP/SL, daily cap, and active hours.
|
||||
|
||||
@@ -46,7 +46,7 @@ Optional: bring your own Hyperliquid account for auto-trading.
|
||||
|
||||
- Homepage / landing: https://trumpsignal.com
|
||||
- Trump signal dashboard: https://trumpsignal.com/en/trump
|
||||
- BTC bottom reversal: https://trumpsignal.com/en/btc
|
||||
- Macro Vibes (crypto macro regime + BTC bottom + funding triggers): https://trumpsignal.com/en/macro
|
||||
- KOL signals & divergence: https://trumpsignal.com/en/kol
|
||||
- Signal methodology (full technical detail): https://trumpsignal.com/en/methodology
|
||||
- Glossary (MVRV-Z, STH-SOPR, KOL, divergence...): https://trumpsignal.com/en/glossary
|
||||
|
||||
Reference in New Issue
Block a user