"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走势,寻找规律性前兆。

); }