Skip to content

Commit e99c1e6

Browse files
committed
refactor: 支持 Binance 实时数据源与成交量追踪优化
- **数据源切换**: - 新增 `dataSource` 状态管理(mock/binance),支持 URL 参数与 localStorage 持久化 - 在 Header 中新增数据源切换按钮,显示连接状态 - LIVE 模式下隐藏交易表单与交易栏,仅展示市场数据 - **成交量处理优化**: - 在 `OrderBook` 模型中新增 `volume` 字段,用于接收 Binance K 线的累计成交量 - 更新 `MarketEngine::process_tick` 逻辑
1 parent 002dad0 commit e99c1e6

20 files changed

Lines changed: 2116 additions & 119 deletions

File tree

core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ serde = { version = "1.0", features = ["derive"] }
1616
serde-wasm-bindgen = "0.4"
1717
# JavaScript API 绑定
1818
js-sys = "0.3"
19+
web-sys = { version = "0.3", features = ["console"] }
1920

2021
[profile.release]
2122
# 优化 wasm 体积

core/src/engine/market_engine/market_data.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,27 @@ impl MarketEngine {
1313
pub(crate) fn process_tick(&mut self, order_book: &OrderBook) {
1414
// 1. 更新 Tick 数据
1515
self.tick_data.push_price(order_book.price);
16-
let volume = self.tick_data.estimate_volume(order_book);
16+
17+
// 🔍 成交量追踪日志:Rust 引擎接收
18+
let volume = if let Some(provided_volume) = order_book.volume {
19+
// 使用提供的成交量(来自 Binance K 线数据)
20+
// 注意:这是该 K 线的累计成交量,不是增量
21+
// 对于实时 K 线,我们需要计算增量
22+
web_sys::console::log_1(&format!(
23+
"[VOL追踪] 🦀 Rust process_tick: 使用提供的成交量={:.4}",
24+
provided_volume
25+
).into());
26+
provided_volume
27+
} else {
28+
// 回退到估算方法(用于 MOCK 数据)
29+
let estimated = self.tick_data.estimate_volume(order_book);
30+
web_sys::console::log_1(&format!(
31+
"[VOL追踪] 🦀 Rust process_tick: 成交量未提供,使用估算值={:.4}",
32+
estimated
33+
).into());
34+
estimated
35+
};
36+
1737
self.tick_data.push_volume(volume);
1838

1939
// 2. 更新 K 线

core/src/engine/tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ fn test_engine_estimate_volume() {
7272
price: 100.0,
7373
bids: vec![(99.0, 10.0)],
7474
asks: vec![(101.0, 20.0)],
75+
volume: None,
7576
};
7677
assert_eq!(engine.tick_data.estimate_volume(&order_book), 15.0);
7778
}
@@ -87,6 +88,7 @@ fn test_compute_all_indicators() {
8788
price: 40500.0,
8889
bids: vec![(40490.0, 1.0)],
8990
asks: vec![(40510.0, 1.0)],
91+
volume: None,
9092
};
9193

9294
let result = engine.compute_all_indicators(&order_book);

core/src/indicators/macd.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub fn calculate_macd(
7171
Some(MacdResult {
7272
dif,
7373
dea,
74-
hist: (dif - dea) * 2.0,
74+
hist: (dif - dea),
7575
})
7676
}
7777

@@ -92,7 +92,7 @@ mod tests {
9292
fn test_macd_histogram_formula() {
9393
let data: Vec<f64> = (1..=60).map(|x| 100.0 + x as f64).collect();
9494
let macd = calculate_macd(&data, 12, 26, 9).unwrap();
95-
let expected_hist = (macd.dif - macd.dea) * 2.0;
95+
let expected_hist = macd.dif - macd.dea;
9696
assert!((macd.hist - expected_hist).abs() < 0.0001);
9797
}
9898

core/src/indicators/rsi.rs

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,31 @@ pub fn calculate_rsi(data: &[f64], period: usize) -> Option<f64> {
1616
return None;
1717
}
1818

19-
let (gains, losses): (Vec<f64>, Vec<f64>) = data
20-
.windows(2)
21-
.map(|window| {
22-
let change = window[1] - window[0];
23-
if change > 0.0 {
24-
(change, 0.0)
25-
} else {
26-
(0.0, change.abs())
27-
}
28-
})
29-
.unzip();
30-
31-
let start = gains.len().saturating_sub(period);
32-
let recent_gains = &gains[start..];
33-
let recent_losses = &losses[start..];
19+
let mut gains_sum = 0.0;
20+
let mut losses_sum = 0.0;
21+
for i in 1..=period {
22+
let change = data[i] - data[i - 1];
23+
if change > 0.0 {
24+
gains_sum += change;
25+
} else {
26+
losses_sum += -change;
27+
}
28+
}
3429

35-
let avg_gain: f64 = recent_gains.iter().sum::<f64>() / period as f64;
36-
let avg_loss: f64 = recent_losses.iter().sum::<f64>() / period as f64;
30+
let mut avg_gain = gains_sum / period as f64;
31+
let mut avg_loss = losses_sum / period as f64;
32+
for i in (period + 1)..data.len() {
33+
let change = data[i] - data[i - 1];
34+
let gain = if change > 0.0 { change } else { 0.0 };
35+
let loss = if change < 0.0 { -change } else { 0.0 };
36+
avg_gain = (avg_gain * (period as f64 - 1.0) + gain) / period as f64;
37+
avg_loss = (avg_loss * (period as f64 - 1.0) + loss) / period as f64;
38+
}
3739

3840
if avg_loss == 0.0 {
41+
if avg_gain == 0.0 {
42+
return Some(50.0);
43+
}
3944
return Some(100.0);
4045
}
4146

@@ -67,7 +72,7 @@ mod tests {
6772
fn test_rsi_flat() {
6873
let data = vec![100.0; 20];
6974
let rsi = calculate_rsi(&data, 14);
70-
assert_eq!(rsi, Some(100.0));
75+
assert_eq!(rsi, Some(50.0));
7176
}
7277

7378
#[test]

core/src/models.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@ pub struct OrderBook {
306306

307307
/// 卖单列表 [(价格, 数量), ...] - 按价格升序
308308
pub asks: Vec<(f64, f64)>,
309+
310+
/// 成交量(可选,来自 Binance K 线数据的累计成交量)
311+
#[serde(default)]
312+
pub volume: Option<f64>,
309313
}
310314

311315
// ============================================================================
@@ -418,6 +422,7 @@ mod tests {
418422
price: 42000.5,
419423
bids: vec![(41999.0, 1.5), (41998.0, 2.0)],
420424
asks: vec![(42001.0, 1.2)],
425+
volume: None,
421426
};
422427

423428
assert_eq!(ob.price, 42000.5);

src/App.tsx

Lines changed: 109 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState, useCallback, useRef } from 'react';
2-
import { useWasmEngine } from './hooks/useWasmEngine';
1+
import { useState, useCallback, useRef, useEffect } from 'react';
2+
import { useWasmEngine, type DataSource } from './hooks/useWasmEngine';
33
import type {
44
Timeframe,
55
Indicator,
@@ -25,11 +25,44 @@ const MAIN_INDICATORS = ['MA', 'EMA', 'BOLL'] as const;
2525
/** 副图指标: VOL, MACD, RSI */
2626
const SUB_INDICATORS = ['VOL', 'MACD', 'RSI'] as const;
2727

28+
/** localStorage key for data source preference */
29+
const DATA_SOURCE_KEY = 'rustquantlab_data_source';
30+
31+
/**
32+
* 获取初始数据源
33+
* 优先级: URL 参数 > localStorage > 默认 mock
34+
*/
35+
function getInitialDataSource(): DataSource {
36+
// 检查 URL 参数
37+
const params = new URLSearchParams(window.location.search);
38+
const urlSource = params.get('source');
39+
if (urlSource === 'binance' || urlSource === 'mock') {
40+
return urlSource;
41+
}
42+
43+
// 检查 localStorage
44+
const savedSource = localStorage.getItem(DATA_SOURCE_KEY);
45+
if (savedSource === 'binance' || savedSource === 'mock') {
46+
return savedSource;
47+
}
48+
49+
// 默认使用模拟数据
50+
return 'mock';
51+
}
52+
2853
/* ============================================
2954
Main App Component (Composition Root)
3055
============================================ */
3156

3257
function App() {
58+
// ========== 数据源状态 ==========
59+
const [dataSource, setDataSource] = useState<DataSource>(getInitialDataSource);
60+
61+
// 持久化数据源偏好
62+
useEffect(() => {
63+
localStorage.setItem(DATA_SOURCE_KEY, dataSource);
64+
}, [dataSource]);
65+
3366
// ========== 统一的 Wasm 引擎 Hook ==========
3467
// 整合市场数据 + 交易状态,React 只做 UI 搬运工
3568
const {
@@ -51,6 +84,8 @@ function App() {
5184
priceColorClass,
5285
toggleFeed,
5386
setTimeframe,
87+
dataSource: engineDataSource,
88+
connectionStatus,
5489

5590
// 交易状态 (Rust 管理)
5691
tradingState,
@@ -65,7 +100,11 @@ function App() {
65100
setLeverage,
66101
cancelOrder,
67102
addMargin,
68-
} = useWasmEngine(100);
103+
} = useWasmEngine({
104+
tickInterval: 100,
105+
dataSource,
106+
historyCount: 5000, // 获取更多历史数据(5000 根 1m K 线 ≈ 3.5 天,足够计算所有指标)
107+
});
69108

70109
// ========== 图表引用 ==========
71110
const chartRef = useRef<KLineChartHandle>(null);
@@ -179,11 +218,14 @@ function App() {
179218
<div className="h-screen w-screen bg-[#161a1e] flex flex-col overflow-hidden">
180219
<Header
181220
isRunning={isRunning}
182-
onToggle={toggleFeed}
221+
onToggle={dataSource === 'mock' ? toggleFeed : undefined} // LIVE 模式下禁用切换
183222
price={latestData?.price}
184223
symbol={latestData?.symbol}
185224
priceTrend={priceTrend}
186225
priceColorClass={priceColorClass}
226+
dataSource={dataSource}
227+
onDataSourceChange={setDataSource}
228+
connectionStatus={connectionStatus}
187229
/>
188230

189231
{/* ========== 主内容区域 ========== */}
@@ -202,13 +244,24 @@ function App() {
202244
"
203245
>
204246
{/* 响应式网格容器 */}
247+
{/*
248+
- 移动端: 单列垂直布局
249+
- 平板端 (md): 2列 (图表 + 订单簿)
250+
- 桌面端 (xl):
251+
* 有交易表单 (MOCK): 3列 (图表 + 订单簿自适应 + 交易表单固定300px)
252+
* 无交易表单 (LIVE): 2列 (图表 + 订单簿自适应占满)
253+
*/}
205254
<div
206-
className="
255+
className={`
207256
flex flex-col
208-
md:grid md:grid-cols-[1fr_280px] md:h-full
209-
xl:grid-cols-[1fr_260px_300px]
257+
md:grid md:h-full
210258
gap-px bg-[#2b2f36]
211-
"
259+
${
260+
dataSource === 'mock'
261+
? 'md:grid-cols-[1fr_240px] xl:grid-cols-[1fr_240px_300px]'
262+
: 'md:grid-cols-[1fr_240px] xl:grid-cols-[1fr_auto]'
263+
}
264+
`}
212265
>
213266
{/* ========== 图表区域 (Chart + Toolbar + Stats) ========== */}
214267
<section className="flex flex-col min-h-0 bg-terminal-bg">
@@ -267,7 +320,13 @@ function App() {
267320

268321
{/* ========== 订单簿区域 ========== */}
269322
{/* 移动端高度压缩: 工具栏(28) + 表头(18) + 卖单(70) + Ticker(28) + 买单(70) = 214px */}
270-
<section className="h-[214px] md:h-full min-h-0 bg-terminal-bg border-t md:border-t-0 md:border-l border-[#2b2f36]">
323+
{/* MOCK 模式: 固定宽度 240px | LIVE 模式: 自动宽度占满剩余空间 */}
324+
<section
325+
className={`
326+
h-[214px] md:h-full min-h-0 bg-terminal-bg border-t md:border-t-0 md:border-l border-[#2b2f36]
327+
${dataSource === 'binance' ? 'xl:min-w-[200px]' : ''}
328+
`}
329+
>
271330
<OrderBook
272331
bids={latestData?.bids ?? []}
273332
asks={latestData?.asks ?? []}
@@ -278,46 +337,50 @@ function App() {
278337
/>
279338
</section>
280339

281-
{/* ========== 交易表单区域 (仅平板/桌面端显示) ========== */}
282-
{/* 🔴 使用 Wasm 交易状态 */}
283-
<section className="hidden xl:block h-full min-h-0 border-l border-[#2b2f36]">
284-
<TradeForm
285-
symbol="BTC"
286-
currentPrice={latestData?.price ?? 40000}
287-
// Wasm Trading State
288-
balance={tradingState?.balance ?? 10000}
289-
availableBalance={tradingState?.availableBalance ?? 10000}
290-
currentLeverage={tradingState?.leverage ?? 10}
291-
position={position}
292-
positions={tradingState?.positions ?? []}
293-
closedPositions={tradingState?.closedPositions ?? []}
294-
riskAssessment={riskAssessment}
295-
hasPosition={hasPosition}
296-
pendingOrders={pendingOrders}
297-
// Wasm Actions
298-
onPlaceOrder={placeOrder}
299-
onClosePosition={(positionId) => closePosition(positionId)}
300-
onSetLeverage={setLeverage}
301-
onCancelOrder={cancelOrder}
302-
onAddMargin={addMargin}
303-
/>
304-
</section>
340+
{/* ========== 交易表单区域 (仅 MOCK 模式下显示) ========== */}
341+
{/* LIVE 模式下隐藏交易表单,只展示数据 */}
342+
{dataSource === 'mock' && (
343+
<section className="hidden xl:block h-full min-h-0 border-l border-[#2b2f36]">
344+
<TradeForm
345+
symbol="BTC"
346+
currentPrice={latestData?.price ?? 40000}
347+
// Wasm Trading State
348+
balance={tradingState?.balance ?? 10000}
349+
availableBalance={tradingState?.availableBalance ?? 10000}
350+
currentLeverage={tradingState?.leverage ?? 10}
351+
position={position}
352+
positions={tradingState?.positions ?? []}
353+
closedPositions={tradingState?.closedPositions ?? []}
354+
riskAssessment={riskAssessment}
355+
hasPosition={hasPosition}
356+
pendingOrders={pendingOrders}
357+
// Wasm Actions
358+
onPlaceOrder={placeOrder}
359+
onClosePosition={(positionId) => closePosition(positionId)}
360+
onSetLeverage={setLeverage}
361+
onCancelOrder={cancelOrder}
362+
onAddMargin={addMargin}
363+
/>
364+
</section>
365+
)}
305366
</div>
306367
</main>
307368

308-
{/* ========== 移动端 Sticky 底部交易栏 ========== */}
309-
{/* 🔴 使用 Wasm placeOrder */}
310-
<MobileTradebar
311-
currentPrice={latestData?.price ?? 40000}
312-
onBuy={() => {
313-
// 移动端快速买入:市价单,固定数量
314-
placeOrder('LONG', 0.01, tradingState?.leverage ?? 10);
315-
}}
316-
onSell={() => {
317-
// 移动端快速卖出:市价单,固定数量
318-
placeOrder('SHORT', 0.01, tradingState?.leverage ?? 10);
319-
}}
320-
/>
369+
{/* ========== 移动端 Sticky 底部交易栏 (仅 MOCK 模式下显示) ========== */}
370+
{/* LIVE 模式下隐藏交易栏,只展示数据 */}
371+
{dataSource === 'mock' && (
372+
<MobileTradebar
373+
currentPrice={latestData?.price ?? 40000}
374+
onBuy={() => {
375+
// 移动端快速买入:市价单,固定数量
376+
placeOrder('LONG', 0.01, tradingState?.leverage ?? 10);
377+
}}
378+
onSell={() => {
379+
// 移动端快速卖出:市价单,固定数量
380+
placeOrder('SHORT', 0.01, tradingState?.leverage ?? 10);
381+
}}
382+
/>
383+
)}
321384
</div>
322385
);
323386
}

0 commit comments

Comments
 (0)