Skip to content

Commit 30b98f2

Browse files
feat: implement recent trades display with a dedicated tab in the chart view
1 parent 8561d31 commit 30b98f2

10 files changed

Lines changed: 287 additions & 14 deletions

File tree

src/App.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ChartTabs, TradeDrawer } from './components/Layout';
1616
import FloatingTradeButton from './components/Layout/FloatingTradeButton';
1717
import { useUiStore, type UiState } from './hooks/ui/useUiStore';
1818
import { useMarketStats } from './hooks/useMarketStats';
19+
import RecentTrades from './components/Dashboard/RecentTrades';
1920

2021
/* ============================================
2122
App - Layout Orchestrator
@@ -59,6 +60,9 @@ function App() {
5960
cancelOrder,
6061
addMargin,
6162
estimateLiquidation,
63+
ticker24h,
64+
recentTrades,
65+
takerBuyRatio,
6266
} = useWasmEngine({
6367
tickInterval: 100,
6468
dataSource,
@@ -87,6 +91,8 @@ function App() {
8791
latestData,
8892
currentPrice: latestData?.price,
8993
timeframe: activeTimeframe,
94+
ticker24h,
95+
takerBuyRatio,
9096
});
9197

9298
const handleTimeframeChange = useCallback(
@@ -181,6 +187,11 @@ function App() {
181187
/>
182188
</div>
183189
}
190+
tradesContent={
191+
<div className="h-full bg-terminal-bg">
192+
<RecentTrades trades={recentTrades ?? []} />
193+
</div>
194+
}
184195
/>
185196
</div>
186197
</main>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* RecentTrades - 实时成交列表
3+
*
4+
* 显示最近 50 笔成交记录,数据来自 Binance WebSocket trade stream
5+
*/
6+
7+
import { memo } from 'react';
8+
import type { TradeRecord } from '../../hooks/useBinanceMarket';
9+
10+
/* ============================================
11+
格式化工具
12+
============================================ */
13+
14+
function formatTime(ms: number): string {
15+
const d = new Date(ms);
16+
const HH = String(d.getHours()).padStart(2, '0');
17+
const MM = String(d.getMinutes()).padStart(2, '0');
18+
const SS = String(d.getSeconds()).padStart(2, '0');
19+
return `${HH}:${MM}:${SS}`;
20+
}
21+
22+
function formatQty(qty: number): string {
23+
if (qty >= 1) return qty.toFixed(4);
24+
if (qty >= 0.01) return qty.toFixed(5);
25+
return qty.toFixed(6);
26+
}
27+
28+
/* ============================================
29+
组件
30+
============================================ */
31+
32+
interface RecentTradesProps {
33+
trades: TradeRecord[];
34+
}
35+
36+
function RecentTrades({ trades }: RecentTradesProps) {
37+
if (trades.length === 0) {
38+
return (
39+
<div className="flex items-center justify-center h-full text-gray-600 text-[10px] font-mono">
40+
等待成交数据...
41+
</div>
42+
);
43+
}
44+
45+
return (
46+
<div className="flex flex-col h-full overflow-hidden">
47+
{/* 表头 */}
48+
<div className="flex items-center px-2 py-1 text-[9px] font-mono text-gray-500 border-b border-border-dark shrink-0">
49+
<span className="w-[32%]">Price (USDT)</span>
50+
<span className="w-[32%] text-right">Amount (BTC)</span>
51+
<span className="w-[36%] text-right">Time</span>
52+
</div>
53+
54+
{/* 成交列表 */}
55+
<div className="flex-1 overflow-y-auto scrollbar-thin">
56+
{trades.map((trade) => {
57+
// isBuyerMaker = true → 卖方主动(价格下跌方向)
58+
// isBuyerMaker = false → 买方主动(价格上涨方向)
59+
const colorClass = trade.isBuyerMaker ? 'text-danger' : 'text-success';
60+
61+
return (
62+
<div
63+
key={trade.id}
64+
className="flex items-center px-2 py-px text-[10px] font-mono tabular-nums hover:bg-bg-surface/50 transition-colors"
65+
>
66+
<span className={`w-[32%] ${colorClass}`}>
67+
{trade.price.toLocaleString(undefined, {
68+
minimumFractionDigits: 2,
69+
maximumFractionDigits: 2,
70+
})}
71+
</span>
72+
<span className="w-[32%] text-right text-gray-300">
73+
{formatQty(trade.qty)}
74+
</span>
75+
<span className="w-[36%] text-right text-gray-500">
76+
{formatTime(trade.time)}
77+
</span>
78+
</div>
79+
);
80+
})}
81+
</div>
82+
</div>
83+
);
84+
}
85+
86+
export default memo(RecentTrades);

src/components/Dashboard/StatsPanel.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ function formatFundingCountdown(seconds: number): string {
6969

7070
/**
7171
* 合约交易信息条
72-
* Binance Futures 风格:单行 5 列布局
73-
* Mark Price / Index Price / Funding Rate / 24h Change / Spread
72+
* Binance Futures 风格:单行 7 列布局
73+
* Mark Price / Index Price / Funding Rate / 24h Change / 24h Vol / Taker Buy / Spread
7474
*/
7575
function StatsPanel({
7676
analysisResult,
@@ -80,16 +80,26 @@ function StatsPanel({
8080
const changeColor = changePercent >= 0 ? 'text-success' : 'text-danger';
8181
const changeSign = changePercent >= 0 ? '+' : '';
8282

83+
// 格式化大数字(成交量/额)
84+
const fmtBig = (n: number): string => {
85+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
86+
if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
87+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
88+
return n.toFixed(2);
89+
};
90+
91+
const takerRatio = marketStats?.takerBuyRatio;
92+
const takerPct = takerRatio != null ? (takerRatio * 100).toFixed(1) : '--';
93+
8394
return (
8495
<div
8596
className="
8697
shrink-0 bg-bg-black border-t border-border-dark
87-
hidden md:grid md:grid-cols-5 divide-x divide-border-dark
98+
hidden md:grid md:grid-cols-7 divide-x divide-border-dark
8899
overflow-x-auto scrollbar-hide
89100
py-2
90101
"
91102
style={{
92-
// 使用 env(safe-area-inset-bottom) 处理刘海屏安全区域
93103
paddingBottom: 'calc(0.5rem + env(safe-area-inset-bottom, 0px))',
94104
}}
95105
>
@@ -123,6 +133,32 @@ function StatsPanel({
123133
colorClass={changeColor}
124134
/>
125135

136+
{/* 24h Volume */}
137+
<StatCell
138+
label="24h Vol"
139+
value={fmtBig(marketStats?.volume24h ?? 0)}
140+
suffix={`≈$${fmtBig(marketStats?.turnover24h ?? 0)}`}
141+
suffixColorClass="text-gray-500"
142+
/>
143+
144+
{/* Taker Buy Ratio */}
145+
<div className="flex flex-col items-center justify-center px-1.5 md:px-3 py-1 h-full">
146+
<span className="text-[8px] md:text-[9px] text-gray-500 uppercase tracking-wider leading-none mb-0.5 md:mb-1">
147+
Buy / Sell
148+
</span>
149+
<div className="flex items-center gap-1">
150+
<div className="w-14 h-1.5 rounded-full overflow-hidden bg-danger/40">
151+
<div
152+
className="h-full rounded-full bg-success transition-all duration-500 ease-out"
153+
style={{ width: takerRatio != null ? `${takerRatio * 100}%` : '50%' }}
154+
/>
155+
</div>
156+
<span className="text-[10px] font-mono font-bold tabular-nums text-success leading-none">
157+
{takerPct}%
158+
</span>
159+
</div>
160+
</div>
161+
126162
{/* Spread */}
127163
<StatCell
128164
label="Spread"

src/components/Layout/ChartTabs/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { memo, useState, type ReactNode } from 'react';
2-
import { CandlestickChart, Layers, ArrowRight } from 'lucide-react';
2+
import { CandlestickChart, Layers, ArrowRight, ListOrdered } from 'lucide-react';
33

4-
type TabType = 'chart' | 'depth';
4+
type TabType = 'chart' | 'depth' | 'trades';
55

66
interface ChartTabsProps {
77
chartContent: ReactNode;
88
depthContent: ReactNode;
9+
/** 逐笔成交内容 */
10+
tradesContent?: ReactNode;
911
/** 当前价格 */
1012
price?: number;
1113
/** 价格变化百分比 */
@@ -29,6 +31,7 @@ interface TabConfig {
2931
const TABS: TabConfig[] = [
3032
{ id: 'chart', label: '图表', icon: <CandlestickChart size={14} /> },
3133
{ id: 'depth', label: '深度', icon: <Layers size={14} /> },
34+
{ id: 'trades', label: '成交', icon: <ListOrdered size={14} /> },
3235
];
3336

3437
/**
@@ -42,6 +45,7 @@ const TABS: TabConfig[] = [
4245
function ChartTabs({
4346
chartContent,
4447
depthContent,
48+
tradesContent,
4549
price,
4650
priceChangePercent = 0,
4751
priceChange = 0,
@@ -161,6 +165,7 @@ function ChartTabs({
161165
>
162166
{activeTab === 'chart' && chartContent}
163167
{activeTab === 'depth' && depthContent}
168+
{activeTab === 'trades' && tradesContent}
164169
</div>
165170
</div>
166171
);

src/hooks/useBinanceMarket.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,26 @@ import {
1919
type MarketType,
2020
type BinanceWsKlineMsg,
2121
type BinanceWsDepthMsg,
22+
type BinanceWsTradeMsg,
23+
type BinanceTicker24h,
2224
type ConnectionStatus,
2325
} from '../services/binance';
2426
import type { OrderBook, HistoryCandle } from '../types/index';
2527

28+
/** 最近成交记录 */
29+
export interface TradeRecord {
30+
/** 交易 ID */
31+
id: number;
32+
/** 价格 */
33+
price: number;
34+
/** 数量 */
35+
qty: number;
36+
/** 成交时间 (毫秒) */
37+
time: number;
38+
/** 是否是买方主动成交 */
39+
isBuyerMaker: boolean;
40+
}
41+
2642
// ============================================================================
2743
// 类型定义
2844
// ============================================================================
@@ -63,6 +79,12 @@ export interface UseBinanceMarketReturn {
6379
currentPrice: number | null;
6480
/** 错误信息 */
6581
error: string | null;
82+
/** 24h Ticker 统计数据 (来自 Binance REST API) */
83+
ticker24h: BinanceTicker24h | null;
84+
/** 最近成交记录 (来自 WebSocket trade stream) */
85+
recentTrades: TradeRecord[];
86+
/** Taker 买入比例 (0~1,实时计算) */
87+
takerBuyRatio: number | null;
6688
}
6789

6890
// ============================================================================
@@ -194,6 +216,9 @@ export function useBinanceMarket(
194216
useState<ConnectionStatus>('disconnected');
195217
const [currentPrice, setCurrentPrice] = useState<number | null>(null);
196218
const [error, setError] = useState<string | null>(null);
219+
const [ticker24h, setTicker24h] = useState<BinanceTicker24h | null>(null);
220+
const [recentTrades, setRecentTrades] = useState<TradeRecord[]>([]);
221+
const [takerBuyRatio, setTakerBuyRatio] = useState<number | null>(null);
197222

198223
// ========== Refs ==========
199224
const wsRef = useRef<BinanceWebSocket | null>(null);
@@ -305,6 +330,15 @@ export function useBinanceMarket(
305330
setIsRunning(true);
306331
setError(null);
307332

333+
// 获取 24h Ticker 统计数据
334+
try {
335+
const ticker = await BinanceAPI.getTicker24h(opts.symbol, opts.market);
336+
setTicker24h(ticker);
337+
console.log('[Binance] ✅ 24h Ticker 已加载');
338+
} catch (tickerErr) {
339+
console.warn('[Binance] ⚠️ 获取 24h Ticker 失败,使用 K 线估算:', tickerErr);
340+
}
341+
308342
// 如果有起始价格,设置它
309343
if (startPrice) {
310344
priceRef.current = startPrice;
@@ -468,8 +502,11 @@ export function useBinanceMarket(
468502
prevKlineVolumeRef.current = volume;
469503
currentKlineVolumeRef.current = volume;
470504

471-
472-
505+
// 计算 Taker 买入比例(实时)
506+
const takerBuyVol = parseFloat(k.V);
507+
if (volume > 0) {
508+
setTakerBuyRatio(takerBuyVol / volume);
509+
}
473510
// K 线完结后,下一根 K 线开始时 prevKlineVolumeRef 会重置为 0
474511

475512
// 立即更新 OrderBook(包含成交量)
@@ -553,10 +590,24 @@ export function useBinanceMarket(
553590
}
554591
}
555592
},
593+
onTrade: (trade: BinanceWsTradeMsg) => {
594+
// 维护最近 50 笔成交记录
595+
const record: TradeRecord = {
596+
id: trade.t,
597+
price: parseFloat(trade.p),
598+
qty: parseFloat(trade.q),
599+
time: trade.T,
600+
isBuyerMaker: trade.m,
601+
};
602+
setRecentTrades((prev) => {
603+
const next = [record, ...prev];
604+
return next.length > 50 ? next.slice(0, 50) : next;
605+
});
606+
},
556607
});
557608

558-
// 订阅 K 线和深度数据
559-
ws.connectKline().connectDepth('100ms').start();
609+
// 订阅 K 线、深度和逐笔交易数据
610+
ws.connectKline().connectDepth('100ms').connectTrade().start();
560611
wsRef.current = ws;
561612

562613
// 定时更新 OrderBook(即使没有深度更新,也定期刷新价格)
@@ -644,5 +695,8 @@ export function useBinanceMarket(
644695
connectionStatus,
645696
currentPrice,
646697
error,
698+
ticker24h,
699+
recentTrades,
700+
takerBuyRatio,
647701
};
648702
}

src/hooks/useMarketData.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
import { useMemo, useEffect, useRef, useCallback } from 'react';
1212
import { useMockMarket } from './useMockMarket';
13-
import { useBinanceMarket } from './useBinanceMarket';
13+
import { useBinanceMarket, type TradeRecord } from './useBinanceMarket';
1414
import type { OrderBook, HistoryCandle } from '../types/index';
15+
import type { BinanceTicker24h } from '../services/binance/types';
1516

1617
// ============================================================================
1718
// 类型定义
@@ -50,6 +51,12 @@ export interface UseMarketDataReturn {
5051
connectionStatus?: string;
5152
/** 错误信息 */
5253
error?: string | null;
54+
/** 24h Ticker 统计 (仅 Binance) */
55+
ticker24h?: BinanceTicker24h | null;
56+
/** 最近成交记录 (仅 Binance) */
57+
recentTrades?: TradeRecord[];
58+
/** Taker 买入比例 (仅 Binance) */
59+
takerBuyRatio?: number | null;
5360
}
5461

5562
// ============================================================================
@@ -146,6 +153,9 @@ export function useMarketData(
146153
dataSource: 'binance',
147154
connectionStatus: binanceData.connectionStatus,
148155
error: binanceData.error,
156+
ticker24h: binanceData.ticker24h,
157+
recentTrades: binanceData.recentTrades,
158+
takerBuyRatio: binanceData.takerBuyRatio,
149159
};
150160
}
151161

0 commit comments

Comments
 (0)