"use client"; import { useEffect, useState, useCallback } from "react"; import { authFetch } from "@/lib/auth"; import { useAuth } from "@/lib/auth"; import Link from "next/link"; import { ComposedChart, Area, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, CartesianGrid, Legend } 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; setTrades([]); setLoading(true); const fetch = async () => { try { const res = await authFetch(`/api/trades/latest?symbol=${symbol}&limit=20`); if (!res.ok) return; const data = await res.json(); if (!running) return; const incoming: TradeRow[] = data.data || []; setTrades(prev => { const existingIds = new Set(prev.map((t: TradeRow) => t.agg_id)); const newOnes = incoming.filter((t: TradeRow) => !existingIds.has(t.agg_id)); if (newOnes.length === 0) return prev; // 最新在顶部,最多保留50条 return [...newOnes, ...prev].slice(0, 50); }); setLoading(false); } catch {} }; fetch(); const iv = setInterval(fetch, 2000); // 移动端友好:2秒刷新降低重排 return () => { running = false; clearInterval(iv); }; }, [symbol]); return (

实时成交 · {symbol}/USDT

实时

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

{/* 桌面端固定高度内滚,手机端展开不嵌套滚动 */}
{loading ? (
加载中...
) : ( {/* 手机端关闭sticky,避免触摸焦点被内层抢走 */} {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 (showLoading = false) => { 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); if (showLoading) 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(true); const iv = setInterval(() => fetchData(false), 30000); // 定时刷新不触发loading 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; // 价格轴范围:只显示实际波动区间(±0.3%缓冲) const prices = chartData.map(d => d.vwap).filter(v => v > 0); const priceMin = prices.length > 0 ? Math.min(...prices) : 0; const priceMax = prices.length > 0 ? Math.max(...prices) : 0; const pricePad = (priceMax - priceMin) * 0.3 || priceMax * 0.001; const priceYMin = Math.floor(priceMin - pricePad); const priceYMax = Math.ceil(priceMax + pricePad); return (

{symbol} 成交流分析

买卖 Delta · 主动买卖量分布

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

统计时间窗口: {interval === "1m" ? "过去1小时" : interval === "5m" ? "过去4小时" : interval === "15m" ? "过去12小时" : "过去48小时"} (每{interval}聚合一根,每30秒刷新)

单位:{symbol === "BTC" ? "BTC" : "ETH"}

主动买量

{totalBuy.toFixed(1)}

买方主动吃卖单

主动卖量

{totalSell.toFixed(1)}

卖方主动砸买单

Delta(买-卖)

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

{totalDelta >= 0 ? "多头占优 ↑" : "空头占优 ↓"}

买方占比

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

{buyPct >= 55 ? "强势买盘" : buyPct >= 45 ? "买卖均衡" : "强势卖盘"}

{/* Delta + 价格双Y轴图 */}

买卖 Delta(正=多头占优,负=空头占优)+ 币价走势

{/* 左轴:Delta */} {/* 右轴:价格,自适应波动范围 */} v >= 1000 ? `$${(v/1000).toFixed(1)}k` : `$${v.toFixed(0)}`} /> { if (name === "price") return [`$${Number(v).toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`, "币价(VWAP)"]; return [`${Number(v).toFixed(2)}`, "Delta"]; }} contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 11 }} />
{/* 买卖量图 */}

主动买量 vs 主动卖量

[`${Number(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走势,寻找规律性前兆。

); }