diff --git a/backend/main.py b/backend/main.py index a5acdc9..b322762 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,7 +2,7 @@ from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware import httpx from datetime import datetime, timedelta -import asyncio, time +import asyncio, time, sqlite3, os app = FastAPI(title="Arbitrage Engine API") @@ -16,6 +16,7 @@ app.add_middleware( BINANCE_FAPI = "https://fapi.binance.com/fapi/v1" SYMBOLS = ["BTCUSDT", "ETHUSDT"] HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} +DB_PATH = os.path.join(os.path.dirname(__file__), "..", "arb.db") # 简单内存缓存(history/stats 60秒,rates 3秒) _cache: dict = {} @@ -30,6 +31,53 @@ def set_cache(key: str, data): _cache[key] = {"ts": time.time(), "data": data} +def init_db(): + conn = sqlite3.connect(DB_PATH) + conn.execute(""" + CREATE TABLE IF NOT EXISTS rate_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + btc_rate REAL NOT NULL, + eth_rate REAL NOT NULL, + btc_price REAL NOT NULL, + eth_price REAL NOT NULL, + btc_index_price REAL, + eth_index_price REAL + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_rate_snapshots_ts ON rate_snapshots(ts)") + conn.commit() + conn.close() + + +def save_snapshot(rates: dict): + try: + conn = sqlite3.connect(DB_PATH) + btc = rates.get("BTC", {}) + eth = rates.get("ETH", {}) + conn.execute( + "INSERT INTO rate_snapshots (ts, btc_rate, eth_rate, btc_price, eth_price, btc_index_price, eth_index_price) VALUES (?,?,?,?,?,?,?)", + ( + int(time.time()), + float(btc.get("lastFundingRate", 0)), + float(eth.get("lastFundingRate", 0)), + float(btc.get("markPrice", 0)), + float(eth.get("markPrice", 0)), + float(btc.get("indexPrice", 0)), + float(eth.get("indexPrice", 0)), + ) + ) + conn.commit() + conn.close() + except Exception as e: + pass # 落库失败不影响API响应 + + +@app.on_event("startup") +async def startup(): + init_db() + + @app.get("/api/health") async def health(): return {"status": "ok", "timestamp": datetime.utcnow().isoformat()} @@ -57,6 +105,28 @@ async def get_rates(): "timestamp": data["time"], } set_cache("rates", result) + # 异步落库(不阻塞响应) + asyncio.create_task(asyncio.to_thread(save_snapshot, result)) + return result + + +@app.get("/api/snapshots") +async def get_snapshots(hours: int = 24, limit: int = 5000): + """查询本地落库的实时快照数据""" + since = int(time.time()) - hours * 3600 + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + rows = conn.execute( + "SELECT ts, btc_rate, eth_rate, btc_price, eth_price FROM rate_snapshots WHERE ts >= ? ORDER BY ts ASC LIMIT ?", + (since, limit) + ).fetchall() + conn.close() + return { + "count": len(rows), + "hours": hours, + "data": [dict(r) for r in rows] + } + set_cache("rates", result) return result diff --git a/frontend/app/live/page.tsx b/frontend/app/live/page.tsx new file mode 100644 index 0000000..d563e93 --- /dev/null +++ b/frontend/app/live/page.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { + LineChart, Line, XAxis, YAxis, Tooltip, Legend, + ResponsiveContainer, ReferenceLine, CartesianGrid +} from "recharts"; + +interface Snapshot { + ts: number; + btc_rate: number; + eth_rate: number; + btc_price: number; + eth_price: number; +} + +interface ChartPoint { + time: string; + btcRate: number; + ethRate: number; + btcPrice: number; + ethPrice: number; +} + +export default function LivePage() { + const [data, setData] = useState([]); + const [count, setCount] = useState(0); + const [loading, setLoading] = useState(true); + const [hours, setHours] = useState(2); + + const fetchSnapshots = useCallback(async () => { + try { + const r = await fetch(`/api/snapshots?hours=${hours}&limit=3600`); + const json = await r.json(); + const rows: Snapshot[] = json.data || []; + setCount(json.count || 0); + // 降采样:每30条取1条,避免图表过密 + const step = Math.max(1, Math.floor(rows.length / 300)); + const sampled = rows.filter((_, i) => i % step === 0); + setData(sampled.map(row => ({ + time: new Date(row.ts * 1000).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }), + btcRate: parseFloat((row.btc_rate * 100).toFixed(5)), + ethRate: parseFloat((row.eth_rate * 100).toFixed(5)), + btcPrice: row.btc_price, + ethPrice: row.eth_price, + }))); + } catch { /* ignore */ } finally { + setLoading(false); + } + }, [hours]); + + useEffect(() => { + fetchSnapshots(); + const iv = setInterval(fetchSnapshots, 10_000); + return () => clearInterval(iv); + }, [fetchSnapshots]); + + return ( +
+
+
+

实时费率变动

+

+ 8小时结算周期内的实时费率曲线 · 已记录 {count.toLocaleString()} 条快照 +

+
+
+ {[1, 2, 6, 12, 24].map(h => ( + + ))} +
+
+ + {loading ? ( +
加载中...
+ ) : data.length === 0 ? ( +
+ 暂无数据(后端刚启动,2秒后开始积累) +
+ ) : ( + <> + {/* 费率图 */} +
+

资金费率实时变动

+

正值=多头付空头,负值=空头付多头。费率上升=多头情绪加热

+ + + + + `${v.toFixed(3)}%`} width={60} /> + [`${v.toFixed(5)}%`]} + contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} /> + + + + + + +
+ + {/* 价格图 */} +
+

标记价格走势

+

与费率对比观察:价格快速拉升时,资金费率通常同步上涨(多头加杠杆)

+ + + + + `$${(v/1000).toFixed(0)}k`} width={55} /> + `$${v.toFixed(0)}`} width={55} /> + [name.includes("BTC") ? `$${v.toLocaleString()}` : `$${v.toFixed(2)}`, name]} + contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} /> + + + + + +
+ + {/* 说明 */} +
+ 数据说明: + 每2秒从Binance拉取实时溢价指数,本地永久存储。这是8小时结算周期内费率变动的原始数据,不在任何公开数据源中提供。 +
+ + )} +
+ ); +} diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index 0352189..279d496 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; const navLinks = [ { href: "/", label: "仪表盘" }, + { href: "/live", label: "实时变动" }, { href: "/history", label: "历史" }, { href: "/signals", label: "信号" }, { href: "/about", label: "说明" },