├── .gitignore ├── example_image ├── 主页.png ├── 详情页.png └── 配置页.png ├── index.html ├── vite.config.js ├── package.json ├── src ├── main.js ├── App.vue ├── components │ ├── WebSocketTest.vue │ └── SystemConfig.vue ├── views │ ├── Detail.vue │ └── Home.vue └── stores │ └── priceStore.js ├── SUMMARY.md ├── README.md └── ARCHITECTURE.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /example_image/主页.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoahMax1997/delta_price_vue/HEAD/example_image/主页.png -------------------------------------------------------------------------------- /example_image/详情页.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoahMax1997/delta_price_vue/HEAD/example_image/详情页.png -------------------------------------------------------------------------------- /example_image/配置页.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoahMax1997/delta_price_vue/HEAD/example_image/配置页.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 价差监控系统 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | resolve: { 8 | alias: { 9 | '@': resolve(__dirname, 'src') 10 | } 11 | }, 12 | server: { 13 | port: 5173, 14 | open: true, 15 | hmr: { 16 | port: 24678 // 使用不同的端口避免冲突 17 | } 18 | } 19 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delta-price-info", 3 | "version": "1.0.0", 4 | "description": "OKX和Binance价差监控系统", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "vue": "^3.3.4", 12 | "vue-router": "^4.2.4", 13 | "pinia": "^2.1.6", 14 | "axios": "^1.5.0", 15 | "echarts": "^5.4.3", 16 | "vue-echarts": "^6.6.1", 17 | "element-plus": "^2.3.9", 18 | "@element-plus/icons-vue": "^2.1.0" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-vue": "^4.3.4", 22 | "vite": "^4.4.9", 23 | "typescript": "^5.1.6", 24 | "vue-tsc": "^1.8.8" 25 | } 26 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | import ElementPlus from 'element-plus' 5 | import 'element-plus/dist/index.css' 6 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 7 | 8 | import App from './App.vue' 9 | import Home from './views/Home.vue' 10 | import Detail from './views/Detail.vue' 11 | 12 | const routes = [ 13 | { path: '/', component: Home }, 14 | { path: '/detail/:symbol', component: Detail, props: true } 15 | ] 16 | 17 | const router = createRouter({ 18 | history: createWebHistory(), 19 | routes 20 | }) 21 | 22 | const app = createApp(App) 23 | const pinia = createPinia() 24 | 25 | // 注册所有图标 26 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 27 | app.component(key, component) 28 | } 29 | 30 | app.use(pinia) 31 | app.use(router) 32 | app.use(ElementPlus) 33 | app.mount('#app') -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 价差监控系统 - 项目总结 2 | 3 | ## 🎯 项目目标 4 | 创建一个实时监控Binance和OKX交易所价差的Vue3前端应用,用于发现套利机会。 5 | 6 | ## ✅ 已实现功能 7 | 8 | ### 核心功能 9 | - ✅ **多交易对选择**: 支持20个常见USDT合约交易对 10 | - ✅ **实时WebSocket连接**: 同时连接Binance和OKX 11 | - ✅ **事件驱动匹配**: 数据到达即匹配,无延迟 12 | - ✅ **精确价差计算**: 基于时间同步的数据匹配 13 | - ✅ **历史数据图表**: 显示价差变化趋势 14 | - ✅ **套利机会提醒**: 自动识别大价差机会 15 | 16 | ### 技术特性 17 | - ✅ **异步队列架构**: 分离的Binance和OKX数据队列 18 | - ✅ **智能数据匹配**: 100ms时间差限制,确保数据同步 19 | - ✅ **自动数据清理**: 防止内存泄漏和数据积压 20 | - ✅ **响应式UI**: Vue3 + Pinia + Element Plus 21 | - ✅ **实时图表**: ECharts集成,支持缩放和平移 22 | 23 | ### 用户界面 24 | - ✅ **交易对选择器**: 多选下拉框,支持搜索 25 | - ✅ **实时数据表格**: 显示买一价、卖一价、总价值、价差率 26 | - ✅ **监控状态面板**: 连接状态、数据流监控、快速操作 27 | - ✅ **详情模态框**: 不断开连接查看详细信息 28 | - ✅ **套利建议**: 智能推荐操作方向 29 | 30 | ## 🔧 技术架构 31 | 32 | ### 前端技术栈 33 | - **Vue 3**: 组合式API + 响应式系统 34 | - **Pinia**: 状态管理 35 | - **Vue Router**: 路由管理 36 | - **Element Plus**: UI组件库 37 | - **ECharts**: 数据可视化 38 | - **Vite**: 构建工具 39 | 40 | ### 数据流架构 41 | ``` 42 | WebSocket数据 → 队列缓存 → 事件驱动匹配 → 价差计算 → UI更新 → 历史保存 43 | ``` 44 | 45 | ### 关键算法 46 | - **最新数据匹配**: 总是选择队列中最新的数据 47 | - **时间差控制**: 超过100ms的数据对被丢弃 48 | - **自动清理**: 过期数据和超量数据自动清理 49 | 50 | ## 📊 数据管理 51 | 52 | ### 实时数据 53 | - **Binance队列**: 最多100个数据点 54 | - **OKX队列**: 最多100个数据点 55 | - **匹配频率**: 事件驱动,无固定频率 56 | - **时间同步**: 100ms精度 57 | 58 | ### 历史数据 59 | - **存储容量**: 每个交易对最多2000个tick 60 | - **数据完整性**: 包含原始价格、数量、时间戳 61 | - **图表支持**: 支持全量数据显示和交互 62 | 63 | ## 🚀 性能优化 64 | 65 | ### 内存管理 66 | - 队列大小限制防止内存泄漏 67 | - 自动清理过期数据 68 | - 历史数据容量控制 69 | 70 | ### 响应性能 71 | - 事件驱动匹配,响应更快 72 | - Vue3响应式系统优化 73 | - 防抖机制避免频繁更新 74 | 75 | ### 网络优化 76 | - WebSocket长连接 77 | - 异步并行连接 78 | - 连接状态监控和自动重连 79 | 80 | ## 📈 监控指标 81 | 82 | ### 实时监控 83 | - WebSocket连接状态 84 | - 数据队列长度 85 | - 匹配成功率 86 | - 平均时间差 87 | 88 | ### 业务指标 89 | - 活跃交易对数量 90 | - 套利机会统计 91 | - 价差变化趋势 92 | - 数据更新频率 93 | 94 | ## 🎨 用户体验 95 | 96 | ### 界面设计 97 | - 现代化卡片布局 98 | - 响应式设计 99 | - 直观的颜色编码 100 | - 清晰的数据展示 101 | 102 | ### 交互体验 103 | - 一键开始监控 104 | - 实时状态反馈 105 | - 模态框详情查看 106 | - 智能套利提醒 107 | 108 | ## 🔮 扩展性 109 | 110 | ### 技术扩展 111 | - 易于添加新交易所 112 | - 支持更多交易对类型 113 | - 可扩展的数据分析功能 114 | - 模块化的组件设计 115 | 116 | ### 功能扩展 117 | - 自动交易接口 118 | - 更多技术指标 119 | - 历史数据分析 120 | - 风险管理工具 121 | 122 | ## 📝 使用说明 123 | 124 | 1. **启动应用**: `npm run dev` 125 | 2. **选择交易对**: 从下拉列表中选择要监控的交易对 126 | 3. **开始监控**: 点击"开始监控"按钮 127 | 4. **查看数据**: 实时表格显示价差数据 128 | 5. **详细分析**: 点击"详情"查看图表和历史数据 129 | 6. **套利操作**: 根据建议进行套利交易 130 | 131 | ## 🎯 项目亮点 132 | 133 | 1. **事件驱动架构**: 数据到达即处理,响应速度快 134 | 2. **精确时间同步**: 100ms精度确保价差计算准确性 135 | 3. **完整数据保存**: 匹配成功的数据完整保存用于分析 136 | 4. **智能数据清理**: 自动管理内存,防止数据积压 137 | 5. **用户友好界面**: 直观的操作和清晰的数据展示 138 | 139 | 这个系统为加密货币套利交易提供了可靠的数据基础和直观的操作界面! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OKX & Binance & Bitget 价差监控系统 2 | 3 | 一个基于Vue3的实时价差监控系统,用于监控OKX和Binance,Bitget交易所之间的价格差异。 4 | 5 | ## 功能特性 6 | 7 | - 🔄 **实时监控**: 通过WebSocket实时获取两个交易所的最佳买卖价 8 | - 📊 **价差计算**: 自动计算Binance买/OKX卖和Binance卖/OKX买的价差率 9 | - 📈 **图表展示**: 使用ECharts展示最近2000个tick的价差变化曲线 10 | - 🎯 **多交易对**: 支持同时监控多个交易对 11 | - 📱 **响应式设计**: 适配不同屏幕尺寸 12 | - 🎨 **现代UI**: 使用Element Plus组件库,界面美观 13 | 14 | ## 技术栈 15 | 16 | - **前端框架**: Vue 3 (Composition API) 17 | - **状态管理**: Pinia 18 | - **路由**: Vue Router 4 19 | - **UI组件**: Element Plus 20 | - **图表库**: ECharts + Vue-ECharts 21 | - **构建工具**: Vite 22 | - **WebSocket**: 原生WebSocket API 23 | 24 | ## 项目结构 25 | 26 | ``` 27 | src/ 28 | ├── main.js # 应用入口 29 | ├── App.vue # 根组件 30 | ├── stores/ 31 | │ └── priceStore.js # 价格数据状态管理 32 | └── views/ 33 | ├── Home.vue # 主页面 - 交易对选择和实时价差 34 | └── Detail.vue # 详情页面 - 价差变化曲线 35 | ``` 36 | 37 | ## 安装和运行 38 | 39 | 1. 安装依赖: 40 | ```bash 41 | npm install 42 | ``` 43 | 44 | 2. 启动开发服务器: 45 | ```bash 46 | npm run dev 47 | ``` 48 | 49 | 3. 构建生产版本: 50 | ```bash 51 | npm run build 52 | 53 | 4. 清除vue 54 | pkill -f node 55 | ``` 56 | 57 | ## 使用说明 58 | 59 | ### 主页面 60 | 1. 在下拉框中选择要监控的交易对(支持多选) 61 | 2. 点击"开始监控"按钮建立WebSocket连接 62 | 3. 实时查看价差数据表格,包括: 63 | - 两个交易所的买一价和卖一价 64 | - Binance买/OKX卖的价差率 65 | - Binance卖/OKX买的价差率 66 | 4. 点击"详情"按钮查看具体交易对的历史数据 67 | 68 | ### 详情页面 69 | 1. 查看当前实时价差数据 70 | 2. 观察价差变化曲线图 71 | 3. 查看统计信息(最大正价差、最大负价差、平均价差等) 72 | 4. 可以选择查看不同时间范围的数据(最近100/500/1000个tick或全部) 73 | 74 | ## WebSocket连接 75 | 76 | ### Binance WebSocket 77 | - 端点: `wss://stream.binance.com:9443/ws/` 78 | - 数据类型: bookTicker (最佳买卖价) 79 | - 格式: `{symbol}@bookTicker` 80 | 81 | ### OKX WebSocket 82 | - 端点: `wss://ws.okx.com:8443/ws/v5/public` 83 | - 频道: `bbo-tbt` (最佳买卖价) 84 | - 订阅格式: `{"op":"subscribe","args":[{"channel":"bbo-tbt","instId":"SYMBOL"}]}` 85 | 86 | ## 价差计算公式 87 | 88 | - **Binance买/OKX卖价差率** = (Binance卖一价 - OKX买一价) / OKX买一价 × 100% 89 | - **Binance卖/OKX买价差率** = (OKX卖一价 - Binance买一价) / Binance买一价 × 100% 90 | 91 | ## 支持的交易对 92 | 93 | 系统预设了20个常见的交易对,这些都是OKX和Binance共同支持的: 94 | 95 | - BTCUSDT, ETHUSDT, BNBUSDT, ADAUSDT, XRPUSDT 96 | - SOLUSDT, DOTUSDT, DOGEUSDT, AVAXUSDT, SHIBUSDT 97 | - MATICUSDT, LTCUSDT, LINKUSDT, UNIUSDT, ATOMUSDT 98 | - ETCUSDT, XLMUSDT, BCHUSDT, FILUSDT, TRXUSDT 99 | 100 | ## 注意事项 101 | 102 | 1. WebSocket连接需要稳定的网络环境 103 | 2. 系统会自动保存最近2000个tick的历史数据 104 | 3. 价差数据仅供参考,实际交易请考虑手续费、滑点等因素 105 | 4. 建议在生产环境中添加错误重连机制 106 | 107 | ## 开发计划 108 | 109 | - [ ] 添加更多交易所支持 110 | - [ ] 增加价差预警功能 111 | - [ ] 添加数据导出功能 112 | - [ ] 优化WebSocket重连机制 113 | - [ ] 添加历史数据持久化存储 114 | 115 | ## 界面展示 116 | ![主页展示 ](./example_image/主页.png) 117 | ![详情页展示 ](./example_image/详情页.png) 118 | ![配置页展示 ](./example_image/配置页.png) 119 | 120 | ## 打赏地址 哈哈和别人一样也写一个 121 | - solana 4HLYN6b3fUAQwJnvz73DknV3MCBHkmomP1WK1TMcbPZC 122 | - bsc 0x4443f4426bdfc6e919b5ad84ea2a49e06da52888 -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # 价差监控系统架构说明 2 | 3 | ## 新的异步队列架构 4 | 5 | ### 概述 6 | 系统现在使用异步队列和数据匹配机制来确保价差计算的准确性和时效性。 7 | 8 | ### 核心组件 9 | 10 | #### 1. 数据队列 11 | - **Binance队列**: 存储来自Binance WebSocket的实时数据 12 | - **OKX队列**: 存储来自OKX WebSocket的实时数据 13 | - **队列大小限制**: 每个队列最多保留100个数据点 14 | - **自动清理**: 超过1秒的过期数据会被自动清理 15 | 16 | #### 2. 异步协程 17 | 18 | ##### Binance数据协程 19 | - 连接到Binance合约WebSocket: `wss://fstream.binance.com/ws/{symbol}@bookTicker` 20 | - 接收实时的bestbid和bestask数据 21 | - 将数据添加到Binance队列 22 | 23 | ##### OKX数据协程 24 | - 连接到OKX WebSocket: `wss://ws.okx.com:8443/ws/v5/public` 25 | - 订阅`bbo-tbt`频道获取实时数据 26 | - 将数据添加到OKX队列 27 | 28 | ##### 数据匹配协程 29 | - **事件驱动匹配**: 每当有新数据入队时立即尝试匹配 30 | - 总是选择两个队列中最新的数据进行匹配 31 | - 时间差超过100ms的数据对会被丢弃 32 | - 匹配成功后计算价差并更新UI 33 | - **备用清理**: 每1秒运行一次,清理过期数据和进行备用匹配检查 34 | 35 | #### 3. 数据匹配算法 36 | 37 | ```javascript 38 | // 获取两个队列中最新的数据 39 | const latestBinance = binanceData[binanceData.length - 1] 40 | const latestOkx = okxData[okxData.length - 1] 41 | 42 | // 检查时间差 43 | const timeDiff = Math.abs(latestBinance.timestamp - latestOkx.timestamp) 44 | 45 | if (timeDiff <= maxTimeDiff) { 46 | // 匹配成功,移除已使用的数据 47 | binanceQueue.pop() 48 | okxQueue.pop() 49 | return { binance: latestBinance, okx: latestOkx, timeDiff } 50 | } else { 51 | // 时间差太大,丢弃较旧的数据 52 | if (latestBinance.timestamp > latestOkx.timestamp) { 53 | okxQueue.pop() // 丢弃OKX旧数据 54 | } else { 55 | binanceQueue.pop() // 丢弃Binance旧数据 56 | } 57 | } 58 | ``` 59 | 60 | #### 4. 价差计算 61 | 只有匹配成功的数据对才会用于价差计算: 62 | 63 | - **Binance买/OKX卖**: `(Binance卖一价 - OKX买一价) / OKX买一价 × 100%` 64 | - **Binance卖/OKX买**: `(OKX卖一价 - Binance买一价) / Binance买一价 × 100%` 65 | 66 | ### 优势 67 | 68 | 1. **事件驱动**: 有数据就立即匹配,响应更快 69 | 2. **最新数据优先**: 总是使用最新的数据进行匹配 70 | 3. **数据准确性**: 避免使用过期或不匹配的数据 71 | 4. **实时性**: 数据到达即匹配,无延迟 72 | 5. **容错性**: 自动清理过期数据,防止内存泄漏 73 | 6. **可扩展性**: 队列机制可以轻松扩展到更多交易所 74 | 75 | ### 配置参数 76 | 77 | - `maxQueueSize`: 100 (每个队列最大数据点数) 78 | - `maxTimeDiff`: 100ms (最大允许时间差) 79 | - `cleanupInterval`: 1000ms (清理协程运行频率) 80 | - `maxHistorySize`: 2000 (历史数据最大保留数量) 81 | 82 | ### 数据流程 83 | 84 | ``` 85 | Binance WebSocket → Binance队列 → 立即尝试匹配 ↘ 86 | → 价差计算 → UI更新 → 历史数据保存 87 | OKX WebSocket → OKX队列 → 立即尝试匹配 ↗ 88 | ``` 89 | 90 | ### 匹配流程 91 | 92 | 1. **数据入队**: WebSocket接收到数据后立即加入对应队列 93 | 2. **立即匹配**: 数据入队后立即调用匹配函数 94 | 3. **最新数据**: 总是选择两个队列中最新的数据 95 | 4. **时间检查**: 检查两个数据的时间差是否在100ms内 96 | 5. **匹配成功**: 时间差合格则匹配成功,移除已使用数据 97 | 6. **匹配失败**: 时间差过大则丢弃较旧的数据 98 | 7. **价差计算**: 匹配成功的数据用于计算价差 99 | 8. **UI更新**: 实时更新界面显示 100 | 101 | ### 监控指标 102 | 103 | - 队列长度监控 104 | - 匹配成功率 105 | - 平均时间差 106 | - 数据丢弃率 107 | - WebSocket连接状态 108 | 109 | 这种架构确保了价差数据的准确性和实时性,为套利交易提供了可靠的数据基础。 110 | 111 | ### 数据管理机制 112 | 113 | #### 队列数据清理 114 | - **匹配成功**: 匹配成功的数据从两个队列中移除,避免重复使用 115 | - **时间差过大**: 丢弃较旧的数据,保持数据时效性 116 | - **过期清理**: 自动清理超过1秒的过期数据 117 | - **队列大小限制**: 每个队列最多保留100个数据点 118 | 119 | #### 历史数据保存 120 | 匹配成功的数据会被保存为完整的历史记录,包含: 121 | 122 | ```javascript 123 | { 124 | timestamp: 1748497345636, // 匹配时间戳 125 | buyBinanceSellOkx: 0.023961, // Binance买/OKX卖价差 126 | sellBinanceBuyOkx: 0.01482, // Binance卖/OKX买价差 127 | timeDiff: 45, // 数据时间差(ms) 128 | binanceData: { // Binance原始数据 129 | bidPrice: 50001.23, 130 | askPrice: 50006.45, 131 | bidQty: 1.0, 132 | askQty: 1.0, 133 | timestamp: 1748497345591 134 | }, 135 | okxData: { // OKX原始数据 136 | bidPrice: 50002.15, 137 | askPrice: 50007.32, 138 | bidQty: 1.0, 139 | askQty: 1.0, 140 | timestamp: 1748497345636 141 | } 142 | } 143 | ``` 144 | 145 | #### 数据流转过程 146 | 147 | 1. **数据入队**: WebSocket数据进入对应队列 148 | 2. **立即匹配**: 尝试与另一个队列的最新数据匹配 149 | 3. **匹配检查**: 验证时间差是否在100ms内 150 | 4. **成功处理**: 151 | - 从队列中移除匹配的数据 152 | - 计算价差 153 | - 保存完整历史记录 154 | - 更新UI显示 155 | 5. **失败处理**: 丢弃较旧的数据 156 | 6. **定期清理**: 清理过期和超量数据 -------------------------------------------------------------------------------- /src/components/WebSocketTest.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 112 | 113 | -------------------------------------------------------------------------------- /src/views/Detail.vue: -------------------------------------------------------------------------------- 1 | 134 | 135 | 400 | 401 | -------------------------------------------------------------------------------- /src/components/SystemConfig.vue: -------------------------------------------------------------------------------- 1 | 198 | 199 | 331 | 332 | -------------------------------------------------------------------------------- /src/stores/priceStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, computed, reactive, nextTick } from 'vue' 3 | 4 | export const usePriceStore = defineStore('price', () => { 5 | // 状态 6 | const selectedSymbols = ref([]) 7 | const selectedExchangePair = ref('binance-okx') // 默认选择binance-okx 8 | const priceData = ref({}) 9 | const tickHistory = ref({}) 10 | const wsConnections = ref({}) 11 | const isConnected = ref(false) 12 | 13 | // 数据队列 - 每个交易对独立管理 14 | const symbolQueues = ref({}) // { symbol: { binance: [], okx: [], bitget: [], matcher: setInterval, stats: {} } } 15 | 16 | // 实时统计数据(从网站打开开始计算) 17 | const realtimeStats = ref({}) // { symbol: { maxBuyBinanceSellOkx: number, maxSellBinanceBuyOkx: number, maxNegativeSpread: number } } 18 | 19 | // 匹配统计数据 20 | const matchStats = ref({ 21 | successfulMatches: 0, // 成功匹配次数 22 | discardedMatches: 0, // 丢弃匹配次数 23 | totalBinanceQueue: 0, // Binance总队列长度 24 | totalOKXQueue: 0, // OKX总队列长度 25 | totalBitgetQueue: 0, // Bitget总队列长度 26 | queueDetails: {} // 每个交易对的队列详情 { symbol: { binance: length, okx: length, bitget: length } } 27 | }) 28 | 29 | // 合约大小映射 - 存储每个交易对的合约大小 30 | const contractSizes = ref({}) // { symbol: contractSize } 31 | 32 | // Funding Rate数据 - 存储每个交易对的资金费率信息 33 | const fundingRates = ref({}) // { symbol: { binance: {...}, okx: {...}, bitget: {...}, lastUpdate: timestamp } } 34 | 35 | // 系统配置参数 - 可动态调整 36 | const systemConfig = ref({ 37 | maxTimeDiff: 1000, // 最大时间差(ms) - 匹配时允许的最大时间差 38 | dataExpirationTime: 1000, // 数据过期时间(ms) - 超过此时间的数据将被清理 39 | cleanupInterval: 5000, // 清理间隔(ms) - 多久执行一次过期数据清理 40 | maxQueueSize: 100, // 队列最大容量 - 每个队列最多保留的数据点数 41 | historyRetentionCount: 2000, // 历史数据保留数量 - 最多保留的历史tick数 42 | timeMatchingMode: 'receiveTime', // 时间匹配模式: 'originalTimestamp' | 'receiveTime' 43 | maxLocalTimeDiff: 50 // 最大本地时间差(ms) - 原始时间戳与本地时间的最大允许差异 44 | }) 45 | 46 | // 协程控制参数(从systemConfig获取,保持向后兼容) 47 | const maxQueueSize = computed(() => systemConfig.value.maxQueueSize) 48 | const maxTimeDiff = computed(() => systemConfig.value.maxTimeDiff) 49 | 50 | // 常见的交易对列表(CCXT格式) 51 | const availableSymbols = ref([ 52 | 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'ADA/USDT:USDT', 'XRP/USDT:USDT', 53 | 'SOL/USDT:USDT', 'DOT/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'SHIB/USDT:USDT', 54 | 'MATIC/USDT:USDT', 'LTC/USDT:USDT', 'LINK/USDT:USDT', 'UNI/USDT:USDT', 'ATOM/USDT:USDT', 55 | 'ETC/USDT:USDT', 'XLM/USDT:USDT', 'BCH/USDT:USDT', 'FIL/USDT:USDT', 'TRX/USDT:USDT' 56 | ]) 57 | 58 | // 初始化队列 59 | const initializeQueues = () => { 60 | selectedSymbols.value.forEach(symbol => { 61 | if (!symbolQueues.value[symbol]) { 62 | symbolQueues.value[symbol] = { 63 | binance: [], 64 | okx: [], 65 | bitget: [], 66 | matcher: null, 67 | stats: { 68 | successfulMatches: 0, 69 | discardedMatches: 0, 70 | totalBinanceDataReceived: 0, 71 | totalOKXDataReceived: 0, 72 | totalBitgetDataReceived: 0, 73 | lastMatchTime: null, 74 | avgTimeDiff: 0, 75 | matchTimeDiffs: [] 76 | } 77 | } 78 | // 为每个交易对启动独立的匹配协程 79 | startSymbolMatcher(symbol) 80 | } 81 | }) 82 | } 83 | 84 | // 更新队列统计数据 85 | const updateQueueStats = () => { 86 | let totalBinance = 0 87 | let totalOKX = 0 88 | let totalBitget = 0 89 | const queueDetails = {} 90 | 91 | Object.keys(symbolQueues.value).forEach(symbol => { 92 | const binanceLength = symbolQueues.value[symbol]?.binance?.length || 0 93 | const okxLength = symbolQueues.value[symbol]?.okx?.length || 0 94 | const bitgetLength = symbolQueues.value[symbol]?.bitget?.length || 0 95 | 96 | totalBinance += binanceLength 97 | totalOKX += okxLength 98 | totalBitget += bitgetLength 99 | 100 | queueDetails[symbol] = { 101 | binance: binanceLength, 102 | okx: okxLength, 103 | bitget: bitgetLength 104 | } 105 | }) 106 | 107 | matchStats.value.totalBinanceQueue = totalBinance 108 | matchStats.value.totalOKXQueue = totalOKX 109 | matchStats.value.totalBitgetQueue = totalBitget 110 | matchStats.value.queueDetails = queueDetails 111 | } 112 | 113 | // 添加数据到Binance队列 114 | const addToBinanceQueue = (symbol, data) => { 115 | if (!symbolQueues.value[symbol]) { 116 | symbolQueues.value[symbol] = { 117 | binance: [], 118 | okx: [], 119 | matcher: null, 120 | stats: { 121 | successfulMatches: 0, 122 | discardedMatches: 0, 123 | totalBinanceDataReceived: 0, 124 | totalOKXDataReceived: 0, 125 | lastMatchTime: null, 126 | avgTimeDiff: 0, 127 | matchTimeDiffs: [] 128 | } 129 | } 130 | // 为新的交易对启动独立协程 131 | startSymbolMatcher(symbol) 132 | } 133 | 134 | const queueData = { 135 | symbol, 136 | exchange: 'binance', 137 | bidPrice: parseFloat(data.b), 138 | askPrice: parseFloat(data.a), 139 | bidQty: parseFloat(data.B), 140 | askQty: parseFloat(data.A), 141 | originalTimestamp: data.E || data.T || null, // 交易所原始时间戳 (Event time 或 Transaction time) 142 | receiveTime: Date.now(), // 本地接收时间 143 | timestamp: Date.now() // 保持向后兼容 144 | } 145 | 146 | symbolQueues.value[symbol].binance.push(queueData) 147 | 148 | // 更新统计数据 149 | symbolQueues.value[symbol].stats.totalBinanceDataReceived++ 150 | 151 | // 保持队列大小 152 | if (symbolQueues.value[symbol].binance.length > maxQueueSize.value) { 153 | symbolQueues.value[symbol].binance.shift() 154 | } 155 | 156 | console.log(`[${symbol}] Binance数据入队: 队列长度 ${symbolQueues.value[symbol].binance.length}`) 157 | 158 | // 更新队列统计 159 | updateQueueStats() 160 | 161 | // 🔄 简化:每次新数据到达就立即尝试匹配 162 | tryMatchPair(symbol) 163 | } 164 | 165 | // 添加数据到OKX队列 166 | const addToOKXQueue = (symbol, data) => { 167 | console.log('=== addToOKXQueue 被调用 ===') 168 | console.log('symbol:', symbol) 169 | console.log('data:', data) 170 | console.log('当前contractSizes:', contractSizes.value) 171 | console.log('==============================') 172 | 173 | if (!symbolQueues.value[symbol]) { 174 | symbolQueues.value[symbol] = { 175 | binance: [], 176 | okx: [], 177 | bitget: [], 178 | matcher: null, 179 | stats: { 180 | successfulMatches: 0, 181 | discardedMatches: 0, 182 | totalBinanceDataReceived: 0, 183 | totalOKXDataReceived: 0, 184 | totalBitgetDataReceived: 0, 185 | lastMatchTime: null, 186 | avgTimeDiff: 0, 187 | matchTimeDiffs: [] 188 | } 189 | } 190 | // 为新的交易对启动独立协程 191 | startSymbolMatcher(symbol) 192 | } 193 | 194 | // 获取该交易对的合约大小,默认为1 195 | const contractSize = contractSizes.value[symbol] || 1 196 | 197 | // 获取原始数量数据 198 | const originalBidQty = parseFloat(data.bids[0][1]) 199 | const originalAskQty = parseFloat(data.asks[0][1]) 200 | 201 | // 计算应用合约大小后的数量 202 | const adjustedBidQty = originalBidQty * contractSize 203 | const adjustedAskQty = originalAskQty * contractSize 204 | 205 | const queueData = { 206 | symbol, 207 | exchange: 'okx', 208 | bidPrice: parseFloat(data.bids[0][0]), 209 | askPrice: parseFloat(data.asks[0][0]), 210 | bidQty: adjustedBidQty, 211 | askQty: adjustedAskQty, 212 | originalTimestamp: data.ts ? parseInt(data.ts) : null, // OKX原始时间戳 213 | receiveTime: Date.now(), // 本地接收时间 214 | timestamp: Date.now() // 保持向后兼容 215 | } 216 | 217 | symbolQueues.value[symbol].okx.push(queueData) 218 | 219 | // 更新统计数据 220 | symbolQueues.value[symbol].stats.totalOKXDataReceived++ 221 | 222 | // 保持队列大小 223 | if (symbolQueues.value[symbol].okx.length > maxQueueSize.value) { 224 | symbolQueues.value[symbol].okx.shift() 225 | } 226 | 227 | console.log(`[${symbol}] OKX数据入队详情:`, { 228 | contractSize: contractSize, 229 | originalBidQty: originalBidQty, 230 | originalAskQty: originalAskQty, 231 | adjustedBidQty: adjustedBidQty, 232 | adjustedAskQty: adjustedAskQty, 233 | contractSizesAvailable: Object.keys(contractSizes.value).length, 234 | queueLength: symbolQueues.value[symbol].okx.length 235 | }) 236 | 237 | // 更新队列统计 238 | updateQueueStats() 239 | 240 | // 🔄 简化:每次新数据到达就立即尝试匹配 241 | tryMatchPair(symbol) 242 | } 243 | 244 | // 匹配最佳数据对 245 | const matchBestPair = (symbol) => { 246 | if (!selectedExchangePair.value) return null 247 | 248 | const [firstExchange, secondExchange] = selectedExchangePair.value.split('-') 249 | const firstData = symbolQueues.value[symbol]?.[firstExchange] || [] 250 | const secondData = symbolQueues.value[symbol]?.[secondExchange] || [] 251 | 252 | if (firstData.length === 0 || secondData.length === 0) { 253 | return null 254 | } 255 | 256 | // 获取两个队列中最新的数据(队列末尾的数据) 257 | const latestFirst = firstData[firstData.length - 1] 258 | const latestSecond = secondData[secondData.length - 1] 259 | 260 | // 根据配置选择使用哪个时间进行匹配 261 | let firstTime, secondTime 262 | 263 | if (systemConfig.value.timeMatchingMode === 'originalTimestamp') { 264 | // 使用交易所原始时间戳 265 | firstTime = latestFirst.originalTimestamp 266 | secondTime = latestSecond.originalTimestamp 267 | 268 | // 如果原始时间戳不可用,回退到接收时间 269 | if (!firstTime || !secondTime) { 270 | console.warn(`${symbol} 原始时间戳不可用,回退到接收时间匹配`) 271 | firstTime = latestFirst.receiveTime 272 | secondTime = latestSecond.receiveTime 273 | } 274 | } else { 275 | // 使用本地接收时间(默认) 276 | firstTime = latestFirst.receiveTime 277 | secondTime = latestSecond.receiveTime 278 | } 279 | 280 | // 新增:检查原始时间戳与当前本地时间的差异 281 | const currentLocalTime = Date.now() 282 | const maxLocalTimeDiff = systemConfig.value.maxLocalTimeDiff // 使用配置的阈值 283 | 284 | // 检查第一个交易所原始时间戳延迟 285 | if (latestFirst.originalTimestamp) { 286 | const firstDelay = Math.abs(currentLocalTime - latestFirst.originalTimestamp) 287 | if (firstDelay > maxLocalTimeDiff) { 288 | console.log(`❌ ${firstExchange}数据过旧: ${symbol}, 原始时间戳延迟: ${firstDelay}ms (超过${maxLocalTimeDiff}ms阈值), 放弃匹配`) 289 | return null 290 | } 291 | } 292 | 293 | // 检查第二个交易所原始时间戳延迟 294 | if (latestSecond.originalTimestamp) { 295 | const secondDelay = Math.abs(currentLocalTime - latestSecond.originalTimestamp) 296 | if (secondDelay > maxLocalTimeDiff) { 297 | console.log(`❌ ${secondExchange}数据过旧: ${symbol}, 原始时间戳延迟: ${secondDelay}ms (超过${maxLocalTimeDiff}ms阈值), 放弃匹配`) 298 | return null 299 | } 300 | } 301 | 302 | // 检查时间差是否在允许范围内 303 | const timeDiff = Math.abs(firstTime - secondTime) 304 | 305 | if (timeDiff <= maxTimeDiff.value) { 306 | // 时间差在允许范围内,进行匹配 307 | const bestMatch = { 308 | [firstExchange]: latestFirst, 309 | [secondExchange]: latestSecond, 310 | // 保持向后兼容性 311 | binance: firstExchange === 'binance' ? latestFirst : secondExchange === 'binance' ? latestSecond : null, 312 | okx: firstExchange === 'okx' ? latestFirst : secondExchange === 'okx' ? latestSecond : null, 313 | bitget: firstExchange === 'bitget' ? latestFirst : secondExchange === 'bitget' ? latestSecond : null, 314 | timeDiff 315 | } 316 | 317 | // 🔄 新策略:匹配成功后不删除数据,保留在队列中 318 | // symbolQueues.value[symbol][firstExchange].pop() // 不再删除 319 | // symbolQueues.value[symbol][secondExchange].pop() // 不再删除 320 | 321 | // 更新全局成功匹配统计 322 | matchStats.value.successfulMatches++ 323 | 324 | // 更新该交易对的成功匹配统计 325 | symbolQueues.value[symbol].stats.successfulMatches++ 326 | symbolQueues.value[symbol].stats.lastMatchTime = Date.now() 327 | 328 | // 记录时间差用于计算平均值 329 | symbolQueues.value[symbol].stats.matchTimeDiffs.push(timeDiff) 330 | if (symbolQueues.value[symbol].stats.matchTimeDiffs.length > 100) { 331 | symbolQueues.value[symbol].stats.matchTimeDiffs.shift() // 只保留最近100个时间差 332 | } 333 | 334 | // 计算平均时间差 335 | const timeDiffs = symbolQueues.value[symbol].stats.matchTimeDiffs 336 | symbolQueues.value[symbol].stats.avgTimeDiff = timeDiffs.reduce((sum, diff) => sum + diff, 0) / timeDiffs.length 337 | 338 | updateQueueStats() 339 | 340 | console.log(`✅ 匹配成功: ${symbol}, 时间差: ${timeDiff}ms (${systemConfig.value.timeMatchingMode}), 数据保留在队列中, ${firstExchange}队列: ${symbolQueues.value[symbol][firstExchange].length}, ${secondExchange}队列: ${symbolQueues.value[symbol][secondExchange].length}`) 341 | console.log(` 匹配时间详情: ${firstExchange}(${systemConfig.value.timeMatchingMode}): ${new Date(firstTime).toLocaleTimeString()}.${firstTime % 1000}, ${secondExchange}(${systemConfig.value.timeMatchingMode}): ${new Date(secondTime).toLocaleTimeString()}.${secondTime % 1000}`) 342 | return bestMatch 343 | } else { 344 | // 🔄 简化:时间差太大时也不删除数据,只记录统计 345 | // 更新全局丢弃匹配统计 346 | matchStats.value.discardedMatches++ 347 | // 更新该交易对的丢弃匹配统计 348 | symbolQueues.value[symbol].stats.discardedMatches++ 349 | updateQueueStats() 350 | 351 | if (firstTime > secondTime) { 352 | console.log(`⏰ 时间差过大(${timeDiff}ms): ${symbol}, ${secondExchange}数据较旧 (${systemConfig.value.timeMatchingMode}: ${new Date(secondTime).toLocaleTimeString()}), 等待更新数据`) 353 | } else { 354 | console.log(`⏰ 时间差过大(${timeDiff}ms): ${symbol}, ${firstExchange}数据较旧 (${systemConfig.value.timeMatchingMode}: ${new Date(firstTime).toLocaleTimeString()}), 等待更新数据`) 355 | } 356 | } 357 | 358 | // 清理过期数据(使用配置的过期时间) 359 | const now = Date.now() 360 | const originalFirstLength = symbolQueues.value[symbol][firstExchange].length 361 | const originalSecondLength = symbolQueues.value[symbol][secondExchange].length 362 | 363 | symbolQueues.value[symbol][firstExchange] = firstData.filter(item => now - item.timestamp < systemConfig.value.dataExpirationTime) 364 | symbolQueues.value[symbol][secondExchange] = secondData.filter(item => now - item.timestamp < systemConfig.value.dataExpirationTime) 365 | 366 | const cleanedFirst = originalFirstLength - symbolQueues.value[symbol][firstExchange].length 367 | const cleanedSecond = originalSecondLength - symbolQueues.value[symbol][secondExchange].length 368 | 369 | if (cleanedFirst > 0 || cleanedSecond > 0) { 370 | console.log(`🧹 清理过期数据: ${symbol}, ${firstExchange}清理${cleanedFirst}个, ${secondExchange}清理${cleanedSecond}个`) 371 | } 372 | 373 | return null 374 | } 375 | 376 | // 添加数据到Bitget队列 377 | const addToBitgetQueue = (symbol, data) => { 378 | console.log('=== addToBitgetQueue 被调用 ===') 379 | console.log('symbol:', symbol) 380 | console.log('data:', data) 381 | 382 | if (!symbolQueues.value[symbol]) { 383 | symbolQueues.value[symbol] = { 384 | binance: [], 385 | okx: [], 386 | bitget: [], 387 | matcher: null, 388 | stats: { 389 | successfulMatches: 0, 390 | discardedMatches: 0, 391 | totalBinanceDataReceived: 0, 392 | totalOKXDataReceived: 0, 393 | totalBitgetDataReceived: 0, 394 | lastMatchTime: null, 395 | avgTimeDiff: 0, 396 | matchTimeDiffs: [] 397 | } 398 | } 399 | // 为新的交易对启动独立协程 400 | startSymbolMatcher(symbol) 401 | } 402 | 403 | // 获取该交易对的合约大小,默认为1 404 | const contractSize = contractSizes.value[symbol] || 1 405 | 406 | // Bitget数据格式类似OKX 407 | const originalBidQty = parseFloat(data.bids[0][1]) 408 | const originalAskQty = parseFloat(data.asks[0][1]) 409 | 410 | // 计算应用合约大小后的数量 411 | const adjustedBidQty = originalBidQty * contractSize 412 | const adjustedAskQty = originalAskQty * contractSize 413 | 414 | const queueData = { 415 | symbol, 416 | exchange: 'bitget', 417 | bidPrice: parseFloat(data.bids[0][0]), 418 | askPrice: parseFloat(data.asks[0][0]), 419 | bidQty: adjustedBidQty, 420 | askQty: adjustedAskQty, 421 | originalTimestamp: data.ts ? parseInt(data.ts) : null, // Bitget原始时间戳 422 | receiveTime: Date.now(), // 本地接收时间 423 | timestamp: Date.now() // 保持向后兼容 424 | } 425 | 426 | symbolQueues.value[symbol].bitget.push(queueData) 427 | 428 | // 更新统计数据 429 | symbolQueues.value[symbol].stats.totalBitgetDataReceived++ 430 | 431 | // 保持队列大小 432 | if (symbolQueues.value[symbol].bitget.length > maxQueueSize.value) { 433 | symbolQueues.value[symbol].bitget.shift() 434 | } 435 | 436 | console.log(`[${symbol}] Bitget数据入队详情:`, { 437 | contractSize: contractSize, 438 | originalBidQty: originalBidQty, 439 | originalAskQty: originalAskQty, 440 | adjustedBidQty: adjustedBidQty, 441 | adjustedAskQty: adjustedAskQty, 442 | queueLength: symbolQueues.value[symbol].bitget.length 443 | }) 444 | 445 | // 更新队列统计 446 | updateQueueStats() 447 | 448 | // 🔄 简化:每次新数据到达就立即尝试匹配 449 | tryMatchPair(symbol) 450 | } 451 | 452 | // 尝试匹配数据对 453 | const tryMatchPair = (symbol) => { 454 | const match = matchBestPair(symbol) 455 | 456 | if (match && selectedExchangePair.value) { 457 | const [firstExchange, secondExchange] = selectedExchangePair.value.split('-') 458 | 459 | // 根据选择的交易所组合动态更新价格数据 460 | const firstData = match[firstExchange] 461 | const secondData = match[secondExchange] 462 | 463 | if (firstData && secondData) { 464 | // 更新实时价格数据 465 | priceData.value[`${firstExchange}_${symbol}`] = firstData 466 | priceData.value[`${secondExchange}_${symbol}`] = secondData 467 | 468 | // 计算价差 469 | const spread = calculateSpread(firstData, secondData) 470 | 471 | if (spread) { 472 | // 更新实时统计数据 473 | updateRealtimeStats(symbol, spread) 474 | 475 | // 保存匹配成功的完整数据作为历史记录 476 | const historyData = { 477 | timestamp: spread.timestamp, 478 | buyFirstSellSecond: spread.buyFirstSellSecond, 479 | sellFirstBuySecond: spread.sellFirstBuySecond, 480 | // 保持向后兼容性 481 | buyBinanceSellOkx: spread.buyBinanceSellOkx, 482 | sellBinanceBuyOkx: spread.sellBinanceBuyOkx, 483 | timeDiff: match.timeDiff, 484 | // 保存原始价格数据用于图表详细显示 485 | firstExchangeData: { 486 | exchange: firstExchange, 487 | bidPrice: firstData.bidPrice, 488 | askPrice: firstData.askPrice, 489 | bidQty: firstData.bidQty, 490 | askQty: firstData.askQty, 491 | timestamp: firstData.timestamp 492 | }, 493 | secondExchangeData: { 494 | exchange: secondExchange, 495 | bidPrice: secondData.bidPrice, 496 | askPrice: secondData.askPrice, 497 | bidQty: secondData.bidQty, 498 | askQty: secondData.askQty, 499 | timestamp: secondData.timestamp 500 | }, 501 | // 保持向后兼容性 502 | binanceData: firstExchange === 'binance' ? { 503 | bidPrice: firstData.bidPrice, 504 | askPrice: firstData.askPrice, 505 | bidQty: firstData.bidQty, 506 | askQty: firstData.askQty, 507 | timestamp: firstData.timestamp 508 | } : secondExchange === 'binance' ? { 509 | bidPrice: secondData.bidPrice, 510 | askPrice: secondData.askPrice, 511 | bidQty: secondData.bidQty, 512 | askQty: secondData.askQty, 513 | timestamp: secondData.timestamp 514 | } : null, 515 | okxData: firstExchange === 'okx' ? { 516 | bidPrice: firstData.bidPrice, 517 | askPrice: firstData.askPrice, 518 | bidQty: firstData.bidQty, 519 | askQty: firstData.askQty, 520 | timestamp: firstData.timestamp 521 | } : secondExchange === 'okx' ? { 522 | bidPrice: secondData.bidPrice, 523 | askPrice: secondData.askPrice, 524 | bidQty: secondData.bidQty, 525 | askQty: secondData.askQty, 526 | timestamp: secondData.timestamp 527 | } : null 528 | } 529 | 530 | // 保存历史数据 531 | saveTickHistory(symbol, historyData) 532 | 533 | console.log(`价差计算完成: ${symbol}`, { 534 | [`buy${firstExchange.charAt(0).toUpperCase() + firstExchange.slice(1)}Sell${secondExchange.charAt(0).toUpperCase() + secondExchange.slice(1)}`]: spread.buyFirstSellSecond, 535 | [`sell${firstExchange.charAt(0).toUpperCase() + firstExchange.slice(1)}Buy${secondExchange.charAt(0).toUpperCase() + secondExchange.slice(1)}`]: spread.sellFirstBuySecond, 536 | timeDiff: match.timeDiff, 537 | [`${firstExchange}Bid`]: firstData.bidPrice, 538 | [`${firstExchange}Ask`]: firstData.askPrice, 539 | [`${secondExchange}Bid`]: secondData.bidPrice, 540 | [`${secondExchange}Ask`]: secondData.askPrice 541 | }) 542 | } 543 | } 544 | } 545 | } 546 | 547 | // 更新实时统计数据 548 | const updateRealtimeStats = (symbol, spread) => { 549 | if (!realtimeStats.value[symbol]) { 550 | realtimeStats.value[symbol] = { 551 | maxBuyBinanceSellOkx: -Infinity, // Binance买/OKX卖的最大价差 552 | maxSellBinanceBuyOkx: -Infinity, // Binance卖/OKX买的最大价差 553 | maxNegativeSpread: Infinity, 554 | startTime: Date.now() 555 | } 556 | } 557 | 558 | const stats = realtimeStats.value[symbol] 559 | 560 | // 分别更新两个方向的最大价差 561 | if (spread.buyBinanceSellOkx > stats.maxBuyBinanceSellOkx) { 562 | stats.maxBuyBinanceSellOkx = spread.buyBinanceSellOkx 563 | console.log(`${symbol} 新的Binance买/OKX卖最大价差: ${spread.buyBinanceSellOkx.toFixed(4)}%`) 564 | } 565 | 566 | if (spread.sellBinanceBuyOkx > stats.maxSellBinanceBuyOkx) { 567 | stats.maxSellBinanceBuyOkx = spread.sellBinanceBuyOkx 568 | console.log(`${symbol} 新的Binance卖/OKX买最大价差: ${spread.sellBinanceBuyOkx.toFixed(4)}%`) 569 | } 570 | 571 | // 更新最大负价差(保持原逻辑) 572 | const currentMinSpread = Math.min(spread.buyBinanceSellOkx, spread.sellBinanceBuyOkx) 573 | if (currentMinSpread < stats.maxNegativeSpread) { 574 | stats.maxNegativeSpread = currentMinSpread 575 | console.log(`${symbol} 新的最大负价差: ${currentMinSpread.toFixed(4)}%`) 576 | } 577 | } 578 | 579 | // 计算价差率 580 | const calculateSpread = (firstExchangeData, secondExchangeData) => { 581 | if (!firstExchangeData || !secondExchangeData) return null 582 | 583 | const firstBid = firstExchangeData.bidPrice 584 | const firstAsk = firstExchangeData.askPrice 585 | const secondBid = secondExchangeData.bidPrice 586 | const secondAsk = secondExchangeData.askPrice 587 | 588 | // 第一个交易所买第二个交易所卖的价差率 589 | const buyFirstSellSecond = ((firstAsk - secondBid) / secondBid * 100) 590 | // 第一个交易所卖第二个交易所买的价差率 591 | const sellFirstBuySecond = ((secondAsk - firstBid) / firstBid * 100) 592 | 593 | return { 594 | buyFirstSellSecond: parseFloat(buyFirstSellSecond.toFixed(6)), 595 | sellFirstBuySecond: parseFloat(sellFirstBuySecond.toFixed(6)), 596 | // 保持向后兼容性的旧属性名 597 | buyBinanceSellOkx: parseFloat(buyFirstSellSecond.toFixed(6)), 598 | sellBinanceBuyOkx: parseFloat(sellFirstBuySecond.toFixed(6)), 599 | timestamp: Math.max(firstExchangeData.timestamp, secondExchangeData.timestamp) 600 | } 601 | } 602 | 603 | // 为单个交易对启动匹配协程 604 | const startSymbolMatcher = (symbol) => { 605 | if (!symbolQueues.value[symbol]) { 606 | symbolQueues.value[symbol] = { 607 | binance: [], 608 | okx: [], 609 | bitget: [], 610 | matcher: null, 611 | stats: {} 612 | } 613 | } 614 | 615 | // 如果已经有协程在运行,先停止它 616 | if (symbolQueues.value[symbol].matcher) { 617 | clearInterval(symbolQueues.value[symbol].matcher) 618 | } 619 | 620 | // 🔄 协程只负责清理过期数据 621 | symbolQueues.value[symbol].matcher = setInterval(() => { 622 | const now = Date.now() 623 | 624 | // 清理过期数据(使用配置的过期时间) 625 | if (symbolQueues.value[symbol].binance) { 626 | const originalBinanceLength = symbolQueues.value[symbol].binance.length 627 | symbolQueues.value[symbol].binance = symbolQueues.value[symbol].binance.filter(item => now - item.timestamp < systemConfig.value.dataExpirationTime) 628 | const cleanedBinance = originalBinanceLength - symbolQueues.value[symbol].binance.length 629 | 630 | if (cleanedBinance > 0) { 631 | console.log(`[${symbol}] 定时清理过期Binance数据: ${cleanedBinance}个`) 632 | } 633 | } 634 | 635 | if (symbolQueues.value[symbol].okx) { 636 | const originalOkxLength = symbolQueues.value[symbol].okx.length 637 | symbolQueues.value[symbol].okx = symbolQueues.value[symbol].okx.filter(item => now - item.timestamp < systemConfig.value.dataExpirationTime) 638 | const cleanedOkx = originalOkxLength - symbolQueues.value[symbol].okx.length 639 | 640 | if (cleanedOkx > 0) { 641 | console.log(`[${symbol}] 定时清理过期OKX数据: ${cleanedOkx}个`) 642 | } 643 | } 644 | 645 | if (symbolQueues.value[symbol].bitget) { 646 | const originalBitgetLength = symbolQueues.value[symbol].bitget.length 647 | symbolQueues.value[symbol].bitget = symbolQueues.value[symbol].bitget.filter(item => now - item.timestamp < systemConfig.value.dataExpirationTime) 648 | const cleanedBitget = originalBitgetLength - symbolQueues.value[symbol].bitget.length 649 | 650 | if (cleanedBitget > 0) { 651 | console.log(`[${symbol}] 定时清理过期Bitget数据: ${cleanedBitget}个`) 652 | } 653 | } 654 | 655 | // 更新队列统计 656 | updateQueueStats() 657 | 658 | }, systemConfig.value.cleanupInterval) // 使用配置的清理间隔 659 | 660 | console.log(`[${symbol}] 清理协程已启动,匹配策略:任一队列更新即触发匹配,数据不删除`) 661 | } 662 | 663 | // 停止指定交易对的匹配协程并清理相关数据 664 | const stopSymbolMatcher = (symbol) => { 665 | if (symbolQueues.value[symbol]?.matcher) { 666 | clearInterval(symbolQueues.value[symbol].matcher) 667 | symbolQueues.value[symbol].matcher = null 668 | console.log(`[${symbol}] 独立匹配协程已停止`) 669 | } 670 | } 671 | 672 | // 完全清理指定交易对的所有数据(协程、队列、价格数据、历史数据、统计数据) 673 | const clearSymbolData = (symbol) => { 674 | // 停止协程 675 | stopSymbolMatcher(symbol) 676 | 677 | // 删除队列数据 678 | delete symbolQueues.value[symbol] 679 | 680 | // 清理价格数据 681 | delete priceData.value[`binance_${symbol}`] 682 | delete priceData.value[`okx_${symbol}`] 683 | delete priceData.value[`bitget_${symbol}`] 684 | 685 | // 清理历史数据 686 | delete tickHistory.value[symbol] 687 | 688 | // 清理实时统计数据 689 | delete realtimeStats.value[symbol] 690 | 691 | console.log(`[${symbol}] 交易对数据已完全清理:队列、协程、价格数据、历史数据、统计数据`) 692 | } 693 | 694 | // 启动所有交易对的匹配协程 695 | const startAllMatchers = () => { 696 | selectedSymbols.value.forEach(symbol => { 697 | startSymbolMatcher(symbol) 698 | }) 699 | console.log('所有交易对的独立匹配协程已启动') 700 | } 701 | 702 | // 停止所有交易对的匹配协程 703 | const stopAllMatchers = () => { 704 | Object.keys(symbolQueues.value).forEach(symbol => { 705 | stopSymbolMatcher(symbol) 706 | }) 707 | console.log('所有交易对的独立匹配协程已停止') 708 | } 709 | 710 | // 数据匹配协程(现在主要用于清理过期数据) 711 | const startMatcherCoroutine = () => { 712 | // 这个函数现在只是启动所有交易对的独立协程 713 | startAllMatchers() 714 | } 715 | 716 | // 停止匹配协程 717 | const stopMatcherCoroutine = () => { 718 | // 这个函数现在只是停止所有交易对的独立协程 719 | stopAllMatchers() 720 | } 721 | 722 | // 获取格式化的价格数据 723 | const getFormattedPriceData = computed(() => { 724 | if (!selectedExchangePair.value) return [] 725 | 726 | const [firstExchange, secondExchange] = selectedExchangePair.value.split('-') 727 | 728 | const result = selectedSymbols.value.map(symbol => { 729 | const firstKey = `${firstExchange}_${symbol}` 730 | const secondKey = `${secondExchange}_${symbol}` 731 | const firstData = priceData.value[firstKey] 732 | const secondData = priceData.value[secondKey] 733 | 734 | const spread = calculateSpread(firstData, secondData) 735 | 736 | const rowData = { 737 | symbol, 738 | [firstExchange]: firstData, 739 | [secondExchange]: secondData, 740 | // 保持向后兼容性 741 | binance: firstExchange === 'binance' ? firstData : secondExchange === 'binance' ? secondData : null, 742 | okx: firstExchange === 'okx' ? firstData : secondExchange === 'okx' ? secondData : null, 743 | bitget: firstExchange === 'bitget' ? firstData : secondExchange === 'bitget' ? secondData : null, 744 | spread, 745 | lastUpdate: Math.max( 746 | firstData?.timestamp || 0, 747 | secondData?.timestamp || 0 748 | ) 749 | } 750 | 751 | return rowData 752 | }) 753 | 754 | return result 755 | }) 756 | 757 | // 设置选中的交易所组合 758 | const setSelectedExchangePair = (exchangePair) => { 759 | console.log(`切换交易所组合: ${selectedExchangePair.value} -> ${exchangePair}`) 760 | selectedExchangePair.value = exchangePair 761 | 762 | // 断开现有连接 763 | if (isConnected.value) { 764 | disconnectWebSockets() 765 | } 766 | 767 | // 清空当前选择的交易对和相关数据 768 | selectedSymbols.value = [] 769 | priceData.value = {} 770 | tickHistory.value = {} 771 | realtimeStats.value = {} 772 | symbolQueues.value = {} 773 | 774 | console.log(`交易所组合已切换到: ${exchangePair}`) 775 | } 776 | 777 | // 设置选中的交易对 778 | const setSelectedSymbols = async (symbols) => { 779 | // 先停止所有现有的协程 780 | stopAllMatchers() 781 | 782 | selectedSymbols.value = symbols 783 | 784 | // 重置实时统计数据 785 | realtimeStats.value = {} 786 | 787 | // 重置匹配统计数据 788 | matchStats.value = { 789 | successfulMatches: 0, 790 | discardedMatches: 0, 791 | totalBinanceQueue: 0, 792 | totalOKXQueue: 0, 793 | queueDetails: {} 794 | } 795 | 796 | // 清理不再使用的交易对的所有相关数据 797 | const symbolsSet = new Set(symbols) 798 | Object.keys(symbolQueues.value).forEach(symbol => { 799 | if (!symbolsSet.has(symbol)) { 800 | // 使用新的统一清理函数 801 | clearSymbolData(symbol) 802 | } 803 | }) 804 | 805 | // 清理priceData中可能残留的数据(额外安全检查) 806 | Object.keys(priceData.value).forEach(key => { 807 | const symbol = key.replace(/^(binance_|okx_)/, '') 808 | if (!symbolsSet.has(symbol)) { 809 | delete priceData.value[key] 810 | console.log(`[${symbol}] 清理残留的价格数据: ${key}`) 811 | } 812 | }) 813 | 814 | // 清理tickHistory中可能残留的数据 815 | Object.keys(tickHistory.value).forEach(symbol => { 816 | if (!symbolsSet.has(symbol)) { 817 | delete tickHistory.value[symbol] 818 | console.log(`[${symbol}] 清理残留的历史数据`) 819 | } 820 | }) 821 | 822 | console.log(`队列清理完成,当前选中交易对: [${symbols.join(', ')}]`) 823 | console.log(`剩余队列数量: ${Object.keys(symbolQueues.value).length}`) 824 | console.log(`剩余价格数据: ${Object.keys(priceData.value).length}`) 825 | console.log(`剩余历史数据: ${Object.keys(tickHistory.value).length}`) 826 | 827 | // 初始化队列(只为新的交易对创建队列和启动协程) 828 | initializeQueues() 829 | 830 | // 异步连接WebSocket 831 | await connectWebSockets() 832 | 833 | // 获取所有交易对的Funding Rate 834 | if (symbols.length > 0) { 835 | setTimeout(() => { 836 | fetchAllFundingRates() 837 | }, 1000) // 延迟1秒获取,确保其他初始化完成 838 | } 839 | } 840 | 841 | // 连接WebSocket 842 | const connectWebSockets = async () => { 843 | // 断开现有连接 844 | disconnectWebSockets() 845 | 846 | if (selectedSymbols.value.length === 0) return 847 | if (!selectedExchangePair.value) { 848 | console.warn('未选择交易所组合,无法连接WebSocket') 849 | return 850 | } 851 | 852 | const [firstExchange, secondExchange] = selectedExchangePair.value.split('-') 853 | console.log(`开始连接WebSocket,交易所组合: ${firstExchange} ↔ ${secondExchange},交易对:`, selectedSymbols.value) 854 | 855 | try { 856 | // 初始化队列(协程已在initializeQueues中启动) 857 | initializeQueues() 858 | 859 | // 根据选择的交易所组合,动态连接对应的WebSocket 860 | const connectionPromises = [] 861 | 862 | if (firstExchange === 'binance' || secondExchange === 'binance') { 863 | connectionPromises.push(connectBinanceWS()) 864 | } 865 | if (firstExchange === 'okx' || secondExchange === 'okx') { 866 | connectionPromises.push(connectOKXWS()) 867 | } 868 | if (firstExchange === 'bitget' || secondExchange === 'bitget') { 869 | connectionPromises.push(connectBitgetWS()) 870 | } 871 | 872 | // 并行连接所需的WebSocket 873 | await Promise.all(connectionPromises) 874 | 875 | // 启动状态检查 876 | startStatusCheck() 877 | 878 | // 启动资金费率定时更新 879 | startFundingRateUpdates() 880 | 881 | isConnected.value = true 882 | console.log(`${firstExchange} ↔ ${secondExchange} WebSocket连接完成,各交易对的独立协程已启动`) 883 | } catch (error) { 884 | console.error(`${firstExchange} ↔ ${secondExchange} WebSocket连接失败:`, error) 885 | isConnected.value = false 886 | throw error 887 | } 888 | } 889 | 890 | // 连接Binance WebSocket 891 | const connectBinanceWS = () => { 892 | return new Promise((resolve, reject) => { 893 | const promises = selectedSymbols.value.map(ccxtSymbol => { 894 | return new Promise((resolveSymbol, rejectSymbol) => { 895 | const binanceSymbol = convertToBinanceFormat(ccxtSymbol) 896 | if (!binanceSymbol) { 897 | rejectSymbol(new Error(`无法转换交易对格式: ${ccxtSymbol}`)) 898 | return 899 | } 900 | 901 | const wsUrl = `wss://fstream.binance.com/ws/${binanceSymbol.toLowerCase()}@bookTicker` 902 | console.log(`连接Binance合约WebSocket: ${wsUrl}`) 903 | const ws = new WebSocket(wsUrl) 904 | 905 | let isResolved = false 906 | 907 | ws.onopen = () => { 908 | console.log(`Binance WebSocket连接成功: ${ccxtSymbol} (${binanceSymbol})`) 909 | if (!isResolved) { 910 | isResolved = true 911 | resolveSymbol(ws) 912 | } 913 | } 914 | 915 | ws.onmessage = (event) => { 916 | try { 917 | const data = JSON.parse(event.data) 918 | const binanceSymbol = data.s 919 | const ccxtSymbol = convertFromBinanceFormat(binanceSymbol) 920 | 921 | console.log(`Binance数据接收: ${ccxtSymbol}, 买一: ${data.b}, 卖一: ${data.a}, 原始时间戳: ${data.E || data.T || 'N/A'}`) 922 | 923 | // 添加到Binance队列 924 | addToBinanceQueue(ccxtSymbol, data) 925 | 926 | } catch (error) { 927 | console.error('Binance数据解析错误:', error) 928 | } 929 | } 930 | 931 | ws.onerror = (error) => { 932 | console.error(`Binance WebSocket错误 ${ccxtSymbol}:`, error) 933 | if (!isResolved) { 934 | isResolved = true 935 | rejectSymbol(error) 936 | } 937 | } 938 | 939 | ws.onclose = (event) => { 940 | console.log(`Binance WebSocket断开: ${ccxtSymbol}, 代码: ${event.code}`) 941 | if (event.code !== 1000) { 942 | isConnected.value = false 943 | } 944 | } 945 | 946 | // 存储WebSocket连接 947 | if (!wsConnections.value.binance) { 948 | wsConnections.value.binance = {} 949 | } 950 | wsConnections.value.binance[ccxtSymbol] = ws 951 | 952 | // 设置超时 953 | setTimeout(() => { 954 | if (!isResolved) { 955 | isResolved = true 956 | rejectSymbol(new Error(`Binance WebSocket连接超时: ${ccxtSymbol}`)) 957 | } 958 | }, 10000) 959 | }) 960 | }) 961 | 962 | Promise.all(promises) 963 | .then(() => resolve()) 964 | .catch(reject) 965 | }) 966 | } 967 | 968 | // 将CCXT格式转换为Binance格式 969 | const convertToBinanceFormat = (ccxtSymbol) => { 970 | // BTC/USDT:USDT -> BTCUSDT (合约格式) 971 | if (!ccxtSymbol || typeof ccxtSymbol !== 'string') { 972 | console.warn('Invalid CCXT symbol:', ccxtSymbol) 973 | return '' 974 | } 975 | // 对于合约交易对,格式是 BASE + QUOTE 976 | const parts = ccxtSymbol.split('/') 977 | if (parts.length !== 2) return '' 978 | 979 | const base = parts[0] 980 | const quote = parts[1].split(':')[0] // 移除 :USDT 部分 981 | return base + quote // 例如: BTCUSDT 982 | } 983 | 984 | // 将CCXT格式转换为OKX格式 985 | const convertToOKXFormat = (ccxtSymbol) => { 986 | // BTC/USDT:USDT -> BTC-USDT-SWAP 987 | if (!ccxtSymbol || typeof ccxtSymbol !== 'string') { 988 | console.warn('Invalid CCXT symbol:', ccxtSymbol) 989 | return '' 990 | } 991 | const parts = ccxtSymbol.split('/') 992 | if (parts.length !== 2) return '' 993 | 994 | const base = parts[0] 995 | const quote = parts[1].split(':')[0] 996 | return `${base}-${quote}-SWAP` 997 | } 998 | 999 | // 将OKX格式转换为CCXT格式 1000 | const convertFromOKXFormat = (okxSymbol) => { 1001 | // BTC-USDT-SWAP -> BTC/USDT:USDT 1002 | if (!okxSymbol || typeof okxSymbol !== 'string') { 1003 | console.warn('Invalid OKX symbol:', okxSymbol) 1004 | return '' 1005 | } 1006 | const parts = okxSymbol.replace('-SWAP', '').split('-') 1007 | if (parts.length !== 2) return '' 1008 | 1009 | return `${parts[0]}/${parts[1]}:${parts[1]}` 1010 | } 1011 | 1012 | // 将Binance格式转换为CCXT格式 1013 | const convertFromBinanceFormat = (binanceSymbol) => { 1014 | // BTCUSDT -> BTC/USDT:USDT (合约格式) 1015 | if (!binanceSymbol || typeof binanceSymbol !== 'string') { 1016 | console.warn('Invalid Binance symbol:', binanceSymbol) 1017 | return '' 1018 | } 1019 | 1020 | // 对于USDT合约,假设都是以USDT结尾 1021 | if (binanceSymbol.endsWith('USDT')) { 1022 | const base = binanceSymbol.replace('USDT', '') 1023 | return `${base}/USDT:USDT` 1024 | } 1025 | 1026 | // 如果不是USDT结尾,尝试其他常见的quote货币 1027 | const commonQuotes = ['BUSD', 'BTC', 'ETH', 'BNB'] 1028 | for (const quote of commonQuotes) { 1029 | if (binanceSymbol.endsWith(quote)) { 1030 | const base = binanceSymbol.replace(quote, '') 1031 | return `${base}/${quote}:${quote}` 1032 | } 1033 | } 1034 | 1035 | return binanceSymbol 1036 | } 1037 | 1038 | // 将CCXT格式转换为Bitget格式 1039 | const convertToBitgetFormat = (ccxtSymbol) => { 1040 | // BTC/USDT:USDT -> BTCUSDT (v2格式,不加_UMCBL) 1041 | if (!ccxtSymbol || typeof ccxtSymbol !== 'string') { 1042 | console.warn('Invalid CCXT symbol:', ccxtSymbol) 1043 | return '' 1044 | } 1045 | const parts = ccxtSymbol.split('/') 1046 | if (parts.length !== 2) return '' 1047 | 1048 | const base = parts[0] 1049 | const quote = parts[1].split(':')[0] 1050 | return `${base}${quote}` // v2格式,不加_UMCBL后缀 1051 | } 1052 | 1053 | // 将Bitget格式转换为CCXT格式 1054 | const convertFromBitgetFormat = (bitgetSymbol) => { 1055 | // BTCUSDT -> BTC/USDT:USDT (v2格式,不需要处理_UMCBL) 1056 | if (!bitgetSymbol || typeof bitgetSymbol !== 'string') { 1057 | console.warn('Invalid Bitget symbol:', bitgetSymbol) 1058 | return '' 1059 | } 1060 | 1061 | // 尝试手动分离常见的quote货币 1062 | if (bitgetSymbol.endsWith('USDT')) { 1063 | const base = bitgetSymbol.replace('USDT', '') 1064 | return `${base}/USDT:USDT` 1065 | } 1066 | 1067 | // 如果不是USDT结尾,尝试其他常见的quote货币 1068 | const commonQuotes = ['BUSD', 'BTC', 'ETH', 'BNB'] 1069 | for (const quote of commonQuotes) { 1070 | if (bitgetSymbol.endsWith(quote)) { 1071 | const base = bitgetSymbol.replace(quote, '') 1072 | return `${base}/${quote}:${quote}` 1073 | } 1074 | } 1075 | 1076 | return bitgetSymbol 1077 | } 1078 | 1079 | // 连接OKX WebSocket 1080 | const connectOKXWS = () => { 1081 | return new Promise((resolve, reject) => { 1082 | const ws = new WebSocket('wss://ws.okx.com:8443/ws/v5/public') 1083 | 1084 | let isResolved = false 1085 | let isSubscribed = false 1086 | 1087 | ws.onopen = () => { 1088 | console.log('OKX WebSocket连接成功') 1089 | 1090 | // 订阅bbo-tbt数据,转换交易对格式 1091 | const subscribeMsg = { 1092 | op: 'subscribe', 1093 | args: selectedSymbols.value.map(ccxtSymbol => ({ 1094 | channel: 'bbo-tbt', 1095 | instId: convertToOKXFormat(ccxtSymbol) 1096 | })) 1097 | } 1098 | 1099 | console.log('OKX订阅消息:', subscribeMsg) 1100 | ws.send(JSON.stringify(subscribeMsg)) 1101 | } 1102 | 1103 | ws.onmessage = (event) => { 1104 | try { 1105 | const response = JSON.parse(event.data) 1106 | // console.log('OKX WebSocket消息:', response) 1107 | 1108 | // 处理订阅确认消息 1109 | if (response.event) { 1110 | if (response.event === 'subscribe') { 1111 | console.log('OKX订阅成功:', response) 1112 | if (!isResolved) { 1113 | isResolved = true 1114 | isSubscribed = true 1115 | resolve() 1116 | } 1117 | } else if (response.event === 'error') { 1118 | console.error('OKX订阅错误:', response) 1119 | if (!isResolved) { 1120 | isResolved = true 1121 | reject(new Error(`OKX订阅失败: ${response.msg}`)) 1122 | } 1123 | } 1124 | return 1125 | } 1126 | 1127 | // 处理数据消息 1128 | if (response.arg && response.data && Array.isArray(response.data)) { 1129 | const instId = response.arg.instId 1130 | 1131 | response.data.forEach(item => { 1132 | if (!item.bids || !item.asks || item.bids.length === 0 || item.asks.length === 0) { 1133 | console.warn('OKX数据不完整:', item) 1134 | return 1135 | } 1136 | 1137 | const ccxtSymbol = convertFromOKXFormat(instId) 1138 | 1139 | if (!ccxtSymbol) { 1140 | console.warn('无法转换OKX交易对格式:', instId) 1141 | return 1142 | } 1143 | 1144 | console.log(`OKX数据接收: ${ccxtSymbol}, 买一: ${item.bids[0][0]}, 卖一: ${item.asks[0][0]}, 原始时间戳: ${item.ts || 'N/A'}`) 1145 | 1146 | // 添加到OKX队列 1147 | addToOKXQueue(ccxtSymbol, item) 1148 | }) 1149 | } 1150 | } catch (error) { 1151 | console.error('OKX数据解析错误:', error) 1152 | } 1153 | } 1154 | 1155 | ws.onerror = (error) => { 1156 | console.error('OKX WebSocket错误:', error) 1157 | if (!isResolved) { 1158 | isResolved = true 1159 | reject(error) 1160 | } 1161 | } 1162 | 1163 | ws.onclose = (event) => { 1164 | console.log(`OKX WebSocket断开, 代码: ${event.code}`) 1165 | if (event.code !== 1000) { 1166 | isConnected.value = false 1167 | } 1168 | } 1169 | 1170 | wsConnections.value.okx = ws 1171 | 1172 | // 设置连接超时 1173 | setTimeout(() => { 1174 | if (!isResolved) { 1175 | isResolved = true 1176 | reject(new Error('OKX WebSocket连接超时')) 1177 | } 1178 | }, 10000) 1179 | }) 1180 | } 1181 | 1182 | // 连接Bitget WebSocket 1183 | const connectBitgetWS = () => { 1184 | return new Promise((resolve, reject) => { 1185 | const ws = new WebSocket('wss://ws.bitget.com/v2/ws/public') 1186 | 1187 | let isResolved = false 1188 | let isSubscribed = false 1189 | 1190 | ws.onopen = () => { 1191 | console.log('Bitget WebSocket连接成功') 1192 | 1193 | // 订阅orderbook数据,转换交易对格式 1194 | const subscribeMsg = { 1195 | op: 'subscribe', 1196 | args: selectedSymbols.value.map(ccxtSymbol => ({ 1197 | instType: 'USDT-FUTURES', 1198 | channel: 'books', 1199 | instId: convertToBitgetFormat(ccxtSymbol) 1200 | })) 1201 | } 1202 | 1203 | console.log('Bitget订阅消息:', subscribeMsg) 1204 | ws.send(JSON.stringify(subscribeMsg)) 1205 | } 1206 | 1207 | ws.onmessage = (event) => { 1208 | try { 1209 | const response = JSON.parse(event.data) 1210 | // console.log('Bitget WebSocket消息:', response) 1211 | 1212 | // 处理订阅确认消息 1213 | if (response.event) { 1214 | if (response.event === 'subscribe') { 1215 | console.log('Bitget订阅成功:', response) 1216 | if (!isResolved) { 1217 | isResolved = true 1218 | isSubscribed = true 1219 | resolve() 1220 | } 1221 | } else if (response.event === 'error') { 1222 | console.error('Bitget订阅错误:', response) 1223 | if (!isResolved) { 1224 | isResolved = true 1225 | reject(new Error(`Bitget订阅失败: ${response.msg}`)) 1226 | } 1227 | } 1228 | return 1229 | } 1230 | 1231 | // 处理数据消息 1232 | if (response.arg && response.data && Array.isArray(response.data)) { 1233 | const instId = response.arg.instId 1234 | 1235 | response.data.forEach(item => { 1236 | if (!item.bids || !item.asks || item.bids.length === 0 || item.asks.length === 0) { 1237 | console.warn('Bitget数据不完整:', item) 1238 | return 1239 | } 1240 | 1241 | const ccxtSymbol = convertFromBitgetFormat(instId) 1242 | 1243 | if (!ccxtSymbol) { 1244 | console.warn('无法转换Bitget交易对格式:', instId) 1245 | return 1246 | } 1247 | 1248 | console.log(`Bitget数据接收: ${ccxtSymbol}, 买一: ${item.bids[0][0]}, 卖一: ${item.asks[0][0]}, 原始时间戳: ${item.ts || 'N/A'}`) 1249 | 1250 | // 添加到Bitget队列 1251 | addToBitgetQueue(ccxtSymbol, item) 1252 | }) 1253 | } 1254 | } catch (error) { 1255 | console.error('Bitget数据解析错误:', error) 1256 | } 1257 | } 1258 | 1259 | ws.onerror = (error) => { 1260 | console.error('Bitget WebSocket错误:', error) 1261 | if (!isResolved) { 1262 | isResolved = true 1263 | reject(error) 1264 | } 1265 | } 1266 | 1267 | ws.onclose = (event) => { 1268 | console.log(`Bitget WebSocket断开, 代码: ${event.code}`) 1269 | if (event.code !== 1000) { 1270 | isConnected.value = false 1271 | } 1272 | } 1273 | 1274 | wsConnections.value.bitget = ws 1275 | 1276 | // 设置连接超时 1277 | setTimeout(() => { 1278 | if (!isResolved) { 1279 | isResolved = true 1280 | reject(new Error('Bitget WebSocket连接超时')) 1281 | } 1282 | }, 10000) 1283 | }) 1284 | } 1285 | 1286 | // 保存tick历史数据 1287 | const saveTickHistory = (symbol, historyData) => { 1288 | if (!tickHistory.value[symbol]) { 1289 | tickHistory.value[symbol] = [] 1290 | } 1291 | 1292 | if (historyData) { 1293 | // 确保历史数据包含所有必要字段 1294 | const tickData = { 1295 | timestamp: historyData.timestamp, 1296 | buyBinanceSellOkx: historyData.buyBinanceSellOkx, 1297 | sellBinanceBuyOkx: historyData.sellBinanceBuyOkx, 1298 | timeDiff: historyData.timeDiff || 0, 1299 | // 保存完整的价格数据 1300 | binanceData: historyData.binanceData || null, 1301 | okxData: historyData.okxData || null 1302 | } 1303 | 1304 | tickHistory.value[symbol].push(tickData) 1305 | 1306 | console.log(`历史数据已保存: ${symbol}, 当前历史数据数量: ${tickHistory.value[symbol].length}`) 1307 | console.log('完整历史数据:', { 1308 | timestamp: new Date(tickData.timestamp).toLocaleTimeString(), 1309 | buyBinanceSellOkx: tickData.buyBinanceSellOkx, 1310 | sellBinanceBuyOkx: tickData.sellBinanceBuyOkx, 1311 | timeDiff: tickData.timeDiff, 1312 | binanceBid: tickData.binanceData?.bidPrice, 1313 | binanceAsk: tickData.binanceData?.askPrice, 1314 | okxBid: tickData.okxData?.bidPrice, 1315 | okxAsk: tickData.okxData?.askPrice 1316 | }) 1317 | 1318 | // 只保留配置的历史数据数量 1319 | if (tickHistory.value[symbol].length > systemConfig.value.historyRetentionCount) { 1320 | const removed = tickHistory.value[symbol].splice(0, tickHistory.value[symbol].length - systemConfig.value.historyRetentionCount) 1321 | console.log(`历史数据超限,移除了 ${removed.length} 个旧数据点`) 1322 | } 1323 | } 1324 | } 1325 | 1326 | // 断开WebSocket连接 1327 | const disconnectWebSockets = () => { 1328 | // 停止所有交易对的匹配协程 1329 | stopAllMatchers() 1330 | 1331 | // 停止状态检查 1332 | stopStatusCheck() 1333 | 1334 | // 停止资金费率定时更新 1335 | stopFundingRateUpdates() 1336 | 1337 | // 清空所有队列和协程 1338 | symbolQueues.value = {} 1339 | 1340 | // 重置实时统计数据 1341 | realtimeStats.value = {} 1342 | 1343 | // 重置匹配统计数据 1344 | matchStats.value = { 1345 | successfulMatches: 0, 1346 | discardedMatches: 0, 1347 | totalBinanceQueue: 0, 1348 | totalOKXQueue: 0, 1349 | totalBitgetQueue: 0, 1350 | queueDetails: {} 1351 | } 1352 | 1353 | // 断开Binance连接 1354 | if (wsConnections.value.binance) { 1355 | if (typeof wsConnections.value.binance === 'object') { 1356 | // 多个连接的情况 1357 | Object.values(wsConnections.value.binance).forEach(ws => { 1358 | if (ws && ws.readyState === WebSocket.OPEN) { 1359 | ws.close() 1360 | } 1361 | }) 1362 | } else { 1363 | // 单个连接的情况 1364 | if (wsConnections.value.binance.readyState === WebSocket.OPEN) { 1365 | wsConnections.value.binance.close() 1366 | } 1367 | } 1368 | } 1369 | 1370 | // 断开OKX连接 1371 | if (wsConnections.value.okx && wsConnections.value.okx.readyState === WebSocket.OPEN) { 1372 | wsConnections.value.okx.close() 1373 | } 1374 | 1375 | // 断开Bitget连接 1376 | if (wsConnections.value.bitget && wsConnections.value.bitget.readyState === WebSocket.OPEN) { 1377 | wsConnections.value.bitget.close() 1378 | } 1379 | 1380 | wsConnections.value = {} 1381 | isConnected.value = false 1382 | console.log('所有WebSocket连接已断开,所有队列和协程已清空,实时统计数据已重置') 1383 | } 1384 | 1385 | // 获取指定交易对的历史数据 1386 | const getTickHistory = (symbol) => { 1387 | return tickHistory.value[symbol] || [] 1388 | } 1389 | 1390 | // 检查WebSocket连接状态 1391 | const checkConnectionStatus = () => { 1392 | if (!selectedExchangePair.value) { 1393 | isConnected.value = false 1394 | return false 1395 | } 1396 | 1397 | const [firstExchange, secondExchange] = selectedExchangePair.value.split('-') 1398 | let firstConnected = false 1399 | let secondConnected = false 1400 | 1401 | // 检查第一个交易所连接 1402 | if (wsConnections.value[firstExchange]) { 1403 | if (typeof wsConnections.value[firstExchange] === 'object' && !wsConnections.value[firstExchange].readyState) { 1404 | // 多个连接的情况(如Binance) 1405 | firstConnected = Object.values(wsConnections.value[firstExchange]).some(ws => 1406 | ws && ws.readyState === WebSocket.OPEN 1407 | ) 1408 | } else { 1409 | // 单个连接的情况(如OKX、Bitget) 1410 | firstConnected = wsConnections.value[firstExchange].readyState === WebSocket.OPEN 1411 | } 1412 | } 1413 | 1414 | // 检查第二个交易所连接 1415 | if (wsConnections.value[secondExchange]) { 1416 | if (typeof wsConnections.value[secondExchange] === 'object' && !wsConnections.value[secondExchange].readyState) { 1417 | // 多个连接的情况(如Binance) 1418 | secondConnected = Object.values(wsConnections.value[secondExchange]).some(ws => 1419 | ws && ws.readyState === WebSocket.OPEN 1420 | ) 1421 | } else { 1422 | // 单个连接的情况(如OKX、Bitget) 1423 | secondConnected = wsConnections.value[secondExchange].readyState === WebSocket.OPEN 1424 | } 1425 | } 1426 | 1427 | const newStatus = firstConnected && secondConnected 1428 | if (isConnected.value !== newStatus) { 1429 | isConnected.value = newStatus 1430 | console.log('连接状态变化:', { 1431 | [`${firstExchange}Connected`]: firstConnected, 1432 | [`${secondExchange}Connected`]: secondConnected, 1433 | overall: newStatus 1434 | }) 1435 | } 1436 | 1437 | return newStatus 1438 | } 1439 | 1440 | // 定期检查连接状态 1441 | let statusCheckInterval = null 1442 | 1443 | const startStatusCheck = () => { 1444 | if (statusCheckInterval) { 1445 | clearInterval(statusCheckInterval) 1446 | } 1447 | statusCheckInterval = setInterval(checkConnectionStatus, 5000) // 每5秒检查一次 1448 | } 1449 | 1450 | const stopStatusCheck = () => { 1451 | if (statusCheckInterval) { 1452 | clearInterval(statusCheckInterval) 1453 | statusCheckInterval = null 1454 | } 1455 | } 1456 | 1457 | // 强制刷新价格数据显示 1458 | const forceUpdatePriceData = () => { 1459 | // 强制触发响应式更新 1460 | const temp = { ...priceData.value } 1461 | priceData.value = temp 1462 | console.log('强制刷新完成,当前价格数据:', Object.keys(priceData.value).length, '个数据流') 1463 | } 1464 | 1465 | // 获取实时最大正价差(从网站打开开始计算)- Binance买/OKX卖 1466 | const getRealtimeMaxPositiveSpread = (symbol) => { 1467 | const stats = realtimeStats.value[symbol] 1468 | if (!stats || stats.maxBuyBinanceSellOkx === -Infinity) { 1469 | return 0 1470 | } 1471 | return stats.maxBuyBinanceSellOkx 1472 | } 1473 | 1474 | // 获取实时最大价差(从网站打开开始计算)- Binance卖/OKX买 1475 | const getRealtimeMaxSellBinanceBuyOkx = (symbol) => { 1476 | const stats = realtimeStats.value[symbol] 1477 | if (!stats || stats.maxSellBinanceBuyOkx === -Infinity) { 1478 | return 0 1479 | } 1480 | return stats.maxSellBinanceBuyOkx 1481 | } 1482 | 1483 | // 获取实时最大负价差(从网站打开开始计算) 1484 | const getRealtimeMaxNegativeSpread = (symbol) => { 1485 | const stats = realtimeStats.value[symbol] 1486 | if (!stats || stats.maxNegativeSpread === Infinity) { 1487 | return 0 1488 | } 1489 | return stats.maxNegativeSpread 1490 | } 1491 | 1492 | // 重置实时统计数据 1493 | const resetRealtimeStats = (symbol = null) => { 1494 | if (symbol) { 1495 | if (realtimeStats.value[symbol]) { 1496 | realtimeStats.value[symbol] = { 1497 | maxBuyBinanceSellOkx: -Infinity, // Binance买/OKX卖的最大价差 1498 | maxSellBinanceBuyOkx: -Infinity, // Binance卖/OKX买的最大价差 1499 | maxNegativeSpread: Infinity, 1500 | startTime: Date.now() 1501 | } 1502 | console.log(`已重置 ${symbol} 的实时统计数据`) 1503 | } 1504 | } else { 1505 | // 重置所有交易对的统计数据 1506 | Object.keys(realtimeStats.value).forEach(sym => { 1507 | realtimeStats.value[sym] = { 1508 | maxBuyBinanceSellOkx: -Infinity, // Binance买/OKX卖的最大价差 1509 | maxSellBinanceBuyOkx: -Infinity, // Binance卖/OKX买的最大价差 1510 | maxNegativeSpread: Infinity, 1511 | startTime: Date.now() 1512 | } 1513 | }) 1514 | console.log('已重置所有交易对的实时统计数据') 1515 | } 1516 | } 1517 | 1518 | // 手动添加测试数据 1519 | const addTestData = (symbol = 'BTC/USDT:USDT') => { 1520 | // 确保交易对被选中 1521 | if (!selectedSymbols.value.includes(symbol)) { 1522 | selectedSymbols.value.push(symbol) 1523 | console.log(`已自动选中交易对: ${symbol}`) 1524 | } 1525 | 1526 | // 如果没有合约大小数据,手动设置一些测试值 1527 | if (Object.keys(contractSizes.value).length === 0) { 1528 | const testContractSizes = { 1529 | 'BTC/USDT:USDT': 0.01, 1530 | 'ETH/USDT:USDT': 0.1, 1531 | 'BNB/USDT:USDT': 1, 1532 | 'SOL/USDT:USDT': 1, 1533 | 'XRP/USDT:USDT': 1, 1534 | 'ADA/USDT:USDT': 1, 1535 | 'DOGE/USDT:USDT': 1 1536 | } 1537 | contractSizes.value = testContractSizes 1538 | console.log('已设置测试合约大小:', testContractSizes) 1539 | } 1540 | 1541 | // 初始化队列 1542 | initializeQueues() 1543 | 1544 | const basePrice = 50000 1545 | const binanceBid = (basePrice + Math.random() * 100 - 50).toFixed(2) 1546 | const binanceAsk = (parseFloat(binanceBid) + Math.random() * 10 + 1).toFixed(2) 1547 | const okxBid = (basePrice + Math.random() * 100 - 50).toFixed(2) 1548 | const okxAsk = (parseFloat(okxBid) + Math.random() * 10 + 1).toFixed(2) 1549 | 1550 | // 模拟Binance数据格式 1551 | const binanceData = { 1552 | s: convertToBinanceFormat(symbol), 1553 | b: binanceBid, 1554 | a: binanceAsk, 1555 | B: '1.0', 1556 | A: '1.0' 1557 | } 1558 | 1559 | // 模拟OKX数据格式 - 使用较大的原始数量来测试合约大小效果 1560 | const okxData = { 1561 | bids: [[okxBid, '100.0']], // 使用100张合约 1562 | asks: [[okxAsk, '150.0']] // 使用150张合约 1563 | } 1564 | 1565 | console.log(`准备添加测试数据: ${symbol}`) 1566 | console.log('Binance数据:', { bid: binanceBid, ask: binanceAsk, bidQty: '1.0', askQty: '1.0' }) 1567 | console.log('OKX数据:', { bid: okxBid, ask: okxAsk, bidQty: '100.0', askQty: '150.0' }) 1568 | console.log('合约大小:', contractSizes.value[symbol] || 1) 1569 | 1570 | // 先添加Binance数据 1571 | addToBinanceQueue(symbol, binanceData) 1572 | 1573 | // 稍微延迟添加OKX数据,模拟真实的网络延迟 1574 | setTimeout(() => { 1575 | addToOKXQueue(symbol, okxData) 1576 | }, Math.random() * 50) // 0-50ms随机延迟 1577 | } 1578 | 1579 | // 设置可用交易对列表 1580 | const setAvailableSymbols = (symbols) => { 1581 | availableSymbols.value = symbols 1582 | console.log(`已更新可用交易对列表: ${symbols.length} 个交易对`) 1583 | } 1584 | 1585 | // 设置合约大小映射 1586 | const setContractSizes = (sizes) => { 1587 | contractSizes.value = sizes 1588 | console.log(`已更新合约大小映射:`, sizes) 1589 | } 1590 | 1591 | // 获取当前合约大小映射(调试用) 1592 | const getContractSizes = () => { 1593 | return contractSizes.value 1594 | } 1595 | 1596 | // 检查合约大小映射状态 1597 | const checkContractSizes = () => { 1598 | console.log('=== 当前合约大小映射状态 ===') 1599 | console.log('映射数量:', Object.keys(contractSizes.value).length) 1600 | console.log('详细映射:', contractSizes.value) 1601 | console.log('===============================') 1602 | return contractSizes.value 1603 | } 1604 | 1605 | // ============ 系统配置管理函数 ============ 1606 | 1607 | // 获取当前系统配置 1608 | const getSystemConfig = () => { 1609 | return { ...systemConfig.value } 1610 | } 1611 | 1612 | // 更新系统配置(部分更新) 1613 | const updateSystemConfig = (newConfig) => { 1614 | const oldConfig = { ...systemConfig.value } 1615 | systemConfig.value = { ...systemConfig.value, ...newConfig } 1616 | 1617 | console.log('=== 系统配置已更新 ===') 1618 | console.log('旧配置:', oldConfig) 1619 | console.log('新配置:', systemConfig.value) 1620 | console.log('更新项:', newConfig) 1621 | console.log('=========================') 1622 | 1623 | // 如果清理间隔发生变化,需要重启所有匹配协程 1624 | if (newConfig.cleanupInterval && newConfig.cleanupInterval !== oldConfig.cleanupInterval) { 1625 | console.log('清理间隔发生变化,重启所有匹配协程...') 1626 | restartAllMatchers() 1627 | } 1628 | 1629 | return systemConfig.value 1630 | } 1631 | 1632 | // 重置系统配置为默认值 1633 | const resetSystemConfig = () => { 1634 | const defaultConfig = { 1635 | maxTimeDiff: 1000, 1636 | dataExpirationTime: 1000, 1637 | cleanupInterval: 5000, 1638 | maxQueueSize: 100, 1639 | historyRetentionCount: 2000, 1640 | timeMatchingMode: 'receiveTime', 1641 | maxLocalTimeDiff: 500 // 最大本地时间差(ms) - 原始时间戳与本地时间的最大允许差异 1642 | } 1643 | 1644 | systemConfig.value = defaultConfig 1645 | console.log('系统配置已重置为默认值:', defaultConfig) 1646 | 1647 | // 重启所有匹配协程以应用新配置 1648 | restartAllMatchers() 1649 | 1650 | return systemConfig.value 1651 | } 1652 | 1653 | // 重启所有匹配协程(配置变更时使用) 1654 | const restartAllMatchers = () => { 1655 | console.log('重启所有匹配协程以应用新配置...') 1656 | stopAllMatchers() 1657 | // 稍微延迟后重新启动,确保旧协程完全停止 1658 | setTimeout(() => { 1659 | startAllMatchers() 1660 | console.log('所有匹配协程已使用新配置重启') 1661 | }, 100) 1662 | } 1663 | 1664 | // 获取配置项说明 1665 | const getConfigDescription = () => { 1666 | return { 1667 | maxTimeDiff: { 1668 | name: '匹配最大时间差', 1669 | description: '两个交易所数据匹配时允许的最大时间差(毫秒)', 1670 | unit: 'ms', 1671 | defaultValue: 1000, 1672 | recommendedRange: '500-5000' 1673 | }, 1674 | dataExpirationTime: { 1675 | name: '数据匹配过期时间', 1676 | description: '队列中数据的匹配过期时间,超过此时间的数据将被清理且不再参与匹配(毫秒)', 1677 | unit: 'ms', 1678 | defaultValue: 1000, 1679 | recommendedRange: '500-3000' 1680 | }, 1681 | cleanupInterval: { 1682 | name: '清理间隔', 1683 | description: '多久执行一次过期数据清理(毫秒)', 1684 | unit: 'ms', 1685 | defaultValue: 5000, 1686 | recommendedRange: '100-10000' 1687 | }, 1688 | maxQueueSize: { 1689 | name: '队列最大容量', 1690 | description: '每个队列最多保留的数据点数', 1691 | unit: '个', 1692 | defaultValue: 100, 1693 | recommendedRange: '10-1000' 1694 | }, 1695 | historyRetentionCount: { 1696 | name: '历史数据保留数量', 1697 | description: '最多保留的历史tick数量', 1698 | unit: '个', 1699 | defaultValue: 2000, 1700 | recommendedRange: '1000-10000' 1701 | }, 1702 | timeMatchingMode: { 1703 | name: '时间匹配模式', 1704 | description: '使用原始时间戳还是接收时间进行匹配', 1705 | defaultValue: 'receiveTime', 1706 | recommendedRange: 'originalTimestamp | receiveTime' 1707 | }, 1708 | maxLocalTimeDiff: { 1709 | name: '最大本地时间差', 1710 | description: '原始时间戳与本地时间的最大允许差异(毫秒)', 1711 | unit: 'ms', 1712 | defaultValue: 500, 1713 | recommendedRange: '100-2000' 1714 | } 1715 | } 1716 | } 1717 | 1718 | // 验证配置值的合理性 1719 | const validateConfig = (config) => { 1720 | const errors = [] 1721 | 1722 | if (config.maxTimeDiff && (config.maxTimeDiff < 500 || config.maxTimeDiff > 5000)) { 1723 | errors.push('maxTimeDiff 应该在 500-5000ms 之间') 1724 | } 1725 | 1726 | if (config.dataExpirationTime && (config.dataExpirationTime < 100 || config.dataExpirationTime > 5000)) { 1727 | errors.push('dataExpirationTime 应该在 100-5000ms 之间') 1728 | } 1729 | 1730 | if (config.cleanupInterval && (config.cleanupInterval < 100 || config.cleanupInterval > 10000)) { 1731 | errors.push('cleanupInterval 应该在 100-10000ms 之间') 1732 | } 1733 | 1734 | if (config.maxQueueSize && (config.maxQueueSize < 10 || config.maxQueueSize > 1000)) { 1735 | errors.push('maxQueueSize 应该在 10-1000 之间') 1736 | } 1737 | 1738 | if (config.historyRetentionCount && (config.historyRetentionCount < 100 || config.historyRetentionCount > 10000)) { 1739 | errors.push('historyRetentionCount 应该在 100-10000 之间') 1740 | } 1741 | 1742 | if (config.maxLocalTimeDiff && (config.maxLocalTimeDiff < 100 || config.maxLocalTimeDiff > 2000)) { 1743 | errors.push('maxLocalTimeDiff 应该在 100-2000ms 之间') 1744 | } 1745 | 1746 | return errors 1747 | } 1748 | 1749 | // 安全更新系统配置(带验证) 1750 | const safeUpdateSystemConfig = (newConfig) => { 1751 | const errors = validateConfig(newConfig) 1752 | 1753 | if (errors.length > 0) { 1754 | console.error('配置验证失败:', errors) 1755 | throw new Error(`配置验证失败: ${errors.join(', ')}`) 1756 | } 1757 | 1758 | return updateSystemConfig(newConfig) 1759 | } 1760 | 1761 | // ============ Funding Rate 相关函数 ============ 1762 | 1763 | // 获取Binance历史Funding Rate(用于计算周期) 1764 | const fetchBinanceFundingRateHistory = async (symbol, limit = 5) => { 1765 | try { 1766 | const binanceSymbol = convertToBinanceFormat(symbol) 1767 | if (!binanceSymbol) { 1768 | throw new Error(`无法转换Binance交易对格式: ${symbol}`) 1769 | } 1770 | 1771 | const response = await fetch(`https://fapi.binance.com/fapi/v1/fundingRate?symbol=${binanceSymbol}&limit=${limit}`) 1772 | const data = await response.json() 1773 | 1774 | if (data.code) { 1775 | throw new Error(`Binance历史Funding Rate API错误: ${data.msg}`) 1776 | } 1777 | 1778 | return data 1779 | } catch (error) { 1780 | console.error(`获取Binance历史Funding Rate失败 ${symbol}:`, error) 1781 | return [] 1782 | } 1783 | } 1784 | 1785 | // 计算Funding Rate周期(小时) 1786 | const calculateFundingRatePeriod = (historyData) => { 1787 | if (!historyData || historyData.length < 2) { 1788 | return 8 // 默认8小时周期 1789 | } 1790 | 1791 | // 取最近两次funding rate的时间差 1792 | const latest = historyData[0] 1793 | const previous = historyData[1] 1794 | 1795 | const timeDiff = parseInt(latest.fundingTime) - parseInt(previous.fundingTime) 1796 | const hours = Math.round(timeDiff / (1000 * 60 * 60)) 1797 | 1798 | // 验证周期合理性(通常是8小时,但有些可能是1小时、4小时等) 1799 | const validPeriods = [1, 4, 8, 12, 24] 1800 | const closestPeriod = validPeriods.reduce((prev, curr) => 1801 | Math.abs(curr - hours) < Math.abs(prev - hours) ? curr : prev 1802 | ) 1803 | 1804 | return closestPeriod 1805 | } 1806 | 1807 | // 获取Binance Funding Rate(增强版,包含周期计算) 1808 | const fetchBinanceFundingRate = async (symbol) => { 1809 | try { 1810 | const binanceSymbol = convertToBinanceFormat(symbol) 1811 | if (!binanceSymbol) { 1812 | throw new Error(`无法转换Binance交易对格式: ${symbol}`) 1813 | } 1814 | 1815 | // 并行获取当前数据和历史数据 1816 | const [currentResponse, historyData] = await Promise.all([ 1817 | fetch(`https://fapi.binance.com/fapi/v1/premiumIndex?symbol=${binanceSymbol}`), 1818 | fetchBinanceFundingRateHistory(symbol, 5) 1819 | ]) 1820 | 1821 | const currentData = await currentResponse.json() 1822 | 1823 | if (currentData.code) { 1824 | throw new Error(`Binance API错误: ${currentData.msg}`) 1825 | } 1826 | 1827 | // 计算funding rate周期 1828 | const period = calculateFundingRatePeriod(historyData) 1829 | 1830 | return { 1831 | symbol: binanceSymbol, 1832 | fundingRate: parseFloat(currentData.lastFundingRate), 1833 | nextFundingTime: parseInt(currentData.nextFundingTime), 1834 | fundingCountdown: parseInt(currentData.nextFundingTime) - Date.now(), 1835 | indexPrice: parseFloat(currentData.indexPrice), 1836 | markPrice: parseFloat(currentData.markPrice), 1837 | period: period, // 周期(小时) 1838 | historyData: historyData // 保存历史数据以备用 1839 | } 1840 | } catch (error) { 1841 | console.error(`获取Binance Funding Rate失败 ${symbol}:`, error) 1842 | return null 1843 | } 1844 | } 1845 | 1846 | // 获取OKX历史Funding Rate(用于计算周期) 1847 | const fetchOKXFundingRateHistory = async (symbol, limit = 5) => { 1848 | try { 1849 | const okxSymbol = convertToOKXFormat(symbol) 1850 | if (!okxSymbol) { 1851 | throw new Error(`无法转换OKX交易对格式: ${symbol}`) 1852 | } 1853 | 1854 | const response = await fetch(`https://www.okx.com/api/v5/public/funding-rate-history?instId=${okxSymbol}&limit=${limit}`) 1855 | const data = await response.json() 1856 | 1857 | if (data.code !== '0') { 1858 | throw new Error(`OKX历史Funding Rate API错误: ${data.msg}`) 1859 | } 1860 | 1861 | return data.data || [] 1862 | } catch (error) { 1863 | console.error(`获取OKX历史Funding Rate失败 ${symbol}:`, error) 1864 | return [] 1865 | } 1866 | } 1867 | 1868 | // 获取OKX Funding Rate(增强版,包含周期计算) 1869 | const fetchOKXFundingRate = async (symbol) => { 1870 | try { 1871 | const okxSymbol = convertToOKXFormat(symbol) 1872 | if (!okxSymbol) { 1873 | throw new Error(`无法转换OKX交易对格式: ${symbol}`) 1874 | } 1875 | 1876 | // 并行获取当前数据和历史数据 1877 | const [currentResponse, historyData] = await Promise.all([ 1878 | fetch(`https://www.okx.com/api/v5/public/funding-rate?instId=${okxSymbol}`), 1879 | fetchOKXFundingRateHistory(symbol, 5) 1880 | ]) 1881 | 1882 | const currentData = await currentResponse.json() 1883 | 1884 | if (currentData.code !== '0') { 1885 | throw new Error(`OKX API错误: ${currentData.msg}`) 1886 | } 1887 | 1888 | if (!currentData.data || currentData.data.length === 0) { 1889 | throw new Error('OKX API返回空数据') 1890 | } 1891 | 1892 | // 计算OKX的funding rate周期 1893 | let period = 8 // 默认8小时 1894 | if (historyData && historyData.length >= 2) { 1895 | const latest = historyData[0] 1896 | const previous = historyData[1] 1897 | const timeDiff = parseInt(latest.fundingTime) - parseInt(previous.fundingTime) 1898 | const hours = Math.round(timeDiff / (1000 * 60 * 60)) 1899 | 1900 | const validPeriods = [1, 4, 8, 12, 24] 1901 | period = validPeriods.reduce((prev, curr) => 1902 | Math.abs(curr - hours) < Math.abs(prev - hours) ? curr : prev 1903 | ) 1904 | } 1905 | 1906 | const fundingData = currentData.data[0] 1907 | return { 1908 | symbol: okxSymbol, 1909 | fundingRate: parseFloat(fundingData.fundingRate), 1910 | nextFundingTime: parseInt(fundingData.fundingTime), 1911 | fundingCountdown: parseInt(fundingData.fundingTime) - Date.now(), 1912 | realizedRate: parseFloat(fundingData.realizedRate), 1913 | period: period, // 周期(小时) 1914 | historyData: historyData // 保存历史数据以备用 1915 | } 1916 | } catch (error) { 1917 | console.error(`获取OKX Funding Rate失败 ${symbol}:`, error) 1918 | return null 1919 | } 1920 | } 1921 | 1922 | // 获取Bitget Funding Rate(增强版,包含周期计算) 1923 | const fetchBitgetFundingRate = async (symbol) => { 1924 | try { 1925 | const bitgetSymbol = convertToBitgetFormat(symbol) 1926 | if (!bitgetSymbol) { 1927 | throw new Error(`无法转换Bitget交易对格式: ${symbol}`) 1928 | } 1929 | 1930 | // 尝试多个可能的API端点 1931 | let currentData = null 1932 | let historyData = [] 1933 | 1934 | try { 1935 | const v2Response = await fetch(`https://api.bitget.com/api/v2/mix/market/current-fund-rate?symbol=${bitgetSymbol}&productType=usdt-futures`) 1936 | const v2Data = await v2Response.json() 1937 | 1938 | if (v2Data.code === '00000') { 1939 | currentData = v2Data 1940 | } else { 1941 | throw new Error(`Bitget API错误: ${v2Data.msg || 'API调用失败'}`) 1942 | } 1943 | } catch (apiError) { 1944 | console.error(`Bitget API调用失败:`, apiError) 1945 | return null 1946 | } 1947 | 1948 | 1949 | if (!currentData || !currentData.data) { 1950 | throw new Error('Bitget API返回空数据') 1951 | } 1952 | 1953 | const fundingData = currentData.data[0] 1954 | console.log('Bitget资金费率数据:', fundingData) 1955 | 1956 | // Bitget API字段映射 1957 | const fundingRate = parseFloat(fundingData.fundingRate || fundingData.rate || 0) 1958 | const nextSettleTime = parseInt(fundingData.nextUpdate || fundingData.nextSettleTime || fundingData.nextFundingTime || fundingData.fundingTime || Date.now() + 8 * 60 * 60 * 1000) 1959 | 1960 | return { 1961 | symbol: bitgetSymbol, 1962 | fundingRate: fundingRate, 1963 | nextFundingTime: nextSettleTime, 1964 | fundingCountdown: nextSettleTime - Date.now(), 1965 | period: fundingData.fundingRateInterval, // 周期(小时) 1966 | historyData: [] // 保存历史数据以备用 1967 | } 1968 | } catch (error) { 1969 | console.error(`获取Bitget Funding Rate失败 ${symbol}:`, error) 1970 | return null 1971 | } 1972 | } 1973 | 1974 | // 获取单个交易对的Funding Rate 1975 | const fetchFundingRateForSymbol = async (symbol) => { 1976 | console.log(`开始获取 ${symbol} 的Funding Rate...`) 1977 | 1978 | const [binanceData, okxData, bitgetData] = await Promise.all([ 1979 | fetchBinanceFundingRate(symbol), 1980 | fetchOKXFundingRate(symbol), 1981 | fetchBitgetFundingRate(symbol) 1982 | ]) 1983 | 1984 | if (binanceData || okxData || bitgetData) { 1985 | fundingRates.value[symbol] = { 1986 | binance: binanceData, 1987 | okx: okxData, 1988 | bitget: bitgetData, 1989 | lastUpdate: Date.now() 1990 | } 1991 | 1992 | console.log(`${symbol} Funding Rate获取完成:`, { 1993 | binance: binanceData?.fundingRate, 1994 | okx: okxData?.fundingRate, 1995 | bitget: bitgetData?.fundingRate, 1996 | binanceNext: binanceData?.nextFundingTime ? new Date(binanceData.nextFundingTime).toLocaleString() : 'N/A', 1997 | okxNext: okxData?.nextFundingTime ? new Date(okxData.nextFundingTime).toLocaleString() : 'N/A', 1998 | bitgetNext: bitgetData?.nextFundingTime ? new Date(bitgetData.nextFundingTime).toLocaleString() : 'N/A' 1999 | }) 2000 | } 2001 | } 2002 | 2003 | // 获取所有选中交易对的Funding Rate 2004 | const fetchAllFundingRates = async () => { 2005 | console.log('开始获取所有交易对的Funding Rate...') 2006 | 2007 | const promises = selectedSymbols.value.map(symbol => fetchFundingRateForSymbol(symbol)) 2008 | await Promise.allSettled(promises) 2009 | 2010 | console.log('所有Funding Rate获取完成') 2011 | } 2012 | 2013 | // 定时更新资金费率 2014 | let fundingRateUpdateInterval = null 2015 | const startFundingRateUpdates = () => { 2016 | if (fundingRateUpdateInterval) { 2017 | clearInterval(fundingRateUpdateInterval) 2018 | } 2019 | 2020 | // 立即获取一次 2021 | if (selectedSymbols.value.length > 0) { 2022 | fetchAllFundingRates() 2023 | } 2024 | 2025 | // 每10分钟更新一次资金费率 2026 | fundingRateUpdateInterval = setInterval(() => { 2027 | if (selectedSymbols.value.length > 0) { 2028 | console.log('定时更新资金费率...') 2029 | fetchAllFundingRates() 2030 | } 2031 | }, 10 * 60 * 1000) // 10分钟 2032 | 2033 | console.log('资金费率定时更新已启动 (每10分钟)') 2034 | } 2035 | 2036 | const stopFundingRateUpdates = () => { 2037 | if (fundingRateUpdateInterval) { 2038 | clearInterval(fundingRateUpdateInterval) 2039 | fundingRateUpdateInterval = null 2040 | console.log('资金费率定时更新已停止') 2041 | } 2042 | } 2043 | 2044 | // 格式化Funding Rate显示 2045 | const formatFundingRate = (rate) => { 2046 | if (rate === null || rate === undefined) return 'N/A' 2047 | return `${(rate * 100).toFixed(4)}%` 2048 | } 2049 | 2050 | // 格式化Funding Rate周期显示 2051 | const formatFundingRatePeriod = (period) => { 2052 | if (!period) return '8h' // 默认8小时 2053 | return `${period}h` 2054 | } 2055 | 2056 | // 格式化下次Funding时间(倒计时显示) 2057 | const formatNextFundingTime = (timestamp) => { 2058 | if (!timestamp) return 'N/A' 2059 | 2060 | const now = Date.now() 2061 | const diff = timestamp - now 2062 | 2063 | if (diff <= 0) return '即将开始' 2064 | 2065 | const hours = Math.floor(diff / (1000 * 60 * 60)) 2066 | const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) 2067 | 2068 | return `${hours}h ${minutes}m` 2069 | } 2070 | 2071 | // 仅格式化倒计时(可选使用) 2072 | const formatFundingCountdown = (timestamp) => { 2073 | if (!timestamp) return 'N/A' 2074 | 2075 | const now = Date.now() 2076 | const diff = timestamp - now 2077 | 2078 | if (diff <= 0) return '即将开始' 2079 | 2080 | const hours = Math.floor(diff / (1000 * 60 * 60)) 2081 | const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) 2082 | 2083 | return `${hours}h ${minutes}m` 2084 | } 2085 | 2086 | // 获取Funding Rate数据 2087 | const getFundingRates = () => { 2088 | return fundingRates.value 2089 | } 2090 | 2091 | return { 2092 | selectedSymbols, 2093 | selectedExchangePair, 2094 | availableSymbols, 2095 | priceData, 2096 | isConnected, 2097 | matchStats, 2098 | symbolQueues, 2099 | fundingRates, 2100 | getFormattedPriceData, 2101 | setSelectedExchangePair, 2102 | setSelectedSymbols, 2103 | connectWebSockets, 2104 | disconnectWebSockets, 2105 | getTickHistory, 2106 | startStatusCheck, 2107 | stopStatusCheck, 2108 | addTestData, 2109 | saveTickHistory, 2110 | tickHistory, 2111 | startMatcherCoroutine, 2112 | stopMatcherCoroutine, 2113 | startSymbolMatcher, 2114 | stopSymbolMatcher, 2115 | startAllMatchers, 2116 | stopAllMatchers, 2117 | getRealtimeMaxPositiveSpread, 2118 | getRealtimeMaxSellBinanceBuyOkx, 2119 | getRealtimeMaxNegativeSpread, 2120 | resetRealtimeStats, 2121 | setAvailableSymbols, 2122 | setContractSizes, 2123 | getContractSizes, 2124 | checkContractSizes, 2125 | addToBinanceQueue, 2126 | addToOKXQueue, 2127 | addToBitgetQueue, 2128 | convertToBinanceFormat, 2129 | convertToOKXFormat, 2130 | convertFromOKXFormat, 2131 | convertFromBinanceFormat, 2132 | convertToBitgetFormat, 2133 | convertFromBitgetFormat, 2134 | clearSymbolData, 2135 | getSystemConfig, 2136 | updateSystemConfig, 2137 | resetSystemConfig, 2138 | restartAllMatchers, 2139 | getConfigDescription, 2140 | validateConfig, 2141 | safeUpdateSystemConfig, 2142 | fetchFundingRateForSymbol, 2143 | fetchAllFundingRates, 2144 | fetchBitgetFundingRate, 2145 | startFundingRateUpdates, 2146 | stopFundingRateUpdates, 2147 | formatFundingRate, 2148 | formatFundingRatePeriod, 2149 | formatNextFundingTime, 2150 | formatFundingCountdown, 2151 | getFundingRates 2152 | } 2153 | }) -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 689 | 690 | 1730 | 1731 | --------------------------------------------------------------------------------