Skip to content

Commit eba9649

Browse files
feat: implement dynamic precision selection for market data and depth chart visualization.
1 parent 30b98f2 commit eba9649

24 files changed

Lines changed: 479 additions & 98 deletions

src/App.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ function App() {
3131
handleDataSourceChange,
3232
} = useDataSource();
3333

34+
// ========== 交易对状态 ==========
35+
const [activeSymbol, setActiveSymbol] = useState('BTCUSDT');
36+
const handleSymbolChange = useCallback((symbol: string) => {
37+
setActiveSymbol(symbol);
38+
// 切换 symbol 时也可以触发一些重置逻辑 if needed
39+
}, []);
40+
3441
const setSwitching = useUiStore((s: UiState) => s.setSwitching);
3542

3643
// ========== Wasm 引擎 ==========
@@ -63,10 +70,12 @@ function App() {
6370
ticker24h,
6471
recentTrades,
6572
takerBuyRatio,
73+
premiumIndex,
6674
} = useWasmEngine({
6775
tickInterval: 100,
6876
dataSource,
6977
historyCount: 5000,
78+
symbol: activeSymbol,
7079
});
7180

7281
// 切换完成条件
@@ -93,6 +102,7 @@ function App() {
93102
timeframe: activeTimeframe,
94103
ticker24h,
95104
takerBuyRatio,
105+
premiumIndex,
96106
});
97107

98108
const handleTimeframeChange = useCallback(
@@ -133,14 +143,15 @@ function App() {
133143
isRunning={isRunning}
134144
onToggle={dataSource === 'mock' ? toggleFeed : undefined}
135145
price={latestData?.price}
136-
symbol={latestData?.symbol}
146+
symbol={activeSymbol}
137147
priceTrend={priceTrend}
138148
priceColorClass={priceColorClass}
139149
dataSource={dataSource}
140150
onDataSourceChange={handleDataSourceChange}
141151
connectionStatus={connectionStatus}
142152
isSwitching={isSwitching}
143153
marketStats={marketStats}
154+
onSymbolChange={handleSymbolChange}
144155
/>
145156

146157
<main className="flex-1 min-h-0 relative flex flex-col bg-terminal-bg">
@@ -150,6 +161,7 @@ function App() {
150161
fullscreen
151162
dataSource={dataSource}
152163
onSwitchToChart={switchToChartView}
164+
symbol={activeSymbol}
153165
/>
154166
</div>
155167

@@ -189,7 +201,7 @@ function App() {
189201
}
190202
tradesContent={
191203
<div className="h-full bg-terminal-bg">
192-
<RecentTrades trades={recentTrades ?? []} />
204+
<RecentTrades trades={recentTrades ?? []} symbol={activeSymbol} />
193205
</div>
194206
}
195207
/>
@@ -220,7 +232,7 @@ function App() {
220232
}
221233
>
222234
<TradePanel
223-
symbol="BTC"
235+
symbol={activeSymbol.replace('USDT', '')}
224236
currentPrice={latestData?.price ?? 40000}
225237
balance={tradingState?.balance ?? 10000}
226238
availableBalance={tradingState?.availableBalance ?? 10000}

src/components/Dashboard/Chart/DepthChart.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function DepthChart({ bids, asks, price }: DepthChartProps) {
2727
const containerRef = useRef<HTMLDivElement>(null);
2828
const [size, setSize] = useState({ w: 0, h: 0 });
2929
const [mouse, setMouse] = useState<{ x: number; y: number } | null>(null);
30+
const [precision, setPrecision] = useState(0.1);
3031

3132
// 数据处理 Hook
3233
const {
@@ -41,7 +42,7 @@ function DepthChart({ bids, asks, price }: DepthChartProps) {
4142
zoomIn,
4243
zoomOut,
4344
hasData,
44-
} = useDepthData(bids, asks, price);
45+
} = useDepthData(bids, asks, price, precision);
4546

4647
// 响应式尺寸
4748
useEffect(() => {
@@ -167,6 +168,22 @@ function DepthChart({ bids, asks, price }: DepthChartProps) {
167168
<ZoomOut className="w-3.5 h-3.5" />
168169
</button>
169170
</div>
171+
172+
{/* 精度选择 (新增) */}
173+
<div className="absolute top-2 right-3 z-10">
174+
<select
175+
value={precision}
176+
onChange={(e) => setPrecision(Number(e.target.value))}
177+
className="bg-bg-surface/80 backdrop-blur-sm border border-border-dark text-[10px] text-gray-400 rounded px-1.5 py-0.5 outline-none hover:text-white hover:bg-bg-surface-alt transition-colors cursor-pointer appearance-none text-right min-w-[60px]"
178+
title="调整聚合精度"
179+
>
180+
{[0.01, 0.05, 0.1, 0.5, 1, 5, 10, 50, 100].map((p) => (
181+
<option key={p} value={p}>
182+
Step: {p}
183+
</option>
184+
))}
185+
</select>
186+
</div>
170187
</div>
171188
);
172189
}

src/components/Dashboard/Chart/hooks/useDepthData.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,28 @@ export interface UseDepthDataResult {
3636
数据处理函数
3737
============================================ */
3838

39-
function buildDepth(bids: [number, number][], asks: [number, number][]) {
40-
// 买单:从 best bid 向外累加,反转为 [低价 → 高价]
41-
const bidPts: DepthPoint[] = [];
42-
let cum = 0;
43-
for (const [p, a] of bids) {
44-
cum += a;
45-
bidPts.push({ price: p, cumVolume: cum });
46-
}
47-
bidPts.reverse();
48-
49-
// 卖单:从 best ask 向外累加 [低价 → 高价]
50-
const askPts: DepthPoint[] = [];
51-
cum = 0;
52-
for (const [p, a] of asks) {
53-
cum += a;
54-
askPts.push({ price: p, cumVolume: cum });
55-
}
39+
function buildDepth(bids: [number, number][], asks: [number, number][], precision: number) {
40+
// 聚合逻辑
41+
const aggregate = (list: [number, number][], desc: boolean) => {
42+
const map = new Map<number, number>();
43+
for (const [p, a] of list) {
44+
// 向下取整到精度
45+
const k = Math.floor(p / precision) * precision;
46+
map.set(k, (map.get(k) || 0) + a);
47+
}
48+
const sortedKeys = Array.from(map.keys()).sort((a, b) => desc ? b - a : a - b);
49+
50+
const pts: DepthPoint[] = [];
51+
let cum = 0;
52+
for (const k of sortedKeys) {
53+
cum += map.get(k)!;
54+
pts.push({ price: k, cumVolume: cum });
55+
}
56+
return pts;
57+
};
58+
59+
const bidPts = aggregate(bids, true); // 买单价格降序
60+
const askPts = aggregate(asks, false); // 卖单价格升序
5661

5762
return { bidPts, askPts };
5863
}
@@ -65,12 +70,13 @@ export function useDepthData(
6570
bids: [number, number][],
6671
asks: [number, number][],
6772
price?: number,
73+
precision: number = 0.1, // 默认精度
6874
): UseDepthDataResult {
6975
const [zoomIdx, setZoomIdx] = useState(DEFAULT_ZOOM);
7076
const zoom = ZOOM_STEPS[zoomIdx];
7177

7278
// 累积深度
73-
const { bidPts, askPts } = useMemo(() => buildDepth(bids, asks), [bids, asks]);
79+
const { bidPts, askPts } = useMemo(() => buildDepth(bids, asks, precision), [bids, asks, precision]);
7480

7581
// 实时统计
7682
const stats = useMemo<DepthStats>(() => {

src/components/Dashboard/RecentTrades.tsx

Lines changed: 118 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
/**
2-
* RecentTrades - 实时成交列表
2+
* RecentTrades (TradeAnalysis) - 实时成交分析
33
*
4-
* 显示最近 50 笔成交记录,数据来自 Binance WebSocket trade stream
4+
* 显示最近 50 笔成交记录,并提供买卖盘分析(针对特定 Symbol):
5+
* - 买卖量对比 (Taker Buy/Sell Volume)
6+
* - 大单监控 (Whale Alerts)
7+
* - 净流入计算
58
*/
69

7-
import { memo } from 'react';
10+
import { memo, useMemo } from 'react';
811
import type { TradeRecord } from '../../hooks/useBinanceMarket';
12+
import { ArrowDown, ArrowUp } from 'lucide-react';
913

1014
/* ============================================
1115
格式化工具
@@ -20,9 +24,9 @@ function formatTime(ms: number): string {
2024
}
2125

2226
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);
27+
if (qty >= 10) return qty.toFixed(2);
28+
if (qty >= 1) return qty.toFixed(3);
29+
return qty.toFixed(4);
2630
}
2731

2832
/* ============================================
@@ -31,48 +35,137 @@ function formatQty(qty: number): string {
3135

3236
interface RecentTradesProps {
3337
trades: TradeRecord[];
38+
symbol?: string;
3439
}
3540

36-
function RecentTrades({ trades }: RecentTradesProps) {
41+
function RecentTrades({ trades, symbol = 'BTC' }: RecentTradesProps) {
42+
// 统计分析
43+
const stats = useMemo(() => {
44+
let buyVol = 0;
45+
let sellVol = 0;
46+
let buyCount = 0;
47+
let sellCount = 0;
48+
let maxQty = 0;
49+
50+
trades.forEach((t) => {
51+
// isBuyerMaker = false -> 主动买入 (Up)
52+
// isBuyerMaker = true -> 主动卖出 (Down)
53+
if (!t.isBuyerMaker) {
54+
buyVol += t.qty;
55+
buyCount++;
56+
} else {
57+
sellVol += t.qty;
58+
sellCount++;
59+
}
60+
if (t.qty > maxQty) maxQty = t.qty;
61+
});
62+
63+
return {
64+
buyVol,
65+
sellVol,
66+
buyCount,
67+
sellCount,
68+
netVol: buyVol - sellVol,
69+
maxQty: Math.max(maxQty, 0.0001), // 避免除以0
70+
isBuyDominant: buyVol > sellVol,
71+
};
72+
}, [trades]);
73+
3774
if (trades.length === 0) {
3875
return (
39-
<div className="flex items-center justify-center h-full text-gray-600 text-[10px] font-mono">
40-
等待成交数据...
76+
<div className="flex flex-col items-center justify-center h-full text-gray-500 text-[10px] font-mono gap-2">
77+
<span className="w-4 h-4 rounded-full border-2 border-border-dark border-t-accent animate-spin" />
78+
<span>Waiting for trades...</span>
4179
</div>
4280
);
4381
}
4482

83+
// 大单阈值:最大成交量的 30% 或者 绝对值(BTC > 0.1, ETH > 2)
84+
// 这里简化为相对阈值,因为 symbol 不确定
85+
const largeOrderThreshold = stats.maxQty * 0.3;
86+
4587
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>
88+
<div className="flex flex-col h-full overflow-hidden bg-bg-surface text-[10px] font-mono select-none">
89+
{/* ========== 统计面板 ========== */}
90+
<div className="px-2 py-1.5 border-b border-border-dark bg-bg-surface-alt">
91+
{/* 标题行 */}
92+
<div className="flex items-center justify-between mb-1.5">
93+
<span className="font-bold text-gray-300">{symbol} Analysis</span>
94+
<span className={stats.netVol > 0 ? 'text-success' : 'text-danger'}>
95+
Net: {stats.netVol > 0 ? '+' : ''}{formatQty(stats.netVol)}
96+
</span>
97+
</div>
98+
99+
{/* 买卖量条 */}
100+
<div className="flex items-center justify-between mb-1 text-[9px] text-gray-400">
101+
<span className="text-success flex items-center gap-1">
102+
<ArrowUp className="w-2.5 h-2.5" /> Buy {formatQty(stats.buyVol)}
103+
</span>
104+
<span className="text-danger flex items-center gap-1">
105+
Sell {formatQty(stats.sellVol)} <ArrowDown className="w-2.5 h-2.5" />
106+
</span>
107+
</div>
108+
109+
{/* 比例条 */}
110+
<div className="h-1.5 w-full bg-border-dark rounded-full overflow-hidden flex">
111+
<div
112+
className="h-full bg-success transition-all duration-300"
113+
style={{ width: `${(stats.buyVol / (stats.buyVol + stats.sellVol || 1)) * 100}%` }}
114+
/>
115+
<div
116+
className="h-full bg-danger transition-all duration-300"
117+
style={{ width: `${(stats.sellVol / (stats.buyVol + stats.sellVol || 1)) * 100}%` }}
118+
/>
119+
</div>
120+
121+
{/* 净流量 */}
122+
<div className="flex justify-between mt-1 text-[9px]">
123+
<span className="text-gray-500">Vol Ratio</span>
124+
<span className={stats.netVol > 0 ? 'text-success' : 'text-danger'}>
125+
Net: {stats.netVol > 0 ? '+' : ''}{formatQty(stats.netVol)}
126+
</span>
127+
</div>
128+
</div>
129+
130+
{/* ========== 表头 ========== */}
131+
<div className="flex items-center px-2 py-1 text-[9px] text-gray-500 border-b border-border-dark shrink-0 bg-bg-surface">
132+
<span className="w-[30%]">Price</span>
133+
<span className="w-[30%] text-right">Qty</span>
134+
<span className="w-[40%] text-right">Time</span>
52135
</div>
53136

54-
{/* 成交列表 */}
137+
{/* ========== 成交列表 ========== */}
55138
<div className="flex-1 overflow-y-auto scrollbar-thin">
56139
{trades.map((trade) => {
57-
// isBuyerMaker = true → 卖方主动(价格下跌方向)
58-
// isBuyerMaker = false → 买方主动(价格上涨方向)
59-
const colorClass = trade.isBuyerMaker ? 'text-danger' : 'text-success';
140+
const isBuy = !trade.isBuyerMaker;
141+
const isLarge = trade.qty >= largeOrderThreshold;
142+
143+
// 数量条长度 (相对于最大单)
144+
const barWidth = Math.min((trade.qty / stats.maxQty) * 100, 100);
60145

61146
return (
62147
<div
63148
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"
149+
className={`relative flex items-center px-2 py-[2px] transition-colors hover:bg-white/5 ${
150+
isLarge ? 'bg-white/5' : ''
151+
}`}
65152
>
66-
<span className={`w-[32%] ${colorClass}`}>
153+
{/* 背景数量条 */}
154+
<div
155+
className={`absolute left-0 top-0 bottom-0 opacity-10 ${isBuy ? 'bg-success' : 'bg-danger'}`}
156+
style={{ width: `${barWidth}%` }}
157+
/>
158+
159+
<span className={`w-[30%] relative z-10 ${isBuy ? 'text-success' : 'text-danger'} ${isLarge ? 'font-bold' : ''}`}>
67160
{trade.price.toLocaleString(undefined, {
68-
minimumFractionDigits: 2,
69-
maximumFractionDigits: 2,
161+
minimumFractionDigits: 1,
162+
maximumFractionDigits: 1, // 减少小数位,节省空间
70163
})}
71164
</span>
72-
<span className="w-[32%] text-right text-gray-300">
165+
<span className={`w-[30%] text-right relative z-10 ${isLarge ? 'text-gray-100 font-bold' : 'text-gray-400'}`}>
73166
{formatQty(trade.qty)}
74167
</span>
75-
<span className="w-[36%] text-right text-gray-500">
168+
<span className="w-[40%] text-right text-gray-500 relative z-10 opacity-70">
76169
{formatTime(trade.time)}
77170
</span>
78171
</div>

0 commit comments

Comments
 (0)