diff --git a/backend/main.py b/backend/main.py index 6ff0da9..cd21767 100644 --- a/backend/main.py +++ b/backend/main.py @@ -211,6 +211,39 @@ async def get_kline(symbol: str = "BTC", interval: str = "5m", limit: int = 500) return {"symbol": symbol, "interval": interval, "count": len(data), "data": data} +@app.get("/api/stats/ytd") +async def get_stats_ytd(): + """今年以来(YTD)资金费率年化统计""" + cached = get_cache("stats_ytd", 3600) + if cached: return cached + # 今年1月1日 00:00 UTC + import datetime + year_start = int(datetime.datetime(datetime.datetime.utcnow().year, 1, 1).timestamp() * 1000) + end_time = int(time.time() * 1000) + async with httpx.AsyncClient(timeout=20, headers=HEADERS) as client: + tasks = [ + client.get(f"{BINANCE_FAPI}/fundingRate", + params={"symbol": s, "startTime": year_start, "endTime": end_time, "limit": 1000}) + for s in SYMBOLS + ] + responses = await asyncio.gather(*tasks) + result = {} + for sym, resp in zip(SYMBOLS, responses): + if resp.status_code != 200: + result[sym.replace("USDT","")] = {"annualized": 0, "count": 0} + continue + key = sym.replace("USDT", "") + rates = [float(item["fundingRate"]) for item in resp.json()] + if not rates: + result[key] = {"annualized": 0, "count": 0} + continue + mean = sum(rates) / len(rates) + annualized = round(mean * 3 * 365 * 100, 2) + result[key] = {"annualized": annualized, "count": len(rates)} + set_cache("stats_ytd", result) + return result + + @app.get("/api/signals/history") async def get_signals_history(limit: int = 100): """查询信号推送历史""" diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 77b9c94..cfe08fd 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback, useRef } from "react"; import { createChart, ColorType, CandlestickSeries } from "lightweight-charts"; -import { api, RatesResponse, StatsResponse, HistoryResponse, HistoryPoint, SignalHistoryItem, KBar } from "@/lib/api"; +import { api, RatesResponse, StatsResponse, HistoryResponse, HistoryPoint, SignalHistoryItem, KBar, YtdStatsResponse } from "@/lib/api"; import RateCard from "@/components/RateCard"; import StatsCard from "@/components/StatsCard"; import { @@ -97,6 +97,7 @@ export default function Dashboard() { const [stats, setStats] = useState(null); const [history, setHistory] = useState(null); const [signals, setSignals] = useState([]); + const [ytd, setYtd] = useState(null); const [status, setStatus] = useState<"loading"|"running"|"error">("loading"); const [lastUpdate, setLastUpdate] = useState(""); const [symbol, setSymbol] = useState<"BTC"|"ETH">("BTC"); @@ -110,8 +111,8 @@ export default function Dashboard() { const fetchSlow = useCallback(async () => { try { - const [s, h, sig] = await Promise.all([api.stats(), api.history(), api.signalsHistory()]); - setStats(s); setHistory(h); setSignals(sig.items || []); + const [s, h, sig, y] = await Promise.all([api.stats(), api.history(), api.signalsHistory(), api.statsYtd()]); + setStats(s); setHistory(h); setSignals(sig.items || []); setYtd(y); } catch {} }, []); @@ -148,8 +149,8 @@ export default function Dashboard() { {/* 费率卡片 */}
- - + +
{/* 统计卡片 */} diff --git a/frontend/components/RateCard.tsx b/frontend/components/RateCard.tsx index a363b94..7e23d8c 100644 --- a/frontend/components/RateCard.tsx +++ b/frontend/components/RateCard.tsx @@ -1,15 +1,16 @@ "use client"; -import { RateData } from "@/lib/api"; +import { RateData, YtdStats } from "@/lib/api"; interface Props { asset: "BTC" | "ETH"; data: RateData | null; + ytd?: YtdStats | null; } const ASSET_EMOJI: Record = { BTC: "₿", ETH: "Ξ" }; -export default function RateCard({ asset, data }: Props) { +export default function RateCard({ asset, data, ytd }: Props) { const rate = data?.lastFundingRate ?? null; const positive = rate !== null && rate >= 0; const rateColor = rate === null ? "text-slate-400" : positive ? "text-emerald-600" : "text-red-500"; @@ -21,7 +22,6 @@ export default function RateCard({ asset, data }: Props) { const nextTime = (() => { if (!data?.nextFundingTime) return "--"; - // 转北京时间(UTC+8) const ts = data.nextFundingTime; const bjt = new Date(ts + 8 * 3600 * 1000); const now = new Date(Date.now() + 8 * 3600 * 1000); @@ -30,6 +30,9 @@ export default function RateCard({ asset, data }: Props) { return isToday ? hhmm : `明天 ${hhmm}`; })(); + const ytdColor = ytd == null ? "text-slate-400" + : ytd.annualized >= 0 ? "text-emerald-600" : "text-red-500"; + return (
@@ -60,7 +63,20 @@ export default function RateCard({ asset, data }: Props) {

下次结算

{nextTime}

+
+

今年以来年化

+

+ {ytd == null ? "--" : `${ytd.annualized > 0 ? "+" : ""}${ytd.annualized.toFixed(2)}%`} +

+
+
+

YTD样本数

+

+ {ytd == null ? "--" : `${ytd.count}次`} +

+
); } + diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index 7260a96..c254cb1 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -64,7 +64,17 @@ export interface SnapshotsResponse { data: SnapshotItem[]; } -export interface KBar { +export interface YtdStats { + annualized: number; + count: number; +} + +export interface YtdStatsResponse { + BTC: YtdStats; + ETH: YtdStats; +} + +async function fetchAPI(path: string): Promise { time: number; open: number; high: number; low: number; close: number; price_open: number; price_high: number; price_low: number; price_close: number; @@ -90,4 +100,5 @@ export const api = { fetchAPI(`/api/snapshots?hours=${hours}&limit=${limit}`), kline: (symbol = "BTC", interval = "1h", limit = 500) => fetchAPI(`/api/kline?symbol=${symbol}&interval=${interval}&limit=${limit}`), + statsYtd: () => fetchAPI("/api/stats/ytd"), };