feat: add YTD annualized rate to RateCard

This commit is contained in:
root 2026-02-27 10:08:22 +00:00
parent 1f844b946e
commit 11667d4faa
4 changed files with 70 additions and 9 deletions

View File

@ -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} 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") @app.get("/api/signals/history")
async def get_signals_history(limit: int = 100): async def get_signals_history(limit: int = 100):
"""查询信号推送历史""" """查询信号推送历史"""

View File

@ -2,7 +2,7 @@
import { useEffect, useState, useCallback, useRef } from "react"; import { useEffect, useState, useCallback, useRef } from "react";
import { createChart, ColorType, CandlestickSeries } from "lightweight-charts"; 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 RateCard from "@/components/RateCard";
import StatsCard from "@/components/StatsCard"; import StatsCard from "@/components/StatsCard";
import { import {
@ -97,6 +97,7 @@ export default function Dashboard() {
const [stats, setStats] = useState<StatsResponse | null>(null); const [stats, setStats] = useState<StatsResponse | null>(null);
const [history, setHistory] = useState<HistoryResponse | null>(null); const [history, setHistory] = useState<HistoryResponse | null>(null);
const [signals, setSignals] = useState<SignalHistoryItem[]>([]); const [signals, setSignals] = useState<SignalHistoryItem[]>([]);
const [ytd, setYtd] = useState<YtdStatsResponse | null>(null);
const [status, setStatus] = useState<"loading"|"running"|"error">("loading"); const [status, setStatus] = useState<"loading"|"running"|"error">("loading");
const [lastUpdate, setLastUpdate] = useState(""); const [lastUpdate, setLastUpdate] = useState("");
const [symbol, setSymbol] = useState<"BTC"|"ETH">("BTC"); const [symbol, setSymbol] = useState<"BTC"|"ETH">("BTC");
@ -110,8 +111,8 @@ export default function Dashboard() {
const fetchSlow = useCallback(async () => { const fetchSlow = useCallback(async () => {
try { try {
const [s, h, sig] = await Promise.all([api.stats(), api.history(), api.signalsHistory()]); const [s, h, sig, y] = await Promise.all([api.stats(), api.history(), api.signalsHistory(), api.statsYtd()]);
setStats(s); setHistory(h); setSignals(sig.items || []); setStats(s); setHistory(h); setSignals(sig.items || []); setYtd(y);
} catch {} } catch {}
}, []); }, []);
@ -148,8 +149,8 @@ export default function Dashboard() {
{/* 费率卡片 */} {/* 费率卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RateCard asset="BTC" data={rates?.BTC ?? null} /> <RateCard asset="BTC" data={rates?.BTC ?? null} ytd={ytd?.BTC ?? null} />
<RateCard asset="ETH" data={rates?.ETH ?? null} /> <RateCard asset="ETH" data={rates?.ETH ?? null} ytd={ytd?.ETH ?? null} />
</div> </div>
{/* 统计卡片 */} {/* 统计卡片 */}

View File

@ -1,15 +1,16 @@
"use client"; "use client";
import { RateData } from "@/lib/api"; import { RateData, YtdStats } from "@/lib/api";
interface Props { interface Props {
asset: "BTC" | "ETH"; asset: "BTC" | "ETH";
data: RateData | null; data: RateData | null;
ytd?: YtdStats | null;
} }
const ASSET_EMOJI: Record<string, string> = { BTC: "₿", ETH: "Ξ" }; const ASSET_EMOJI: Record<string, string> = { BTC: "₿", ETH: "Ξ" };
export default function RateCard({ asset, data }: Props) { export default function RateCard({ asset, data, ytd }: Props) {
const rate = data?.lastFundingRate ?? null; const rate = data?.lastFundingRate ?? null;
const positive = rate !== null && rate >= 0; const positive = rate !== null && rate >= 0;
const rateColor = rate === null ? "text-slate-400" : positive ? "text-emerald-600" : "text-red-500"; 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 = (() => { const nextTime = (() => {
if (!data?.nextFundingTime) return "--"; if (!data?.nextFundingTime) return "--";
// 转北京时间UTC+8
const ts = data.nextFundingTime; const ts = data.nextFundingTime;
const bjt = new Date(ts + 8 * 3600 * 1000); const bjt = new Date(ts + 8 * 3600 * 1000);
const now = new Date(Date.now() + 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}`; return isToday ? hhmm : `明天 ${hhmm}`;
})(); })();
const ytdColor = ytd == null ? "text-slate-400"
: ytd.annualized >= 0 ? "text-emerald-600" : "text-red-500";
return ( return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6 space-y-4"> <div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6 space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -60,7 +63,20 @@ export default function RateCard({ asset, data }: Props) {
<p className="text-slate-400 text-xs"></p> <p className="text-slate-400 text-xs"></p>
<p className="text-blue-600 font-mono text-sm mt-0.5">{nextTime}</p> <p className="text-blue-600 font-mono text-sm mt-0.5">{nextTime}</p>
</div> </div>
<div>
<p className="text-slate-400 text-xs"></p>
<p className={`font-mono text-sm mt-0.5 font-semibold ${ytdColor}`}>
{ytd == null ? "--" : `${ytd.annualized > 0 ? "+" : ""}${ytd.annualized.toFixed(2)}%`}
</p>
</div>
<div>
<p className="text-slate-400 text-xs">YTD样本数</p>
<p className="text-slate-500 font-mono text-sm mt-0.5">
{ytd == null ? "--" : `${ytd.count}`}
</p>
</div>
</div> </div>
</div> </div>
); );
} }

View File

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