feat: add YTD annualized rate to RateCard
This commit is contained in:
parent
1f844b946e
commit
11667d4faa
@ -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):
|
||||
"""查询信号推送历史"""
|
||||
|
||||
@ -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<StatsResponse | null>(null);
|
||||
const [history, setHistory] = useState<HistoryResponse | null>(null);
|
||||
const [signals, setSignals] = useState<SignalHistoryItem[]>([]);
|
||||
const [ytd, setYtd] = useState<YtdStatsResponse | null>(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() {
|
||||
|
||||
{/* 费率卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<RateCard asset="BTC" data={rates?.BTC ?? null} />
|
||||
<RateCard asset="ETH" data={rates?.ETH ?? null} />
|
||||
<RateCard asset="BTC" data={rates?.BTC ?? null} ytd={ytd?.BTC ?? null} />
|
||||
<RateCard asset="ETH" data={rates?.ETH ?? null} ytd={ytd?.ETH ?? null} />
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
|
||||
@ -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<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 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 (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6 space-y-4">
|
||||
<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-blue-600 font-mono text-sm mt-0.5">{nextTime}</p>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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<T>(path: string): Promise<T> {
|
||||
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<SnapshotsResponse>(`/api/snapshots?hours=${hours}&limit=${limit}`),
|
||||
kline: (symbol = "BTC", interval = "1h", limit = 500) =>
|
||||
fetchAPI<KlineResponse>(`/api/kline?symbol=${symbol}&interval=${interval}&limit=${limit}`),
|
||||
statsYtd: () => fetchAPI<YtdStatsResponse>("/api/stats/ytd"),
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user