fix: Macro fetches by source, Analytics single-basis stats, funding venue label
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<BotPerformance | null>(null)
|
||||
const [trades, setTrades] = useState<BotTrade[]>([])
|
||||
const [accuracy, setAccuracy] = useState<SignalAccuracy | null>(null)
|
||||
const [period, setPeriod] = useState<Period>('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,
|
||||
|
||||
@@ -43,22 +43,27 @@ export default function MacroVibesPage({
|
||||
const [sentFilter, setSentFilter] = useState<SentimentFilter>('all')
|
||||
const [tab, setTab] = useState<BtcTab>('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
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--ink-4)' }}>
|
||||
{`${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'
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+4
-2
@@ -29,8 +29,9 @@ async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
export async function getPosts(limit = 20, page = 1): Promise<TrumpPost[]> {
|
||||
return fetchJson<TrumpPost[]>(`/posts?limit=${limit}&page=${page}`)
|
||||
export async function getPosts(limit = 20, page = 1, source?: string): Promise<TrumpPost[]> {
|
||||
const q = `/posts?limit=${limit}&page=${page}` + (source ? `&source=${encodeURIComponent(source)}` : '')
|
||||
return fetchJson<TrumpPost[]>(q)
|
||||
}
|
||||
|
||||
export async function getPost(id: number): Promise<TrumpPost> {
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user