fix: add cache to prevent Binance rate limit (rates 3s, history/stats 60s)

This commit is contained in:
root 2026-02-26 13:54:59 +00:00
parent 96468d0d59
commit 42e329cc55

View File

@ -2,7 +2,7 @@ from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import httpx import httpx
from datetime import datetime, timedelta from datetime import datetime, timedelta
import asyncio import asyncio, time
app = FastAPI(title="Arbitrage Engine API") app = FastAPI(title="Arbitrage Engine API")
@ -16,6 +16,18 @@ app.add_middleware(
BINANCE_FAPI = "https://fapi.binance.com/fapi/v1" BINANCE_FAPI = "https://fapi.binance.com/fapi/v1"
SYMBOLS = ["BTCUSDT", "ETHUSDT"] SYMBOLS = ["BTCUSDT", "ETHUSDT"]
# 简单内存缓存history/stats 60秒rates 3秒
_cache: dict = {}
def get_cache(key: str, ttl: int):
entry = _cache.get(key)
if entry and time.time() - entry["ts"] < ttl:
return entry["data"]
return None
def set_cache(key: str, data):
_cache[key] = {"ts": time.time(), "data": data}
@app.get("/api/health") @app.get("/api/health")
async def health(): async def health():
@ -24,10 +36,11 @@ async def health():
@app.get("/api/rates") @app.get("/api/rates")
async def get_rates(): async def get_rates():
cached = get_cache("rates", 3)
if cached: return cached
async with httpx.AsyncClient(timeout=10) as client: async with httpx.AsyncClient(timeout=10) as client:
tasks = [client.get(f"{BINANCE_FAPI}/premiumIndex", params={"symbol": s}) for s in SYMBOLS] tasks = [client.get(f"{BINANCE_FAPI}/premiumIndex", params={"symbol": s}) for s in SYMBOLS]
responses = await asyncio.gather(*tasks) responses = await asyncio.gather(*tasks)
result = {} result = {}
for sym, resp in zip(SYMBOLS, responses): for sym, resp in zip(SYMBOLS, responses):
if resp.status_code != 200: if resp.status_code != 200:
@ -42,55 +55,50 @@ async def get_rates():
"nextFundingTime": data["nextFundingTime"], "nextFundingTime": data["nextFundingTime"],
"timestamp": data["time"], "timestamp": data["time"],
} }
set_cache("rates", result)
return result return result
@app.get("/api/history") @app.get("/api/history")
async def get_history(): async def get_history():
cached = get_cache("history", 60)
if cached: return cached
end_time = int(datetime.utcnow().timestamp() * 1000) end_time = int(datetime.utcnow().timestamp() * 1000)
start_time = int((datetime.utcnow() - timedelta(days=7)).timestamp() * 1000) start_time = int((datetime.utcnow() - timedelta(days=7)).timestamp() * 1000)
async with httpx.AsyncClient(timeout=15) as client: async with httpx.AsyncClient(timeout=15) as client:
tasks = [ tasks = [
client.get( client.get(f"{BINANCE_FAPI}/fundingRate",
f"{BINANCE_FAPI}/fundingRate", params={"symbol": s, "startTime": start_time, "endTime": end_time, "limit": 1000})
params={"symbol": s, "startTime": start_time, "endTime": end_time, "limit": 1000},
)
for s in SYMBOLS for s in SYMBOLS
] ]
responses = await asyncio.gather(*tasks) responses = await asyncio.gather(*tasks)
result = {} result = {}
for sym, resp in zip(SYMBOLS, responses): for sym, resp in zip(SYMBOLS, responses):
if resp.status_code != 200: if resp.status_code != 200:
raise HTTPException(status_code=502, detail=f"Binance history error for {sym}") raise HTTPException(status_code=502, detail=f"Binance history error for {sym}")
key = sym.replace("USDT", "") key = sym.replace("USDT", "")
result[key] = [ result[key] = [
{ {"fundingTime": item["fundingTime"], "fundingRate": float(item["fundingRate"]),
"fundingTime": item["fundingTime"], "timestamp": datetime.utcfromtimestamp(item["fundingTime"] / 1000).isoformat()}
"fundingRate": float(item["fundingRate"]),
"timestamp": datetime.utcfromtimestamp(item["fundingTime"] / 1000).isoformat(),
}
for item in resp.json() for item in resp.json()
] ]
set_cache("history", result)
return result return result
@app.get("/api/stats") @app.get("/api/stats")
async def get_stats(): async def get_stats():
cached = get_cache("stats", 60)
if cached: return cached
end_time = int(datetime.utcnow().timestamp() * 1000) end_time = int(datetime.utcnow().timestamp() * 1000)
start_time = int((datetime.utcnow() - timedelta(days=7)).timestamp() * 1000) start_time = int((datetime.utcnow() - timedelta(days=7)).timestamp() * 1000)
async with httpx.AsyncClient(timeout=15) as client: async with httpx.AsyncClient(timeout=15) as client:
tasks = [ tasks = [
client.get( client.get(f"{BINANCE_FAPI}/fundingRate",
f"{BINANCE_FAPI}/fundingRate", params={"symbol": s, "startTime": start_time, "endTime": end_time, "limit": 1000})
params={"symbol": s, "startTime": start_time, "endTime": end_time, "limit": 1000},
)
for s in SYMBOLS for s in SYMBOLS
] ]
responses = await asyncio.gather(*tasks) responses = await asyncio.gather(*tasks)
stats = {} stats = {}
for sym, resp in zip(SYMBOLS, responses): for sym, resp in zip(SYMBOLS, responses):
if resp.status_code != 200: if resp.status_code != 200:
@ -101,14 +109,12 @@ async def get_stats():
stats[key] = {"mean7d": 0, "annualized": 0, "count": 0} stats[key] = {"mean7d": 0, "annualized": 0, "count": 0}
continue continue
mean = sum(rates) / len(rates) mean = sum(rates) / len(rates)
annualized = mean * 3 * 365 * 100 # 3 settlements/day * 365 days * % annualized = mean * 3 * 365 * 100
stats[key] = { stats[key] = {
"mean7d": round(mean * 100, 6), # as percent "mean7d": round(mean * 100, 6),
"annualized": round(annualized, 2), # as percent "annualized": round(annualized, 2),
"count": len(rates), "count": len(rates),
} }
# 50-50 combo
btc_ann = stats.get("BTC", {}).get("annualized", 0) btc_ann = stats.get("BTC", {}).get("annualized", 0)
eth_ann = stats.get("ETH", {}).get("annualized", 0) eth_ann = stats.get("ETH", {}).get("annualized", 0)
btc_mean = stats.get("BTC", {}).get("mean7d", 0) btc_mean = stats.get("BTC", {}).get("mean7d", 0)
@ -117,5 +123,5 @@ async def get_stats():
"mean7d": round((btc_mean + eth_mean) / 2, 6), "mean7d": round((btc_mean + eth_mean) / 2, 6),
"annualized": round((btc_ann + eth_ann) / 2, 2), "annualized": round((btc_ann + eth_ann) / 2, 2),
} }
set_cache("stats", stats)
return stats return stats