fix: add cache to prevent Binance rate limit (rates 3s, history/stats 60s)
This commit is contained in:
parent
96468d0d59
commit
42e329cc55
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user