From a8522a2bf83ecaec02ee398426481dc88ecf4c10 Mon Sep 17 00:00:00 2001 From: k Date: Fri, 29 May 2026 15:15:41 +0800 Subject: [PATCH] fix: Macro fetches by source, Analytics single-basis stats, funding venue label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Macro page fetches getPosts(200, 1, source) per active tab instead of filtering the latest-500 global feed — rare bottom/funding signals no longer vanish behind Trump posts (backend now supports ?source=). - Analytics: compute ALL time windows from the one closed_at-filtered trades list; dropped the 30d-only backend /performance special-case that mixed an opened_at basis into the same screen as the closed_at metric grid. Trade fetch raised 100→500 so the local computation covers ample history. - Funding panel shows snap.venue (capitalized) instead of hardcoded "Binance". - lib/api: getPosts gains optional source param; FundingSnapshot gains venue. tsc + next build clean. Co-Authored-By: Claude Opus 4.7 --- .../analytics/AnalyticsPageClient.tsx | 37 ++++++++++--------- app/[locale]/macro/MacroVibesPageClient.tsx | 23 ++++++++---- lib/api.ts | 6 ++- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/app/[locale]/analytics/AnalyticsPageClient.tsx b/app/[locale]/analytics/AnalyticsPageClient.tsx index a8966ca..821d5ac 100644 --- a/app/[locale]/analytics/AnalyticsPageClient.tsx +++ b/app/[locale]/analytics/AnalyticsPageClient.tsx @@ -3,8 +3,8 @@ import { useState, useEffect, useRef } from 'react' import { useLocale } from 'next-intl' import { useAccount, useSignMessage } from 'wagmi' -import { getPerformance, getTrades, getSignalAccuracy } from '@/lib/api' -import type { BotPerformance, BotTrade } from '@/types' +import { getTrades, getSignalAccuracy } from '@/lib/api' +import type { BotTrade } from '@/types' import type { SignalAccuracy } from '@/lib/api' import { getCachedViewEnvelope, getOrCreateViewEnvelope, type SignedEnvelope } from '@/lib/signedRequest' import PageHint from '@/components/ui/PageHint' @@ -64,7 +64,6 @@ export default function AnalyticsPageClient() { const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json const { address, isConnected } = useAccount() const { signMessageAsync } = useSignMessage() - const [perf, setPerf] = useState(null) const [trades, setTrades] = useState([]) const [accuracy, setAccuracy] = useState(null) const [period, setPeriod] = useState('30d') @@ -81,24 +80,29 @@ export default function AnalyticsPageClient() { // `forcedEnv` (a freshly-minted view_user) lets the in-page Unlock button // load private data without a detour through the Settings page. Public // signal-accuracy always loads regardless. + // + // All displayed stats are computed locally from this single trades list on + // ONE basis (closed_at, see inPeriod) for EVERY time window — we no longer + // mix in the backend /performance aggregate for 30d, which used a different + // basis (it filtered opened_at and capped at 30d) and produced numbers that + // disagreed with the closed_at-based metric grid on the same screen. Limit + // raised to 500 so the local computation covers ample history. async function loadAll(forcedEnv?: SignedEnvelope) { if (!address || !isConnected) { - setPerf(null); setTrades([]); setAccuracy(null); setPrivateLocked(false) + setTrades([]); setAccuracy(null); setPrivateLocked(false) return } - const sharedEnv = forcedEnv ?? getCachedViewEnvelope('view_user', address) - const perfEnv = getCachedViewEnvelope('view_performance', address) ?? sharedEnv - const tradesEnv = getCachedViewEnvelope('view_trades', address) ?? sharedEnv - if (aliveRef.current) setPrivateLocked(!perfEnv && !tradesEnv) + const tradesEnv = getCachedViewEnvelope('view_trades', address) + ?? (forcedEnv ?? getCachedViewEnvelope('view_user', address)) + if (aliveRef.current) setPrivateLocked(!tradesEnv) try { - const [p, t, a] = await Promise.all([ - perfEnv ? getPerformance(address, perfEnv).catch(() => null) : Promise.resolve(null), - tradesEnv ? getTrades(address, tradesEnv, 100, 1).catch(() => []) : Promise.resolve([]), + const [t, a] = await Promise.all([ + tradesEnv ? getTrades(address, tradesEnv, 500, 1).catch(() => []) : Promise.resolve([]), getSignalAccuracy().catch(() => null), ]) - if (aliveRef.current) { setPerf(p); setTrades(t); setAccuracy(a) } + if (aliveRef.current) { setTrades(t); setAccuracy(a) } } catch { - if (aliveRef.current) { setPerf(null); setTrades([]) } + if (aliveRef.current) setTrades([]) } } @@ -139,10 +143,9 @@ export default function AnalyticsPageClient() { const worstTrade = pnls.length ? Math.min(...pnls) : 0 const avgTrade = pnls.length ? totalPnl / pnls.length : 0 const avgHold = filteredTrades.length ? filteredTrades.reduce((sum, trade) => sum + trade.hold_seconds, 0) / filteredTrades.length : 0 - const maxDrawdown = period === '30d' && perf ? perf.max_drawdown_pct : calcDrawdownPct(filteredTrades) - const summaryPnl = period === '30d' && perf && trades.length >= filteredTrades.length - ? perf.net_pnl_usd - : totalPnl + // One basis for every window: derive from the closed_at-filtered trades. + const maxDrawdown = calcDrawdownPct(filteredTrades) + const summaryPnl = totalPnl const metrics = [ { k: isZh ? '最大回撤' : 'Max drawdown', v: filteredTrades.length ? maxDrawdown.toFixed(1) + '%' : '—', sub: isZh ? '从高点到低点的最大跌幅' : 'Worst peak-to-trough', down: true, diff --git a/app/[locale]/macro/MacroVibesPageClient.tsx b/app/[locale]/macro/MacroVibesPageClient.tsx index 5b156b2..e760acf 100644 --- a/app/[locale]/macro/MacroVibesPageClient.tsx +++ b/app/[locale]/macro/MacroVibesPageClient.tsx @@ -43,22 +43,27 @@ export default function MacroVibesPage({ const [sentFilter, setSentFilter] = useState('all') const [tab, setTab] = useState('bottom') + // Tab-scoped post source. 'bottom' = MVRV/200WMA confluence, + // 'funding' = funding-rate extreme reversal. + const tabSource = tab === 'bottom' ? 'btc_bottom_reversal' : 'funding_reversal' + useEffect(() => { - // BTC on-chain signals update daily — 30 min TTL is safe + // Fetch BY SOURCE, not the latest-500 global feed. These scanner signals + // are rare (2–4/cycle, hourly) and were being pushed off the latest-500 + // page by frequent Trump posts — making the Macro page look empty even + // when signals existed. Server-side source filter guarantees we get them. + setLoading(true) swrFetch( - 'posts-500', + `posts-src-${tabSource}`, 3 * 60_000, - () => getPosts(500, 1), + () => getPosts(200, 1, tabSource), fresh => setPosts(fresh), ) .then(p => { setPosts(p); setLoadErr('') }) .catch(e => setLoadErr(e instanceof Error ? e.message : (isZh ? '信号加载失败' : 'Failed to load signals'))) .finally(() => setLoading(false)) - }, [isZh]) + }, [tabSource, isZh]) - // Tab-scoped post source. 'bottom' = MVRV/200WMA confluence, - // 'funding' = funding-rate extreme reversal. - const tabSource = tab === 'bottom' ? 'btc_bottom_reversal' : 'funding_reversal' const btcPosts = useMemo( () => posts.filter(p => (p.source || '') === tabSource), [posts, tabSource], @@ -270,7 +275,9 @@ function FundingPanel({ BTC perp funding · live
- {`${snap.cadence_hours}h cadence · ${snap.coverage_days}d coverage · Binance`} + {`${snap.cadence_hours}h cadence · ${snap.coverage_days}d coverage · ${ + snap.venue ? snap.venue.charAt(0).toUpperCase() + snap.venue.slice(1) : 'Binance' + }`}
diff --git a/lib/api.ts b/lib/api.ts index 02e41a9..03ab342 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -29,8 +29,9 @@ async function fetchJson(path: string, init?: RequestInit): Promise { return res.json() as Promise } -export async function getPosts(limit = 20, page = 1): Promise { - return fetchJson(`/posts?limit=${limit}&page=${page}`) +export async function getPosts(limit = 20, page = 1, source?: string): Promise { + const q = `/posts?limit=${limit}&page=${page}` + (source ? `&source=${encodeURIComponent(source)}` : '') + return fetchJson(q) } export async function getPost(id: number): Promise { @@ -459,6 +460,7 @@ export interface FundingSnapshot { ok: boolean error?: string asset?: string + venue?: string cadence_hours?: number coverage_days?: number latest_rate_pct?: number