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}
|
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):
|
||||||
"""查询信号推送历史"""
|
"""查询信号推送历史"""
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"),
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user