diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index dd6b495..c977ed4 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -6,7 +6,6 @@ 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, @@ -186,12 +185,6 @@ export default function Dashboard() { {/* 统计卡片 */} - {/* 实时成交流 */} -
- - -
- {stats && (
diff --git a/frontend/app/trades/page.tsx b/frontend/app/trades/page.tsx new file mode 100644 index 0000000..702d5b4 --- /dev/null +++ b/frontend/app/trades/page.tsx @@ -0,0 +1,332 @@ +"use client"; + +import { useEffect, useState, useRef, useCallback } from "react"; +import { authFetch } from "@/lib/auth"; +import { useAuth } from "@/lib/auth"; +import Link from "next/link"; +import { + AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, CartesianGrid +} from "recharts"; + +// ─── Types ─────────────────────────────────────────────────────── + +interface TradeRow { + agg_id: number; + price: number; + qty: number; + time_ms: number; + is_buyer_maker: number; +} + +interface SummaryBar { + time_ms: number; + buy_vol: number; + sell_vol: number; + delta: number; + total_vol: number; + trade_count: number; + vwap: number; + max_qty: number; +} + +type Symbol = "BTC" | "ETH"; +type Interval = "1m" | "5m" | "15m" | "1h"; + +const INTERVALS: { label: string; value: Interval }[] = [ + { label: "1m", value: "1m" }, + { label: "5m", value: "5m" }, + { label: "15m", value: "15m" }, + { label: "1h", value: "1h" }, +]; + +function bjtStr(ms: number, short = false) { + const d = new Date(ms + 8 * 3600 * 1000); + const hhmm = `${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}`; + if (short) return hhmm; + return `${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,"0")}-${String(d.getUTCDate()).padStart(2,"0")} ${hhmm}`; +} + +function timeStr(ms: number) { + 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")}`; +} + +// ─── 实时成交流 ────────────────────────────────────────────────── + +function LiveTrades({ symbol }: { symbol: Symbol }) { + const [trades, setTrades] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let running = true; + const fetch = async () => { + try { + const res = await authFetch(`/api/trades/latest?symbol=${symbol}&limit=30`); + if (!res.ok) return; + const data = await res.json(); + if (!running) return; + setTrades(prev => { + const existingIds = new Set(prev.map((t: TradeRow) => t.agg_id)); + const newOnes = (data.data || []).filter((t: TradeRow) => !existingIds.has(t.agg_id)); + return [...newOnes, ...prev].slice(0, 60); + }); + setLoading(false); + } catch {} + }; + fetch(); + const iv = setInterval(fetch, 1000); + return () => { running = false; clearInterval(iv); }; + }, [symbol]); + + return ( +
+
+
+

实时成交 · {symbol}/USDT

+
+ + 实时 +
+
+

+ ▲ 绿=主动买(多方发动)  + ▼ 红=主动卖(空方发动) +

+
+
+ {loading ? ( +
加载中...
+ ) : ( + + + + + + + + + + {trades.map(t => ( + + + + + + ))} + +
价格数量时间
+ {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)}
+ )} +
+
+ ); +} + +// ─── 成交流分析(Delta图+买卖量柱状图)──────────────────────────── + +function FlowAnalysis({ symbol }: { symbol: Symbol }) { + const [interval, setInterval_] = useState("5m"); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [dataAge, setDataAge] = useState(0); // 数据积累分钟数 + + const fetchData = useCallback(async () => { + const end_ms = Date.now(); + const windowMs = { "1m": 3600000, "5m": 14400000, "15m": 43200000, "1h": 172800000 }[interval]; + const start_ms = end_ms - windowMs; + try { + const res = await authFetch( + `/api/trades/summary?symbol=${symbol}&start_ms=${start_ms}&end_ms=${end_ms}&interval=${interval}` + ); + if (!res.ok) return; + const json = await res.json(); + const bars: SummaryBar[] = json.data || []; + setData(bars); + setLoading(false); + if (bars.length > 0) { + const ageMs = end_ms - bars[0].time_ms; + setDataAge(Math.round(ageMs / 60000)); + } + } catch {} + }, [symbol, interval]); + + useEffect(() => { + setLoading(true); + fetchData(); + const iv = setInterval(fetchData, 30000); + return () => clearInterval(iv); + }, [fetchData]); + + // Recharts数据格式 + const chartData = data.map(b => ({ + time: bjtStr(b.time_ms, true), + buy: parseFloat(b.buy_vol.toFixed(2)), + sell: parseFloat(b.sell_vol.toFixed(2)), + delta: parseFloat(b.delta.toFixed(2)), + count: b.trade_count, + vwap: b.vwap, + })); + + // 统计摘要 + const totalBuy = data.reduce((s, b) => s + b.buy_vol, 0); + const totalSell = data.reduce((s, b) => s + b.sell_vol, 0); + const totalDelta = totalBuy - totalSell; + const buyPct = totalBuy + totalSell > 0 ? (totalBuy / (totalBuy + totalSell) * 100) : 50; + + return ( +
+
+
+
+

{symbol} 成交流分析

+

买卖 Delta · 主动买卖量分布

+
+
+ {INTERVALS.map(iv => ( + + ))} +
+
+
+ + {loading ? ( +
加载中...
+ ) : data.length === 0 ? ( +
+ 📊 + 暂无数据,正在积累中... + 采集器已启动,数据将持续写入 +
+ ) : ( +
+ {/* 摘要卡片 */} +
+
+

主动买量

+

{totalBuy.toFixed(1)}

+
+
+

主动卖量

+

{totalSell.toFixed(1)}

+
+
+

Delta

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {totalDelta >= 0 ? "+" : ""}{totalDelta.toFixed(1)} +

+
+
+

买方占比

+

= 50 ? "text-emerald-600" : "text-red-500"}`}> + {buyPct.toFixed(1)}% +

+
+
+ + {/* Delta折线图 */} +
+

买卖 Delta(正=多头占优,负=空头占优)

+ + + + + + [v.toFixed(2), "Delta"]} + contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 11 }} + /> + + + + +
+ + {/* 买卖量柱状图 */} +
+

主动买量 vs 主动卖量

+ + + + + + [v.toFixed(2), name === "buy" ? "主动买" : "主动卖"]} + contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 11 }} + /> + + + + +
+ +

+ 数据跨度约 {dataAge} 分钟 · {data.length} 根K · 每30秒刷新 +

+
+ )} +
+ ); +} + +// ─── 主页面 ────────────────────────────────────────────────────── + +export default function TradesPage() { + const { isLoggedIn, loading } = useAuth(); + const [symbol, setSymbol] = useState("BTC"); + + if (loading) return ( +
加载中...
+ ); + + if (!isLoggedIn) return ( +
+
🔒
+

请先登录查看成交流数据

+
+ 登录 + 注册 +
+
+ ); + + return ( +
+ {/* 标题+币种切换 */} +
+
+

成交流分析

+

实时成交记录 + 买卖 Delta 分析

+
+
+ {(["BTC", "ETH"] as Symbol[]).map(s => ( + + ))} +
+
+ + {/* 实时成交 + 分析图 并排 */} +
+ + +
+ + {/* 说明 */} +
+

读图方法:Delta持续为正(买多卖少)说明多方在积累仓位,是潜在上涨信号;Delta突然大幅转负,说明空方在砸盘。

+

复盘思路:找K线大涨大跌的时间点,在此页查看涨跌前15-30分钟的Delta走势,寻找规律性前兆。

+
+
+ ); +} diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 9cb64c5..c3d7e45 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -6,11 +6,12 @@ import { usePathname } from "next/navigation"; import { LayoutDashboard, Info, Menu, X, Zap, LogIn, UserPlus, - ChevronLeft, ChevronRight + ChevronLeft, ChevronRight, Activity } from "lucide-react"; const navItems = [ { href: "/", label: "仪表盘", icon: LayoutDashboard }, + { href: "/trades", label: "成交流", icon: Activity }, { href: "/about", label: "说明", icon: Info }, ];