diff --git a/backend/main.py b/backend/main.py index 2cb21b5..24079d2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -440,7 +440,32 @@ async def get_trades_summary( return {"symbol": symbol, "interval": interval, "count": len(result), "data": result} -@app.get("/api/collector/health") +@app.get("/api/trades/latest") +async def get_trades_latest( + symbol: str = "BTC", + limit: int = 30, + user: dict = Depends(get_current_user), +): + """查最新N条原始成交记录(从本地DB,实时刷新用)""" + sym_full = symbol.upper() + "USDT" + now_month = _dt.datetime.now(_dt.timezone.utc).strftime("%Y%m") + tname = f"agg_trades_{now_month}" + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + try: + rows = conn.execute( + f"SELECT agg_id, price, qty, time_ms, is_buyer_maker FROM {tname} " + f"WHERE symbol = ? ORDER BY agg_id DESC LIMIT ?", + (sym_full, limit) + ).fetchall() + except Exception: + rows = [] + conn.close() + return { + "symbol": symbol, + "count": len(rows), + "data": [dict(r) for r in rows], + } async def collector_health(user: dict = Depends(get_current_user)): """采集器健康状态""" conn = sqlite3.connect(DB_PATH) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index c977ed4..dd6b495 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -6,6 +6,7 @@ import { api, RatesResponse, StatsResponse, HistoryResponse, HistoryPoint, Signa import { useAuth } from "@/lib/auth"; import RateCard from "@/components/RateCard"; import StatsCard from "@/components/StatsCard"; +import LiveTradesCard from "@/components/LiveTradesCard"; import Link from "next/link"; import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, @@ -185,6 +186,12 @@ export default function Dashboard() { {/* 统计卡片 */} + {/* 实时成交流 */} +
+ + +
+ {stats && (
diff --git a/frontend/components/LiveTradesCard.tsx b/frontend/components/LiveTradesCard.tsx new file mode 100644 index 0000000..e5a1928 --- /dev/null +++ b/frontend/components/LiveTradesCard.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { authFetch } from "@/lib/auth"; + +interface TradeRow { + time_ms: number; + price: number; + qty: number; + is_buyer_maker: number; // 0=主动买, 1=主动卖 + agg_id: number; +} + +interface TradesResponse { + symbol: string; + count: number; + data: TradeRow[]; +} + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; + +async function fetchLatestTrades(symbol: string): Promise { + // 查最近5秒的原始trades + const end_ms = Date.now(); + const start_ms = end_ms - 5000; + const res = await authFetch( + `/api/trades/latest?symbol=${symbol}&limit=30`, + { cache: "no-store" } + ); + if (!res.ok) return []; + const data: TradesResponse = await res.json(); + return data.data || []; +} + +function timeStr(ms: number): string { + const d = new Date(ms + 8 * 3600 * 1000); + return `${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}:${String(d.getUTCSeconds()).padStart(2,"0")}`; +} + +export default function LiveTradesCard({ symbol }: { symbol: "BTC" | "ETH" }) { + const [trades, setTrades] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + let running = true; + + const fetch = async () => { + try { + const data = await fetchLatestTrades(symbol); + if (running) { + setTrades(prev => { + // 合并新数据,按时间降序,最多保留50条 + const existingIds = new Set(prev.map(t => t.agg_id)); + const newOnes = data.filter(t => !existingIds.has(t.agg_id)); + return [...newOnes, ...prev].slice(0, 50); + }); + setLoading(false); + } + } catch (e) { + if (running) setError("加载失败"); + } + }; + + fetch(); + const iv = setInterval(fetch, 1000); + return () => { running = false; clearInterval(iv); }; + }, [symbol]); + + return ( +
+
+

实时成交流 · {symbol}/USDT

+
+ + 实时 +
+
+ +
+ {loading ? ( +
加载中...
+ ) : error ? ( +
{error}
+ ) : trades.length === 0 ? ( +
等待数据...
+ ) : ( + + + + + + + + + + {trades.map(t => ( + + + + + + ))} + +
价格(USDT)数量时间
+ {t.is_buyer_maker === 0 ? "▲" : "▼"} {t.price.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} + + {t.qty >= 1000 + ? `${(t.qty / 1000).toFixed(2)}K` + : t.qty.toFixed(symbol === "BTC" ? 4 : 3)} + + {timeStr(t.time_ms)} +
+ )} +
+
+ ); +}