├── src ├── .DS_Store ├── utils │ ├── timeUtils.js │ ├── formatter.js │ └── logger.js ├── config │ └── configLoader.js ├── models │ ├── TradeStats.js │ └── Order.js ├── index.js ├── core │ ├── tradingStrategy.js │ ├── orderManager.js │ └── priceMonitor.js ├── README.md ├── services │ └── backpackService.js ├── network │ └── webSocketManager.js └── app.js ├── logs ├── .DS_Store ├── trading_2025-04-05.log └── backpack_trading_2025-04-05.log ├── package.json ├── backpack_trading_config.json ├── README.md └── backpack_exchange-main └── backpack_client.js /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocj520/bp3/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /logs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocj520/bp3/HEAD/logs/.DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "axios": "^1.8.4", 4 | "got": "^11.8.6", 5 | "qs": "^6.14.0", 6 | "ws": "^8.18.1" 7 | }, 8 | "name": "bpmading", 9 | "version": "1.0.0", 10 | "description": "这是一个基于 Backpack 交易所的自动交易系统,支持自动买入、止盈和风险控制。", 11 | "main": "backpack_api.js", 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/cryptocj520/backpack2.git" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/cryptocj520/backpack2/issues" 24 | }, 25 | "homepage": "https://github.com/cryptocj520/backpack2#readme" 26 | } 27 | -------------------------------------------------------------------------------- /backpack_trading_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "privateKey": "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&", 4 | "publicKey": "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" 5 | }, 6 | "trading": { 7 | "tradingCoin": "BTC", 8 | "maxDropPercentage": 7, 9 | "totalAmount": 2500, 10 | "orderCount": 8, 11 | "incrementPercentage": 50, 12 | "takeProfitPercentage": 0.5 13 | }, 14 | "actions": { 15 | "sellNonUsdcAssets": true, 16 | "cancelAllOrders": true, 17 | "restartAfterTakeProfit": true, 18 | "autoRestartNoFill": true 19 | }, 20 | "advanced": { 21 | "minOrderAmount": 10, 22 | "priceTickSize": 0.01, 23 | "checkOrdersIntervalMinutes": 10, 24 | "monitorIntervalSeconds": 15, 25 | "sellNonUsdcMinValue": 10, 26 | "noFillRestartMinutes": 1 27 | }, 28 | "quantityPrecisions": { 29 | "BTC": 5, 30 | "ETH": 4, 31 | "SOL": 2, 32 | "DEFAULT": 2 33 | }, 34 | "pricePrecisions": { 35 | "BTC": 0, 36 | "ETH": 2, 37 | "SOL": 2, 38 | "DEFAULT": 2 39 | }, 40 | "minQuantities": { 41 | "BTC": 0.00001, 42 | "ETH": 0.001, 43 | "SOL": 0.01, 44 | "DEFAULT": 0.1 45 | }, 46 | "websocket": { 47 | "url": "wss://ws.backpack.exchange", 48 | "options": { 49 | "reconnect": true, 50 | "reconnectInterval": 5000, 51 | "maxReconnectAttempts": 5, 52 | "pingInterval": 60000, 53 | "pongTimeout": 120000, 54 | "subscriptionTimeout": 5000, 55 | "messageQueueSize": 1000, 56 | "messageTimeout": 30000 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /logs/trading_2025-04-05.log: -------------------------------------------------------------------------------- 1 | [2025/4/5 20:31:58] 收到WS消息类型: ["data","stream"] 2 | [2025/4/5 20:31:58] 消息内容: {"data":{"E":1743856318231879,"V":"7495639.955072","c":"83429","e":"ticker","h":"84557","l":"81822.7","n":33813,"o":"82727.2","s":"BTC_USDC","v":"89.9... 3 | [2025/4/5 20:31:58] 找到价格数据消息: {"data":{"E":1743856318231879,"V":"7495639.955072","c":"83429","e":"ticker","h":"84557","l":"81822.7... 4 | [2025/4/5 20:31:58] 处理价格数据: {"data":{"E":1743856318231879,"V":"7495639.955072","c":"83429","e":"ticker","h":"84557","l":"81822.7","n":33813,"o":"82727.2","s":"BTC_USDC","v":"89.90515"},"stream":"ticker.BTC_USDC"}... 5 | [2025/4/5 20:31:58] 识别为Backpack嵌套格式数据 6 | [2025/4/5 20:31:58] 成功提取价格数据: 交易对=BTC_USDC, 价格=83429 7 | [2025/4/5 20:31:58] 20:31:58 - BTC_USDC: 83429 8 | [2025/4/5 20:31:58] 准备调用外部价格回调: symbol=BTC_USDC, price=83429 9 | [2025/4/5 20:31:58] 收到WebSocket价格更新: BTC_USDC = 83429 USDC (时间: 20:31:58) 10 | [2025/4/5 20:31:58] WebSocket价格更新: {"price":83429,"symbol":"BTC_USDC","source":"WebSocket","updateTime":1743856318272,"change":0} 11 | [2025/4/5 20:31:58] 相对均价下跌: 0.32% (当前: 83429.00, 均价: 83694.00) 12 | [2025/4/5 20:31:58] 价格信息已传递给应用程序 13 | [2025/4/5 20:31:58] 外部价格回调调用成功 14 | [2025/4/5 20:31:59] 找到价格数据消息: {"data":{"E":1743856319222531,"V":"7495748.412707","c":"83429","e":"ticker","h":"84557","l":"81822.7... 15 | [2025/4/5 20:31:59] 处理价格数据: {"data":{"E":1743856319222531,"V":"7495748.412707","c":"83429","e":"ticker","h":"84557","l":"81822.7","n":33818,"o":"82727.2","s":"BTC_USDC","v":"89.90645"},"stream":"ticker.BTC_USDC"}... 16 | [2025/4/5 20:31:59] 识别为Backpack嵌套格式数据 17 | [2025/4/5 20:31:59] 成功提取价格数据: 交易对=BTC_USDC, 价格=83429 18 | [2025/4/5 20:31:59] 20:31:59 - BTC_USDC: 83429 19 | [2025/4/5 20:31:59] 准备调用外部价格回调: symbol=BTC_USDC, price=83429 20 | [2025/4/5 20:31:59] 收到WebSocket价格更新: BTC_USDC = 83429 USDC (时间: 20:31:59) 21 | [2025/4/5 20:31:59] WebSocket价格更新: {"price":83429,"symbol":"BTC_USDC","source":"WebSocket","updateTime":1743856319295,"change":0} 22 | [2025/4/5 20:31:59] 相对均价下跌: 0.32% (当前: 83429.00, 均价: 83694.00) 23 | [2025/4/5 20:31:59] 价格信息已传递给应用程序 24 | [2025/4/5 20:31:59] 外部价格回调调用成功 25 | -------------------------------------------------------------------------------- /logs/backpack_trading_2025-04-05.log: -------------------------------------------------------------------------------- 1 | [2025/4/5 20:45:55] === Backpack 自动化交易系统启动 === 2 | [2025/4/5 20:45:55] Node.js版本: v22.13.0 3 | [2025/4/5 20:45:55] WorkingDir: /Users/m4-2/Documents/fabu/fabumadiing 4 | [2025/4/5 20:45:55] 脚本基本路径: /Users/m4-2/Documents/fabu/fabumadiing/src 5 | [2025/4/5 20:45:55] 读取配置文件: /Users/m4-2/Documents/fabu/fabumadiing/backpack_trading_config.json 6 | [2025/4/5 20:45:55] 交易币种: BTC 7 | [2025/4/5 20:45:55] 总投资额: 2500 USDC 8 | [2025/4/5 20:45:55] 订单数量: 8 9 | [2025/4/5 20:45:55] 程序执行出错: 私钥处理错误: 私钥长度不足 10 | [2025/4/5 20:45:55] 错误堆栈: Error: 私钥处理错误: 私钥长度不足 11 | at toPkcs8der (/Users/m4-2/Documents/fabu/fabumadiing/backpack_exchange-main/backpack_client.js:92:15) 12 | at new BackpackClient (/Users/m4-2/Documents/fabu/fabumadiing/backpack_exchange-main/backpack_client.js:211:30) 13 | at new BackpackService (/Users/m4-2/Documents/fabu/fabumadiing/src/services/backpackService.js:25:19) 14 | at new TradingApp (/Users/m4-2/Documents/fabu/fabumadiing/src/app.js:24:28) 15 | at main (/Users/m4-2/Documents/fabu/fabumadiing/src/index.js:57:21) 16 | at Object. (/Users/m4-2/Documents/fabu/fabumadiing/src/index.js:152:1) 17 | at Module._compile (node:internal/modules/cjs/loader:1562:14) 18 | at Object..js (node:internal/modules/cjs/loader:1699:10) 19 | at Module.load (node:internal/modules/cjs/loader:1313:32) 20 | at Function._load (node:internal/modules/cjs/loader:1123:12) 21 | [2025/4/5 20:45:55] 程序执行完成,退出 22 | [2025/4/5 20:45:55] 清理资源... 23 | [2025/4/5 20:45:55] 程序退出,退出码: 0 24 | [2025/4/5 21:02:12] === Backpack 自动化交易系统启动 === 25 | [2025/4/5 21:02:12] Node.js版本: v22.14.0 26 | [2025/4/5 21:02:12] WorkingDir: D:\fabu2\mading2 27 | [2025/4/5 21:02:12] 脚本基本路径: D:\fabu2\mading2\src 28 | [2025/4/5 21:02:12] 读取配置文件: D:\fabu2\mading2\backpack_trading_config.json 29 | [2025/4/5 21:02:12] 交易币种: BTC 30 | [2025/4/5 21:02:12] 总投资额: 2500 USDC 31 | [2025/4/5 21:02:12] 订单数量: 8 32 | [2025/4/5 21:02:12] 程序执行出错: 私钥处理错误: 私钥长度不足 33 | [2025/4/5 21:02:12] 错误堆栈: Error: 私钥处理错误: 私钥长度不足 34 | at toPkcs8der (D:\fabu2\mading2\backpack_exchange-main\backpack_client.js:92:15) 35 | at new BackpackClient (D:\fabu2\mading2\backpack_exchange-main\backpack_client.js:211:30) 36 | at new BackpackService (D:\fabu2\mading2\src\services\backpackService.js:25:19) 37 | at new TradingApp (D:\fabu2\mading2\src\app.js:24:28) 38 | at main (D:\fabu2\mading2\src\index.js:57:21) 39 | at Object. (D:\fabu2\mading2\src\index.js:152:1) 40 | at Module._compile (node:internal/modules/cjs/loader:1554:14) 41 | at Object..js (node:internal/modules/cjs/loader:1706:10) 42 | at Module.load (node:internal/modules/cjs/loader:1289:32) 43 | at Function._load (node:internal/modules/cjs/loader:1108:12) 44 | [2025/4/5 21:02:12] 程序执行完成,退出 45 | [2025/4/5 21:02:12] 清理资源... 46 | [2025/4/5 21:02:12] 程序退出,退出码: 0 47 | -------------------------------------------------------------------------------- /src/utils/timeUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 时间工具类 - 负责处理时间相关功能 3 | */ 4 | class TimeUtils { 5 | /** 6 | * 获取当前时间的格式化字符串 7 | * @param {string} format - 格式化类型 (default: 'full') 8 | * @returns {string} 格式化的时间字符串 9 | */ 10 | static getCurrentTime(format = 'full') { 11 | const now = new Date(); 12 | 13 | switch (format) { 14 | case 'date': 15 | return now.toISOString().split('T')[0]; 16 | case 'time': 17 | return now.toLocaleTimeString(); 18 | case 'compact': 19 | return now.toISOString().replace(/[:.]/g, '-').replace('T', '_').replace('Z', ''); 20 | case 'timestamp': 21 | return now.getTime().toString(); 22 | case 'full': 23 | default: 24 | return now.toLocaleString(); 25 | } 26 | } 27 | 28 | /** 29 | * 获取运行时间的格式化字符串 30 | * @param {Date|number} startTime - 开始时间 31 | * @param {Date|number} endTime - 结束时间(默认为当前时间) 32 | * @returns {string} 格式化的时间差字符串 33 | */ 34 | static getElapsedTime(startTime, endTime = new Date()) { 35 | // 转换为Date对象 36 | const start = startTime instanceof Date ? startTime : new Date(startTime); 37 | const end = endTime instanceof Date ? endTime : new Date(endTime); 38 | 39 | // 计算时间差(毫秒) 40 | const elapsedMs = end - start; 41 | 42 | // 计算小时、分钟和秒 43 | const seconds = Math.floor(elapsedMs / 1000) % 60; 44 | const minutes = Math.floor(elapsedMs / (1000 * 60)) % 60; 45 | const hours = Math.floor(elapsedMs / (1000 * 60 * 60)); 46 | 47 | return `${hours}小时${minutes}分${seconds}秒`; 48 | } 49 | 50 | /** 51 | * 创建延迟Promise 52 | * @param {number} ms - 延迟毫秒数 53 | * @returns {Promise} 延迟Promise 54 | */ 55 | static delay(ms) { 56 | return new Promise(resolve => setTimeout(resolve, ms)); 57 | } 58 | 59 | /** 60 | * 计算到期时间 61 | * @param {number} minutes - 分钟数 62 | * @returns {Date} 到期时间 63 | */ 64 | static getExpiryTime(minutes) { 65 | const expiry = new Date(); 66 | expiry.setMinutes(expiry.getMinutes() + minutes); 67 | return expiry; 68 | } 69 | 70 | /** 71 | * 检查指定时间是否已到期 72 | * @param {Date|number} time - 要检查的时间 73 | * @returns {boolean} 是否已到期 74 | */ 75 | static isExpired(time) { 76 | const checkTime = time instanceof Date ? time : new Date(time); 77 | return new Date() > checkTime; 78 | } 79 | 80 | /** 81 | * 获取剩余时间(分钟) 82 | * @param {Date|number} targetTime - 目标时间 83 | * @returns {number} 剩余分钟数 84 | */ 85 | static getRemainingMinutes(targetTime) { 86 | const target = targetTime instanceof Date ? targetTime : new Date(targetTime); 87 | const now = new Date(); 88 | const diffMs = target - now; 89 | 90 | if (diffMs <= 0) return 0; 91 | 92 | return Math.ceil(diffMs / (1000 * 60)); 93 | } 94 | 95 | /** 96 | * 格式化持续时间(秒) 97 | * @param {number} seconds - 秒数 98 | * @returns {string} 格式化的持续时间 99 | */ 100 | static formatDuration(seconds) { 101 | const mins = Math.floor(seconds / 60); 102 | const secs = Math.floor(seconds % 60); 103 | 104 | if (mins === 0) { 105 | return `${secs}秒`; 106 | } else { 107 | return `${mins}分${secs}秒`; 108 | } 109 | } 110 | } 111 | 112 | module.exports = TimeUtils; -------------------------------------------------------------------------------- /src/config/configLoader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | /** 5 | * 配置加载器类 - 负责加载和验证配置 6 | */ 7 | class ConfigLoader { 8 | /** 9 | * 加载配置文件 10 | * @param {string} configPath - 配置文件路径,默认为当前工作目录下的backpack_trading_config.json 11 | * @returns {Object} 配置对象 12 | */ 13 | static loadConfig(configPath = path.join(process.cwd(), 'backpack_trading_config.json')) { 14 | try { 15 | console.log(`加载配置文件: ${configPath}`); 16 | 17 | if (!fs.existsSync(configPath)) { 18 | throw new Error(`配置文件不存在: ${configPath}`); 19 | } 20 | 21 | const configData = fs.readFileSync(configPath, 'utf8'); 22 | const config = JSON.parse(configData); 23 | 24 | // 验证配置 25 | this.validateConfig(config); 26 | 27 | console.log(`配置文件加载成功`); 28 | return config; 29 | } catch (error) { 30 | console.error(`加载配置文件失败: ${error.message}`); 31 | throw error; 32 | } 33 | } 34 | 35 | /** 36 | * 验证配置有效性 37 | * @param {Object} config - 配置对象 38 | * @throws {Error} 如果配置无效 39 | */ 40 | static validateConfig(config) { 41 | // 验证API配置 42 | if (!config.api) { 43 | throw new Error('缺少API配置'); 44 | } 45 | if (!config.api.privateKey) { 46 | throw new Error('缺少API私钥配置'); 47 | } 48 | if (!config.api.publicKey) { 49 | throw new Error('缺少API公钥配置'); 50 | } 51 | 52 | // 验证交易配置 53 | if (!config.trading) { 54 | throw new Error('缺少交易配置'); 55 | } 56 | if (!config.trading.tradingCoin) { 57 | throw new Error('缺少交易币种配置'); 58 | } 59 | if (!config.trading.totalAmount) { 60 | throw new Error('缺少总金额配置'); 61 | } 62 | if (!config.trading.orderCount) { 63 | throw new Error('缺少订单数量配置'); 64 | } 65 | if (!config.trading.maxDropPercentage) { 66 | throw new Error('缺少最大下跌百分比配置'); 67 | } 68 | if (!config.trading.incrementPercentage) { 69 | throw new Error('缺少增量百分比配置'); 70 | } 71 | if (!config.trading.takeProfitPercentage) { 72 | throw new Error('缺少止盈百分比配置'); 73 | } 74 | 75 | // 验证精度配置 76 | if (!config.minQuantities) { 77 | throw new Error('缺少最小数量配置'); 78 | } 79 | if (!config.quantityPrecisions) { 80 | throw new Error('缺少数量精度配置'); 81 | } 82 | if (!config.pricePrecisions) { 83 | throw new Error('缺少价格精度配置'); 84 | } 85 | 86 | // 验证动作配置 87 | if (!config.actions) { 88 | config.actions = { 89 | sellNonUsdcAssets: false, 90 | cancelAllOrders: true, 91 | restartAfterTakeProfit: false, 92 | autoRestartNoFill: false, 93 | executeTrade: true, 94 | cancelOrdersOnExit: true 95 | }; 96 | } else { 97 | // 确保新版本中的字段存在 98 | if (config.actions.executeTrade === undefined) { 99 | config.actions.executeTrade = true; 100 | } 101 | if (config.actions.cancelOrdersOnExit === undefined) { 102 | config.actions.cancelOrdersOnExit = true; 103 | } 104 | } 105 | 106 | // 验证高级配置 107 | if (!config.advanced) { 108 | config.advanced = { 109 | minOrderAmount: 10, 110 | priceTickSize: 0.01, 111 | checkOrdersIntervalMinutes: 5, 112 | monitorIntervalSeconds: 15, 113 | sellNonUsdcMinValue: 10, 114 | noFillRestartMinutes: 60 115 | }; 116 | } 117 | 118 | // 验证websocket配置 119 | if (!config.websocket) { 120 | config.websocket = { 121 | url: 'wss://ws.backpack.exchange' 122 | }; 123 | } 124 | } 125 | } 126 | 127 | module.exports = ConfigLoader; -------------------------------------------------------------------------------- /src/models/TradeStats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 交易统计类 - 负责管理和计算交易统计数据 3 | */ 4 | // 导入日志工具 5 | const { log } = require('../utils/logger'); 6 | 7 | class TradeStats { 8 | /** 9 | * 构造函数 - 初始化统计数据 10 | */ 11 | constructor() { 12 | this.reset(); 13 | } 14 | 15 | /** 16 | * 重置所有统计数据 17 | */ 18 | reset() { 19 | this.totalOrders = 0; 20 | this.filledOrders = 0; 21 | this.totalFilledAmount = 0; 22 | this.totalFilledQuantity = 0; 23 | this.averagePrice = 0; 24 | this.lastUpdateTime = null; 25 | this.processedOrderIds = new Set(); 26 | } 27 | 28 | /** 29 | * 更新统计信息 30 | * @param {Object} order - 订单信息 31 | * @returns {boolean} 是否已更新数据 32 | */ 33 | updateStats(order) { 34 | if (!order || !order.id) return false; 35 | 36 | // 检查订单ID是否已处理过 37 | if (this.processedOrderIds.has(order.id)) { 38 | // 使用全局log函数(如果可用)记录已处理订单的情况 39 | if (typeof log === 'function') { 40 | log(`跳过已处理订单ID: ${order.id}`); 41 | } 42 | return false; 43 | } 44 | 45 | // 不再在这里增加totalOrders计数,因为订单创建时已经增加 46 | // 避免重复计数问题 47 | 48 | // 确保有成交信息再更新成交统计 49 | if (order.status === 'Filled' || order.status === 'PartiallyFilled') { 50 | // 确保使用数字类型进行计算 51 | const filledAmount = parseFloat(order.filledAmount || order.amount || 0); 52 | const filledQuantity = parseFloat(order.filledQuantity || order.quantity || 0); 53 | 54 | // 添加到已处理订单集合 55 | this.processedOrderIds.add(order.id); 56 | 57 | // 记录处理详情(如果log函数可用) 58 | if (typeof log === 'function') { 59 | log(`处理订单ID: ${order.id}, 状态: ${order.status}, 数量: ${filledQuantity}, 金额: ${filledAmount}`); 60 | } 61 | 62 | if (!isNaN(filledAmount) && filledAmount > 0) { 63 | this.totalFilledAmount += filledAmount; 64 | } 65 | 66 | if (!isNaN(filledQuantity) && filledQuantity > 0) { 67 | this.totalFilledQuantity += filledQuantity; 68 | this.filledOrders++; 69 | 70 | // 记录已成交订单计数增加(如果log函数可用) 71 | if (typeof log === 'function') { 72 | log(`成交订单数增加到: ${this.filledOrders}, 订单ID: ${order.id}`); 73 | } 74 | } 75 | 76 | // 只有当有效成交量存在时才计算均价 77 | if (this.totalFilledQuantity > 0) { 78 | this.averagePrice = this.totalFilledAmount / this.totalFilledQuantity; 79 | } 80 | 81 | this.lastUpdateTime = new Date(); 82 | return true; 83 | } else { 84 | // 非成交状态订单(如果log函数可用) 85 | if (typeof log === 'function') { 86 | log(`订单${order.id}非成交状态: ${order.status}, 不更新成交统计`); 87 | } 88 | } 89 | 90 | return false; 91 | } 92 | 93 | /** 94 | * 计算当前盈亏情况 95 | * @param {number} currentPrice - 当前市场价格 96 | * @returns {Object|null} 盈亏信息对象,包含金额和百分比 97 | */ 98 | calculateProfit(currentPrice) { 99 | if (this.filledOrders === 0 || this.totalFilledQuantity <= 0 || !currentPrice) { 100 | return null; 101 | } 102 | 103 | const currentValue = currentPrice * this.totalFilledQuantity; 104 | const profit = currentValue - this.totalFilledAmount; 105 | const profitPercent = profit / this.totalFilledAmount * 100; 106 | 107 | return { 108 | currentValue, 109 | profit, 110 | profitPercent 111 | }; 112 | } 113 | 114 | /** 115 | * 获取统计摘要 116 | * @returns {Object} 统计摘要对象 117 | */ 118 | getSummary() { 119 | return { 120 | totalOrders: this.totalOrders, 121 | filledOrders: this.filledOrders, 122 | totalFilledAmount: this.totalFilledAmount, 123 | totalFilledQuantity: this.totalFilledQuantity, 124 | averagePrice: this.averagePrice, 125 | lastUpdateTime: this.lastUpdateTime, 126 | processedOrdersCount: this.processedOrderIds.size 127 | }; 128 | } 129 | 130 | /** 131 | * 添加已处理订单ID 132 | * @param {string} orderId - 订单ID 133 | */ 134 | addProcessedOrderId(orderId) { 135 | if (orderId) { 136 | this.processedOrderIds.add(orderId); 137 | } 138 | } 139 | 140 | /** 141 | * 检查订单ID是否已处理 142 | * @param {string} orderId - 订单ID 143 | * @returns {boolean} 是否已处理 144 | */ 145 | isOrderProcessed(orderId) { 146 | return this.processedOrderIds.has(orderId); 147 | } 148 | } 149 | 150 | module.exports = TradeStats; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { Logger, log } = require('./utils/logger'); 4 | const TradingApp = require('./app'); 5 | const TimeUtils = require('./utils/timeUtils'); 6 | 7 | // 全局日志记录器 8 | let logger; 9 | 10 | /** 11 | * 读取配置文件 12 | * @param {string} configPath - 配置文件路径 13 | * @returns {Object} 配置对象 14 | */ 15 | function readConfig(configPath) { 16 | try { 17 | const configFile = fs.readFileSync(configPath, 'utf8'); 18 | return JSON.parse(configFile); 19 | } catch (error) { 20 | console.error(`读取配置文件失败: ${error.message}`); 21 | process.exit(1); 22 | } 23 | } 24 | 25 | /** 26 | * 应用主函数 27 | */ 28 | async function main() { 29 | try { 30 | // 初始化日志记录器 31 | logger = new Logger({ 32 | logDir: path.join(__dirname, '../logs'), 33 | prefix: 'backpack_trading' 34 | }); 35 | 36 | // 记录程序启动 37 | logger.log('=== Backpack 自动化交易系统启动 ==='); 38 | logger.log('Node.js版本: ' + process.version); 39 | logger.log('WorkingDir: ' + process.cwd()); 40 | logger.log('脚本基本路径: ' + __dirname); 41 | 42 | // 读取配置文件 43 | const configPath = path.join(__dirname, '../backpack_trading_config.json'); 44 | logger.log(`读取配置文件: ${configPath}`); 45 | const config = readConfig(configPath); 46 | 47 | // 记录基本配置信息 48 | logger.log(`交易币种: ${config.trading?.tradingCoin}`); 49 | logger.log(`总投资额: ${config.trading?.totalAmount} USDC`); 50 | logger.log(`订单数量: ${config.trading?.orderCount}`); 51 | 52 | let restartNeeded = false; 53 | 54 | do { 55 | try { 56 | // 创建并初始化交易应用 57 | const app = new TradingApp(config, logger); 58 | 59 | // 初始化 60 | const initResult = await app.initialize(); 61 | if (!initResult) { 62 | logger.log('初始化失败,退出程序'); 63 | break; 64 | } 65 | 66 | // 启动应用 67 | await app.start(); 68 | 69 | // 执行交易策略 70 | const tradeResult = await app.executeTrade(); 71 | if (!tradeResult) { 72 | logger.log('交易执行失败'); 73 | } 74 | 75 | // 等待应用完成或需要重启 76 | while (app.isRunning() && !app.isRestartNeeded()) { 77 | // 记录当前状态 78 | if (TimeUtils.getElapsedTime(app.lastStatusLogTime) > 60000) { // 每分钟记录一次状态 79 | logger.log('程序运行中...'); 80 | app.lastStatusLogTime = TimeUtils.getCurrentTime(); 81 | } 82 | 83 | // 等待一段时间 84 | await TimeUtils.delay(5000); 85 | } 86 | 87 | // 判断是否需要重启 88 | restartNeeded = app.isRestartNeeded(); 89 | 90 | // 停止应用 91 | await app.stop(); 92 | 93 | // 如果需要重启,记录信息 94 | if (restartNeeded) { 95 | logger.log('需要重启程序...'); 96 | await TimeUtils.delay(2000); // 给一些时间记录日志 97 | } 98 | } catch (error) { 99 | logger.log(`程序执行出错: ${error.message}`); 100 | logger.log(`错误堆栈: ${error.stack}`); 101 | restartNeeded = false; // 发生未处理的错误时不自动重启 102 | } 103 | } while (restartNeeded); 104 | 105 | logger.log('程序执行完成,退出'); 106 | } catch (error) { 107 | if (logger) { 108 | logger.log(`主程序异常: ${error.message}`); 109 | logger.log(`错误堆栈: ${error.stack}`); 110 | } else { 111 | console.error(`严重错误: ${error.message}`); 112 | console.error(error.stack); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * 设置进程退出处理 119 | */ 120 | function setupGracefulShutdown() { 121 | const exitHandler = (options, exitCode) => { 122 | if (options.cleanup) { 123 | logger?.log('清理资源...'); 124 | // 这里可以添加其他清理逻辑 125 | } 126 | 127 | if (exitCode || exitCode === 0) { 128 | logger?.log(`程序退出,退出码: ${exitCode}`); 129 | } 130 | 131 | if (options.exit) { 132 | process.exit(); 133 | } 134 | }; 135 | 136 | // 捕获不同的退出信号 137 | process.on('exit', exitHandler.bind(null, { cleanup: true })); 138 | process.on('SIGINT', exitHandler.bind(null, { exit: true })); 139 | process.on('SIGUSR1', exitHandler.bind(null, { exit: true })); 140 | process.on('SIGUSR2', exitHandler.bind(null, { exit: true })); 141 | process.on('uncaughtException', (error) => { 142 | logger?.log(`未捕获的异常: ${error.message}`); 143 | logger?.log(`错误堆栈: ${error.stack}`); 144 | exitHandler({ exit: true }, 1); 145 | }); 146 | } 147 | 148 | // 设置优雅退出 149 | setupGracefulShutdown(); 150 | 151 | // 启动主程序 152 | main().catch(error => { 153 | logger?.log(`主程序未捕获异常: ${error.message}`); 154 | logger?.log(`错误堆栈: ${error.stack}`); 155 | process.exit(1); 156 | }); -------------------------------------------------------------------------------- /src/utils/formatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 格式化工具类 - 负责格式化数据以便显示或计算 3 | */ 4 | class Formatter { 5 | /** 6 | * 调整数值精度 7 | * @param {number} value - 需要调整的值 8 | * @param {number} precision - 精度(小数位数) 9 | * @returns {number} 调整后的值 10 | */ 11 | static adjustPrecision(value, precision) { 12 | const multiplier = Math.pow(10, precision); 13 | return Math.floor(value * multiplier) / multiplier; 14 | } 15 | 16 | /** 17 | * 调整价格到tickSize,并根据交易对的精度要求进行处理 18 | * @param {number} price - 原始价格 19 | * @param {string} tradingCoin - 交易币种 20 | * @param {Object} config - 配置对象(可选) 21 | * @returns {number} 调整后的价格 22 | */ 23 | static adjustPriceToTickSize(price, tradingCoin, config = {}) { 24 | // 设置默认值 25 | const tickSize = config?.advanced?.priceTickSize || 0.01; 26 | 27 | // 获取该币种的价格精度 28 | const pricePrecisions = config?.pricePrecisions || { 'BTC': 0, 'ETH': 2, 'SOL': 2, 'DEFAULT': 2 }; 29 | const precision = pricePrecisions[tradingCoin] || pricePrecisions.DEFAULT || 2; 30 | 31 | // BTC特殊处理 - 确保价格是整数 32 | if (tradingCoin === 'BTC') { 33 | // 对BTC价格,直接向下取整到整数 34 | return Math.floor(price); 35 | } 36 | 37 | // 其他币种正常处理 38 | // 先向下取整到tickSize的倍数 39 | const adjustedPrice = Math.floor(price / tickSize) * tickSize; 40 | // 然后限制小数位数 41 | return Number(adjustedPrice.toFixed(precision)); 42 | } 43 | 44 | /** 45 | * 调整数量到stepSize 46 | * @param {number} quantity - 原始数量 47 | * @param {string} tradingCoin - 交易币种 48 | * @param {Object} config - 配置对象(可选) 49 | * @returns {number} 调整后的数量 50 | */ 51 | static adjustQuantityToStepSize(quantity, tradingCoin, config = {}) { 52 | // 设置默认值 53 | const quantityPrecisions = config?.quantityPrecisions || { 'BTC': 5, 'ETH': 4, 'SOL': 2, 'DEFAULT': 2 }; 54 | const precision = quantityPrecisions[tradingCoin] || quantityPrecisions.DEFAULT || 2; 55 | 56 | const stepSize = Math.pow(10, -precision); 57 | const adjustedQuantity = Math.floor(quantity / stepSize) * stepSize; 58 | return Number(adjustedQuantity.toFixed(precision)); 59 | } 60 | 61 | /** 62 | * 获取已运行时间的格式化字符串 63 | * @param {Date} startTime - 开始时间 64 | * @param {Date} endTime - 结束时间 65 | * @returns {string} 格式化的时间字符串 66 | */ 67 | static getElapsedTimeString(startTime, endTime) { 68 | const elapsedMs = endTime - startTime; 69 | const seconds = Math.floor(elapsedMs / 1000) % 60; 70 | const minutes = Math.floor(elapsedMs / (1000 * 60)) % 60; 71 | const hours = Math.floor(elapsedMs / (1000 * 60 * 60)); 72 | 73 | return `${hours}小时${minutes}分${seconds}秒`; 74 | } 75 | 76 | /** 77 | * 格式化账户信息显示 78 | * @param {Object} data - 账户数据 79 | * @returns {string} 格式化的账户信息文本 80 | */ 81 | static formatAccountInfo(data) { 82 | const { 83 | timeNow, 84 | symbol, 85 | scriptStartTime, 86 | elapsedTime, 87 | wsStatusInfo, 88 | priceInfo, 89 | priceChangeSymbol, 90 | increase, 91 | takeProfitPercentage, 92 | percentProgress, 93 | stats, 94 | tradingCoin, 95 | currentValue, 96 | profit, 97 | profitPercent, 98 | priceSource 99 | } = data; 100 | 101 | let display = '===== Backpack 自动交易系统 =====\n'; 102 | display += `当前时间: ${timeNow}\n`; 103 | display += `交易对: ${symbol}\n`; 104 | display += `脚本启动时间: ${scriptStartTime}\n`; 105 | display += `运行时间: ${elapsedTime}\n`; 106 | 107 | display += `\n===== 订单统计 =====\n`; 108 | // 添加WebSocket和价格信息到订单统计 109 | display += `WebSocket: ${wsStatusInfo}\n`; 110 | display += `当前价格: ${priceInfo}\n`; 111 | display += `涨跌幅: ${priceChangeSymbol} ${Math.abs(increase || 0).toFixed(2)}%\n`; 112 | display += `止盈目标: ${takeProfitPercentage}%\n`; 113 | display += `完成进度: ${percentProgress}%\n`; 114 | display += `总订单数: ${stats.totalOrders}\n`; 115 | display += `已成交订单: ${stats.filledOrders}\n`; 116 | display += `成交总金额: ${stats.totalFilledAmount.toFixed(2)} USDC\n`; 117 | display += `成交总数量: ${stats.totalFilledQuantity.toFixed(6)} ${tradingCoin}\n`; 118 | display += `平均成交价: ${stats.averagePrice.toFixed(2)} USDC\n`; 119 | 120 | // 显示盈亏情况 121 | if (stats.filledOrders > 0 && stats.totalFilledQuantity > 0) { 122 | const profitSymbol = profit >= 0 ? "↑" : "↓"; 123 | 124 | display += `当前持仓价值: ${currentValue.toFixed(2)} USDC\n`; 125 | display += `盈亏金额: ${profitSymbol} ${Math.abs(profit).toFixed(2)} USDC\n`; 126 | display += `盈亏百分比: ${profitSymbol} ${Math.abs(profitPercent).toFixed(2)}%\n`; 127 | } 128 | 129 | display += `最后更新: ${new Date().toLocaleString()}\n`; 130 | 131 | return display; 132 | } 133 | } 134 | 135 | module.exports = Formatter; -------------------------------------------------------------------------------- /src/core/tradingStrategy.js: -------------------------------------------------------------------------------- 1 | const Formatter = require('../utils/formatter'); 2 | const { log } = require('../utils/logger'); 3 | const { Order } = require('../models/Order'); 4 | 5 | /** 6 | * 交易策略类 - 负责计算交易策略和订单 7 | */ 8 | class TradingStrategy { 9 | /** 10 | * 构造函数 11 | * @param {Object} logger - 日志对象 12 | * @param {Object} config - 配置对象(可选) 13 | */ 14 | constructor(logger, config = {}) { 15 | this.logger = logger; 16 | this.config = config; 17 | } 18 | 19 | /** 20 | * 计算递增订单 21 | * @param {number} currentPrice - 当前市场价格 22 | * @param {number} maxDropPercentage - 最大跌幅百分比 23 | * @param {number} totalAmount - 总投资金额 24 | * @param {number} orderCount - 订单数量 25 | * @param {number} incrementPercentage - 递增百分比 26 | * @param {number} minOrderAmount - 最小订单金额 27 | * @param {string} tradingCoin - 交易币种 28 | * @param {string} symbol - 交易对符号 29 | * @returns {Array} 订单列表 30 | */ 31 | calculateIncrementalOrders( 32 | currentPrice, 33 | maxDropPercentage, 34 | totalAmount, 35 | orderCount, 36 | incrementPercentage, 37 | minOrderAmount, 38 | tradingCoin, 39 | symbol 40 | ) { 41 | const orders = []; 42 | 43 | // 计算价格区间 44 | const lowestPrice = currentPrice * (1 - maxDropPercentage / 100); 45 | const priceStep = (currentPrice - lowestPrice) / (orderCount - 1); 46 | 47 | // 计算基础订单金额(使用等比数列求和公式) 48 | // 总金额 = 基础金额 * (1 + r + r^2 + ... + r^(n-1)) 49 | // 总金额 = 基础金额 * (1 - r^n) / (1 - r) 50 | // 基础金额 = 总金额 * (1 - r) / (1 - r^n) 51 | const r = 1 + incrementPercentage / 100; // 递增比例 52 | 53 | // 确保基础订单金额不小于最小订单金额 54 | const calculatedBaseAmount = totalAmount * (r - 1) / (Math.pow(r, orderCount) - 1); 55 | const baseAmount = Math.max(minOrderAmount, calculatedBaseAmount); 56 | 57 | // 计算实际总金额 58 | let actualTotalAmount = 0; 59 | for (let i = 0; i < orderCount; i++) { 60 | actualTotalAmount += baseAmount * Math.pow(r, i); 61 | } 62 | 63 | // 处理实际总金额超过用户输入的总金额的情况 64 | const orderAmounts = []; 65 | const scale = actualTotalAmount > totalAmount ? totalAmount / actualTotalAmount : 1; 66 | 67 | // 创建订单 68 | for (let i = 0; i < orderCount; i++) { 69 | // 计算当前订单价格 70 | const rawPrice = currentPrice - (priceStep * i); 71 | // 调整价格到交易所接受的格式 72 | const price = Formatter.adjustPriceToTickSize(rawPrice, tradingCoin, this.config); 73 | 74 | // 计算当前订单金额(递增并缩放) 75 | const orderAmount = baseAmount * Math.pow(r, i) * scale; 76 | 77 | // 计算数量并调整精度 78 | const quantity = Formatter.adjustQuantityToStepSize(orderAmount / price, tradingCoin, this.config); 79 | const actualAmount = price * quantity; 80 | 81 | // 只有当订单金额满足最小要求时才添加 82 | if (actualAmount >= minOrderAmount) { 83 | const orderData = { 84 | symbol, 85 | price, 86 | quantity, 87 | amount: actualAmount, 88 | side: 'Bid', 89 | orderType: 'Limit', 90 | timeInForce: 'GTC' 91 | }; 92 | 93 | const order = new Order(orderData); 94 | orders.push(order); 95 | 96 | orderAmounts.push(actualAmount); 97 | } 98 | } 99 | 100 | // 如果没有生成任何订单,抛出错误 101 | if (orders.length === 0) { 102 | throw new Error('无法生成有效订单,请检查输入参数'); 103 | } 104 | 105 | // 计算实际总金额 106 | const finalTotalAmount = orderAmounts.reduce((sum, amount) => sum + amount, 0); 107 | 108 | log(`计划总金额: ${totalAmount.toFixed(2)} USDC`); 109 | log(`实际总金额: ${finalTotalAmount.toFixed(2)} USDC`); 110 | 111 | return orders; 112 | } 113 | 114 | /** 115 | * 检查是否达到止盈条件 116 | * @param {number} currentPrice - 当前价格 117 | * @param {number} averagePrice - 平均买入价格 118 | * @param {number} takeProfitPercentage - 止盈百分比 119 | * @returns {boolean} 是否达到止盈条件 120 | */ 121 | isTakeProfitTriggered(currentPrice, averagePrice, takeProfitPercentage) { 122 | if (!currentPrice || !averagePrice || averagePrice <= 0) { 123 | return false; 124 | } 125 | 126 | // 计算价格涨幅百分比 127 | const priceIncrease = ((currentPrice - averagePrice) / averagePrice) * 100; 128 | 129 | // 判断是否达到止盈条件 130 | return priceIncrease >= takeProfitPercentage; 131 | } 132 | 133 | /** 134 | * 计算最优卖出价格 135 | * @param {number} currentPrice - 当前市场价格 136 | * @param {string} tradingCoin - 交易币种 137 | * @returns {number} 最优卖出价格 138 | */ 139 | calculateOptimalSellPrice(currentPrice, tradingCoin) { 140 | // 设置卖出价格略低于市场价(确保能够成交) 141 | return Formatter.adjustPriceToTickSize(currentPrice * 0.995, tradingCoin, this.config); 142 | } 143 | 144 | /** 145 | * 计算第二次卖出价格(更低) 146 | * @param {number} currentPrice - 当前市场价格 147 | * @param {string} tradingCoin - 交易币种 148 | * @returns {number} 二次卖出价格 149 | */ 150 | calculateSecondSellPrice(currentPrice, tradingCoin) { 151 | // 使用更低的价格进行二次尝试(原价格的99%) 152 | return Formatter.adjustPriceToTickSize(currentPrice * 0.99, tradingCoin, this.config); 153 | } 154 | 155 | /** 156 | * 计算进度百分比 157 | * @param {number} currentPrice - 当前价格 158 | * @param {number} averagePrice - 平均买入价格 159 | * @param {number} takeProfitPercentage - 止盈百分比 160 | * @returns {number} 完成进度百分比 161 | */ 162 | calculateProgressPercentage(currentPrice, averagePrice, takeProfitPercentage) { 163 | if (!currentPrice || !averagePrice || averagePrice <= 0 || takeProfitPercentage <= 0) { 164 | return 0; 165 | } 166 | 167 | // 计算价格涨幅百分比 168 | const priceIncrease = ((currentPrice - averagePrice) / averagePrice) * 100; 169 | 170 | // 计算进度百分比,限制在0-100之间 171 | return Math.min(100, Math.max(0, (priceIncrease / takeProfitPercentage * 100))); 172 | } 173 | } 174 | 175 | module.exports = TradingStrategy; -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | /** 5 | * 日志工具类 - 负责记录日志到控制台和文件 6 | */ 7 | class Logger { 8 | /** 9 | * 构造函数 10 | * @param {Object} options - 日志选项 11 | * @param {string} options.logDir - 日志目录 12 | * @param {string} options.prefix - 日志文件前缀 13 | */ 14 | constructor(options = {}) { 15 | this.logDir = options.logDir || path.join(process.cwd(), 'logs'); 16 | this.prefix = options.prefix || 'trading'; 17 | 18 | // 确保日志目录存在 19 | if (!fs.existsSync(this.logDir)) { 20 | fs.mkdirSync(this.logDir, { recursive: true }); 21 | } 22 | } 23 | 24 | /** 25 | * 获取当前日期字符串,格式为YYYY-MM-DD 26 | * @returns {string} 日期字符串 27 | */ 28 | getDateString() { 29 | return new Date().toISOString().split('T')[0]; 30 | } 31 | 32 | /** 33 | * 记录日志 34 | * @param {string} message - 日志消息 35 | * @param {boolean} isError - 是否为错误日志 36 | * @param {boolean} displayOnConsole - 是否在控制台显示 37 | */ 38 | log(message, isError = false, displayOnConsole = true) { 39 | const timestamp = new Date().toLocaleString(); 40 | const logMessage = `[${timestamp}] ${message}`; 41 | 42 | // 根据参数决定是否在控制台显示 43 | if (displayOnConsole) { 44 | if (isError) { 45 | console.error(logMessage); 46 | } else { 47 | console.log(logMessage); 48 | } 49 | } 50 | 51 | // 获取当前日期 52 | const date = this.getDateString(); 53 | 54 | // 生成普通日志文件路径 55 | const logFile = path.join(this.logDir, `${this.prefix}_${date}.log`); 56 | 57 | // 写入普通日志 58 | fs.appendFileSync( 59 | logFile, 60 | logMessage + '\n', 61 | { encoding: 'utf8' } 62 | ); 63 | 64 | // 如果是错误,还要写入错误日志 65 | if (isError) { 66 | const errorLogFile = path.join(this.logDir, `error_${date}.log`); 67 | fs.appendFileSync( 68 | errorLogFile, 69 | logMessage + '\n', 70 | { encoding: 'utf8' } 71 | ); 72 | } 73 | } 74 | 75 | /** 76 | * 只记录到日志文件,不在控制台显示 77 | * @param {string} message - 日志消息 78 | * @param {boolean} isError - 是否为错误日志 79 | */ 80 | logToFile(message, isError = false) { 81 | this.log(message, isError, false); 82 | } 83 | 84 | /** 85 | * 创建交易周期日志文件 86 | * @returns {string} 周期日志文件路径 87 | */ 88 | createCycleLogFile() { 89 | const date = this.getDateString(); 90 | const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); 91 | const cycleLogFile = path.join(this.logDir, `auto_trading_cycle_${date}_${timestamp}.log`); 92 | return cycleLogFile; 93 | } 94 | 95 | /** 96 | * 记录订单到交易周期日志 97 | * @param {string} logFile - 周期日志文件路径 98 | * @param {Object} order - 订单信息 99 | * @param {Object} config - 配置信息 100 | */ 101 | logOrderToCycle(logFile, order, config) { 102 | try { 103 | if (!logFile) return; 104 | 105 | const timestamp = new Date().toLocaleString(); 106 | const logEntry = { 107 | timestamp, 108 | orderId: order.id, 109 | symbol: order.symbol || config.symbol, 110 | price: order.price, 111 | quantity: order.quantity, 112 | status: order.status, 113 | side: order.side, 114 | filled: order.status === 'Filled' 115 | }; 116 | 117 | fs.appendFileSync( 118 | logFile, 119 | JSON.stringify(logEntry) + '\n', 120 | { encoding: 'utf8' } 121 | ); 122 | } catch (error) { 123 | this.log(`记录订单到周期日志失败: ${error.message}`, true, false); 124 | } 125 | } 126 | 127 | /** 128 | * 专门记录API错误的方法 129 | * @param {Error} error - 错误对象 130 | * @param {string} context - 错误上下文描述 131 | * @param {Object} params - API调用参数 132 | */ 133 | logApiError(error, context, params = {}) { 134 | // 构建基本错误信息 135 | let messages = []; 136 | messages.push(`[API错误] ${context}: ${error.message}`); 137 | 138 | // 记录API调用参数 139 | if (Object.keys(params).length > 0) { 140 | messages.push(`请求参数: ${JSON.stringify(params)}`); 141 | } 142 | 143 | // 处理响应错误 144 | if (error.response) { 145 | const { status, statusText, data } = error.response; 146 | messages.push(`响应状态: ${status} (${statusText || 'No status text'})`); 147 | 148 | // 记录响应数据 149 | if (data) { 150 | messages.push(`响应数据: ${JSON.stringify(data)}`); 151 | 152 | // 提取错误信息 153 | if (data.message) messages.push(`错误消息: ${data.message}`); 154 | if (data.code) messages.push(`错误代码: ${data.code}`); 155 | if (data.error) messages.push(`错误详情: ${JSON.stringify(data.error)}`); 156 | } 157 | } 158 | 159 | // 记录请求信息 160 | if (error.request) { 161 | const { method, path, headers } = error.request; 162 | messages.push(`请求方法: ${method || 'N/A'}`); 163 | messages.push(`请求URL: ${path || 'N/A'}`); 164 | 165 | // 可选: 记录请求头 (可能包含敏感信息,谨慎使用) 166 | // messages.push(`请求头: ${JSON.stringify(headers || {})}`); 167 | } 168 | 169 | // 记录错误堆栈 170 | if (error.stack) { 171 | messages.push(`堆栈: ${error.stack}`); 172 | } 173 | 174 | // 将所有信息写入日志 175 | messages.forEach(msg => this.log(msg, true)); 176 | 177 | // 同时写入错误专用日志文件 178 | const date = this.getDateString(); 179 | const apiErrorLogFile = path.join(this.logDir, `api_error_${date}.log`); 180 | 181 | fs.appendFileSync( 182 | apiErrorLogFile, 183 | `\n--- API错误 [${new Date().toISOString()}] ---\n${messages.join('\n')}\n`, 184 | { encoding: 'utf8' } 185 | ); 186 | } 187 | } 188 | 189 | // 创建默认实例 190 | const defaultLogger = new Logger(); 191 | 192 | module.exports = { 193 | Logger, 194 | defaultLogger, 195 | log: (...args) => defaultLogger.log(...args), 196 | logToFile: (...args) => defaultLogger.logToFile(...args) 197 | }; -------------------------------------------------------------------------------- /src/models/Order.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 订单类 - 负责管理单个订单的信息 3 | */ 4 | class Order { 5 | /** 6 | * 构造函数 7 | * @param {Object} orderData - 订单数据 8 | */ 9 | constructor(orderData = {}) { 10 | this.id = orderData.id; 11 | this.symbol = orderData.symbol; 12 | this.price = parseFloat(orderData.price); 13 | this.quantity = parseFloat(orderData.quantity); 14 | this.amount = this.price * this.quantity; 15 | this.side = orderData.side || 'Bid'; // Bid: 买入, Ask: 卖出 16 | this.orderType = orderData.orderType || 'Limit'; 17 | this.timeInForce = orderData.timeInForce || 'GTC'; 18 | this.status = orderData.status || 'New'; 19 | this.filledQuantity = parseFloat(orderData.filledQuantity || 0); 20 | this.filledAmount = parseFloat(orderData.filledAmount || 0); 21 | this.createTime = orderData.createTime || new Date(); 22 | this.updateTime = orderData.updateTime || new Date(); 23 | this.processed = orderData.processed || false; 24 | } 25 | 26 | /** 27 | * 更新订单信息 28 | * @param {Object} data - 更新的数据 29 | */ 30 | update(data) { 31 | if (!data) return; 32 | 33 | if (data.status) this.status = data.status; 34 | if (data.filledQuantity) this.filledQuantity = parseFloat(data.filledQuantity); 35 | if (data.filledAmount) this.filledAmount = parseFloat(data.filledAmount); 36 | if (data.processed !== undefined) this.processed = data.processed; 37 | 38 | this.updateTime = new Date(); 39 | } 40 | 41 | /** 42 | * 检查订单是否已成交 43 | * @returns {boolean} 是否已成交 44 | */ 45 | isFilled() { 46 | return this.status === 'Filled'; 47 | } 48 | 49 | /** 50 | * 检查订单是否部分成交 51 | * @returns {boolean} 是否部分成交 52 | */ 53 | isPartiallyFilled() { 54 | return this.status === 'PartiallyFilled'; 55 | } 56 | 57 | /** 58 | * 检查订单是否已处理(计入统计) 59 | * @returns {boolean} 是否已处理 60 | */ 61 | isProcessed() { 62 | return this.processed; 63 | } 64 | 65 | /** 66 | * 标记订单为已处理 67 | */ 68 | markAsProcessed() { 69 | this.processed = true; 70 | } 71 | 72 | /** 73 | * 获取订单签名(用于防止重复创建) 74 | * @returns {string} 订单签名 75 | */ 76 | getSignature() { 77 | return `${this.price}_${this.quantity}`; 78 | } 79 | 80 | /** 81 | * 从对象创建订单实例 82 | * @param {Object} data - 订单数据 83 | * @returns {Order} 订单实例 84 | */ 85 | static fromObject(data) { 86 | return new Order(data); 87 | } 88 | 89 | /** 90 | * 转换为API格式的订单参数 91 | * @returns {Object} API格式的订单参数 92 | */ 93 | toApiParams() { 94 | return { 95 | symbol: this.symbol, 96 | side: this.side, 97 | orderType: this.orderType, 98 | quantity: this.quantity.toString(), 99 | price: this.price.toString(), 100 | timeInForce: this.timeInForce 101 | }; 102 | } 103 | } 104 | 105 | /** 106 | * 订单管理器类 - 负责管理多个订单 107 | */ 108 | class OrderManager { 109 | /** 110 | * 构造函数 111 | */ 112 | constructor() { 113 | this.orders = new Map(); 114 | this.createdOrderSignatures = new Set(); 115 | this.pendingOrderIds = new Set(); 116 | this.allCreatedOrderIds = new Set(); 117 | } 118 | 119 | /** 120 | * 添加订单 121 | * @param {Order} order - 订单实例 122 | * @returns {boolean} 是否成功添加 123 | */ 124 | addOrder(order) { 125 | if (!order || !order.id) return false; 126 | 127 | this.orders.set(order.id, order); 128 | this.allCreatedOrderIds.add(order.id); 129 | this.createdOrderSignatures.add(order.getSignature()); 130 | 131 | // 如果订单状态不是已成交,则添加到待处理列表 132 | if (order.status !== 'Filled') { 133 | this.pendingOrderIds.add(order.id); 134 | } 135 | 136 | return true; 137 | } 138 | 139 | /** 140 | * 获取订单 141 | * @param {string} orderId - 订单ID 142 | * @returns {Order|undefined} 订单实例或undefined 143 | */ 144 | getOrder(orderId) { 145 | return this.orders.get(orderId); 146 | } 147 | 148 | /** 149 | * 更新订单状态 150 | * @param {string} orderId - 订单ID 151 | * @param {Object} data - 更新的数据 152 | * @returns {boolean} 是否成功更新 153 | */ 154 | updateOrder(orderId, data) { 155 | const order = this.orders.get(orderId); 156 | if (!order) return false; 157 | 158 | order.update(data); 159 | 160 | // 如果订单成交,从待处理列表移除 161 | if (order.isFilled()) { 162 | this.pendingOrderIds.delete(orderId); 163 | } 164 | 165 | return true; 166 | } 167 | 168 | /** 169 | * 检查签名是否已存在(防止重复创建订单) 170 | * @param {string} signature - 订单签名 171 | * @returns {boolean} 是否已存在 172 | */ 173 | hasOrderSignature(signature) { 174 | return this.createdOrderSignatures.has(signature); 175 | } 176 | 177 | /** 178 | * 重置所有订单数据 179 | */ 180 | reset() { 181 | this.orders.clear(); 182 | this.createdOrderSignatures.clear(); 183 | this.pendingOrderIds.clear(); 184 | this.allCreatedOrderIds.clear(); 185 | } 186 | 187 | /** 188 | * 获取待处理的订单ID列表 189 | * @returns {Array} 待处理的订单ID列表 190 | */ 191 | getPendingOrderIds() { 192 | return Array.from(this.pendingOrderIds); 193 | } 194 | 195 | /** 196 | * 获取所有已创建的订单ID列表 197 | * @returns {Array} 所有已创建的订单ID列表 198 | */ 199 | getAllCreatedOrderIds() { 200 | return Array.from(this.allCreatedOrderIds); 201 | } 202 | 203 | /** 204 | * 移除待处理订单ID 205 | * @param {string} orderId - 订单ID 206 | */ 207 | removePendingOrderId(orderId) { 208 | this.pendingOrderIds.delete(orderId); 209 | } 210 | 211 | /** 212 | * 更新待处理订单ID列表 213 | * @param {Array} orderIds - 新的待处理订单ID列表 214 | */ 215 | updatePendingOrderIds(orderIds) { 216 | this.pendingOrderIds = new Set(orderIds); 217 | } 218 | 219 | /** 220 | * 获取所有订单列表 221 | * @returns {Array} 所有订单列表 222 | */ 223 | getAllOrders() { 224 | return Array.from(this.orders.values()); 225 | } 226 | } 227 | 228 | module.exports = { 229 | Order, 230 | OrderManager 231 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backpack 自动化递增买入交易系统 2 | 3 | 这是一个基于Backpack交易所API的自动化交易系统,专注于实现递增买入策略并自动止盈。 4 | 5 | ## 目录结构 6 | 7 | 系统采用模块化架构设计,目录结构如下: 8 | 9 | ``` 10 | ├── src/ # 源代码目录 11 | │ ├── config/ # 配置模块 12 | │ │ └── configLoader.js # 配置加载器 13 | │ ├── core/ # 核心业务逻辑 14 | │ │ ├── orderManager.js # 订单管理服务 15 | │ │ ├── priceMonitor.js # 价格监控器 16 | │ │ └── tradingStrategy.js # 交易策略 17 | │ ├── models/ # 数据模型 18 | │ │ ├── Order.js # 订单模型 19 | │ │ └── TradeStats.js # 交易统计模型 20 | │ ├── network/ # 网络相关 21 | │ │ └── webSocketManager.js # WebSocket管理器 22 | │ ├── services/ # 服务层 23 | │ │ └── backpackService.js # Backpack API服务 24 | │ ├── utils/ # 工具类 25 | │ │ ├── formatter.js # 格式化工具 26 | │ │ ├── logger.js # 日志工具 27 | │ │ └── timeUtils.js # 时间工具 28 | │ ├── app.js # 应用程序主类 29 | │ └── index.js # 程序入口 30 | ├── backpack_trading_config.json # 交易配置文件 31 | ├── test_create_orders_auto.js # 原始交易脚本(保留兼容) 32 | ├── start_auto_trading.js # 自动化启动脚本 33 | ├── start_modular_trading.js # 模块化版本启动脚本 34 | └── README.md # 项目文档 35 | ``` 36 | 37 | ## 配置说明 38 | 39 | 系统通过`backpack_trading_config.json`文件进行配置,主要配置项包括: 40 | 41 | ### API配置 42 | ```json 43 | "api": { 44 | "privateKey": "YOUR_PRIVATE_KEY", 45 | "publicKey": "YOUR_PUBLIC_KEY" 46 | } 47 | ``` 48 | 49 | ### 交易配置 50 | ```json 51 | "trading": { 52 | "tradingCoin": "SOL", // 交易币种 53 | "totalAmount": 500, // 总投资金额(USDC) 54 | "orderCount": 10, // 订单数量 55 | "maxDropPercentage": 5, // 最大下跌百分比 56 | "incrementPercentage": 1.5, // 订单间隔递增百分比 57 | "takeProfitPercentage": 2 // 止盈百分比 58 | } 59 | ``` 60 | 61 | ### 功能开关 62 | ```json 63 | "actions": { 64 | "sellNonUsdcAssets": false, // 是否卖出非USDC资产 65 | "cancelAllOrders": true, // 启动时是否撤销现有订单 66 | "restartAfterTakeProfit": true, // 止盈后是否自动重启 67 | "autoRestartNoFill": true, // 无订单成交时是否自动重启 68 | "executeTrade": true, // 是否执行交易 69 | "cancelOrdersOnExit": true // 退出时是否撤销未成交订单 70 | } 71 | ``` 72 | 73 | ### 高级配置 74 | ```json 75 | "advanced": { 76 | "checkOrdersIntervalMinutes": 5, // 检查订单状态间隔(分钟) 77 | "monitorIntervalSeconds": 15, // 价格监控间隔(秒) 78 | "noFillRestartMinutes": 60, // 无订单成交重启时间(分钟) 79 | "minOrderAmount": 10, // 最小订单金额(USDC) 80 | "priceTickSize": 0.01, // 价格最小变动单位 81 | "sellNonUsdcMinValue": 10, // 非USDC资产最小卖出价值 82 | } 83 | ``` 84 | 85 | ### WebSocket配置 86 | ```json 87 | "websocket": { 88 | "url": "wss://ws.backpack.exchange" // WebSocket端点 89 | } 90 | ``` 91 | 92 | ### 精度配置 93 | ```json 94 | "minQuantities": { 95 | "DEFAULT": 0.01, 96 | "SOL": 0.1, 97 | "JUP": 1, 98 | "BTC": 0.00001, 99 | "ETH": 0.001 100 | }, 101 | "quantityPrecisions": { 102 | "DEFAULT": 2, 103 | "SOL": 2, 104 | "JUP": 0, 105 | "BTC": 5, 106 | "ETH": 4 107 | }, 108 | "pricePrecisions": { 109 | "DEFAULT": 2, 110 | "SOL": 2, 111 | "JUP": 3, 112 | "BTC": 0, 113 | "ETH": 2 114 | } 115 | ``` 116 | 117 | ## 系统功能 118 | 119 | 1. **递增买入策略**:基于配置创建多个价格递减的买入订单,随价格下跌增加买入量 120 | 2. **实时价格监控**:使用WebSocket连接实时监控市场价格变动 121 | 3. **自动止盈**:达到预设止盈点自动卖出获利 122 | 4. **统计分析**:实时计算和显示交易统计数据,包括成交量、均价和盈亏情况 123 | 5. **失败重试**:订单创建失败时自动重试 124 | 6. **安全退出**:优雅处理进程退出,支持自动撤单 125 | 7. **自动重启**:无订单成交或止盈后自动重启新一轮交易 126 | 127 | ## 使用方法 128 | 129 | ### 安装依赖 130 | ```bash 131 | npm install 132 | ``` 133 | 134 | ### 配置 135 | 编辑`backpack_trading_config.json`文件,设置您的API密钥和交易参数。 136 | 137 | ### 运行 138 | ```bash 139 | # 直接运行模块化版本 140 | node src/index.js 141 | 142 | # 或使用模块化版自动启动脚本(带重启功能) 143 | node start_modular_trading.js 144 | 145 | # 或使用原有脚本(保持兼容性) 146 | node start_auto_trading.js 147 | ``` 148 | 149 | ## 注意事项 150 | 151 | 1. **API密钥安全**:请确保您的API密钥安全,不要在公共环境中泄露 152 | 2. **投资风险**:加密货币交易存在高风险,请根据您的风险承受能力谨慎投资 153 | 3. **测试验证**:建议先使用小额资金进行测试,确认系统正常工作后再增加投资金额 154 | 4. **监控运行**:系统运行期间建议定期检查,确保一切正常 155 | 156 | ## 交易策略说明 157 | 158 | 本系统实现的递增买入策略基于以下原理: 159 | 160 | 1. 在当前价格以下设置多个买入订单,价格逐步降低 161 | 2. 随着价格降低,买入金额逐步增加,形成递增买入 162 | 3. 当价格回升至平均买入价以上一定比例时,自动卖出获利 163 | 164 | 这种策略适合在震荡行情中使用,可以降低平均买入成本,提高盈利机会。 165 | 166 | ## 开发与贡献 167 | 168 | ### 模块化设计 169 | 系统采用模块化设计,使代码结构清晰、易于维护和扩展: 170 | 171 | - **配置模块**:负责加载和验证配置 172 | - **核心模块**:实现交易策略、订单管理和价格监控 173 | - **模型模块**:定义数据模型和状态 174 | - **网络模块**:处理WebSocket通信 175 | - **服务模块**:封装API调用 176 | - **工具模块**:提供通用功能 177 | 178 | ### 添加新功能 179 | 如需添加新功能或支持新的交易策略,建议按以下步骤进行: 180 | 181 | 1. 在对应模块下创建新的组件 182 | 2. 在配置中添加相关参数 183 | 3. 在应用层集成新功能 184 | 4. 充分测试后再投入使用 185 | 186 | ## 免责声明 187 | 188 | 本项目仅供学习和研究使用,作者不对使用本系统进行交易造成的任何损失负责。使用前请充分了解加密货币交易的风险,并自行承担全部责任。 189 | 190 | ## 许可证 191 | 192 | MIT 193 | 194 | ## 项目文件说明 195 | 196 | 以下是项目中所有重要的JavaScript文件及其简要说明: 197 | 198 | ### 核心代码文件(src目录) 199 | 200 | #### 主要入口文件 201 | | 文件 | 大小 | 说明 | 202 | |------|------|------| 203 | | `src/index.js` | 4.4KB | 程序主入口,负责启动应用并处理程序生命周期 | 204 | | `src/app.js` | 27.5KB | 应用程序核心逻辑,协调各组件工作 | 205 | 206 | #### 核心功能模块 207 | | 文件 | 大小 | 说明 | 208 | |------|------|------| 209 | | `src/core/orderManager.js` | 12.7KB | 订单管理器,负责订单的创建、取消和跟踪 | 210 | | `src/core/priceMonitor.js` | 8.7KB | 价格监控器,负责实时监控价格变动 | 211 | | `src/core/tradingStrategy.js` | 6.0KB | 交易策略,实现递增买入和止盈策略 | 212 | 213 | #### 数据模型 214 | | 文件 | 大小 | 说明 | 215 | |------|------|------| 216 | | `src/models/Order.js` | 5.5KB | 订单模型,定义订单数据结构和操作 | 217 | | `src/models/TradeStats.js` | 4.3KB | 交易统计,跟踪和计算交易统计数据 | 218 | 219 | #### 网络服务 220 | | 文件 | 大小 | 说明 | 221 | |------|------|------| 222 | | `src/network/webSocketManager.js` | 12.2KB | WebSocket管理器,处理实时价格数据订阅 | 223 | | `src/services/backpackService.js` | 9.9KB | Backpack交易所API服务,封装API调用 | 224 | 225 | #### 工具类 226 | | 文件 | 大小 | 说明 | 227 | |------|------|------| 228 | | `src/utils/logger.js` | 5.7KB | 日志记录工具,处理日志输出和保存 | 229 | | `src/utils/formatter.js` | 5.0KB | 数据格式化工具,处理价格和数量格式 | 230 | | `src/utils/timeUtils.js` | 3.1KB | 时间工具,提供时间相关功能 | 231 | 232 | #### 配置管理 233 | | 文件 | 大小 | 说明 | 234 | |------|------|------| 235 | | `src/config/configLoader.js` | 3.5KB | 配置加载器,负责读取和验证配置 | 236 | 237 | ### 根目录脚本文件 238 | 239 | #### 启动脚本 240 | | 文件 | 大小 | 说明 | 241 | |------|------|------| 242 | | `start_modular_trading.js` | 3.6KB | 模块化交易启动脚本,支持自动重启 | 243 | | `start_auto_trading.js` | 1.5KB | 自动交易启动脚本(兼容旧版) | 244 | 245 | #### Backpack交易所API相关 246 | | 文件 | 大小 | 说明 | 247 | |------|------|------| 248 | | `backpack_api.js` | 24.9KB | Backpack API包装器,提供API调用功能 | 249 | | `backpack_client.js` | 16.8KB | Backpack客户端,底层API客户端实现 | 250 | 251 | #### 测试和工具脚本 252 | | 文件 | 大小 | 说明 | 253 | |------|------|------| 254 | | `test_create_orders_auto.js` | 134.3KB | 自动创建订单测试脚本(完整实现) | 255 | | `test_websocket.js` | 23.1KB | WebSocket测试脚本,测试行情订阅 | 256 | | `test_websocket2.js` | 16.8KB | WebSocket测试脚本2,替代实现 | 257 | | `backpack_price_reader.js` | 9.4KB | 价格读取工具,独立获取价格数据 | 258 | | `btc_order_test.js` | 11.6KB | BTC订单测试,测试BTC交易对订单 | 259 | | `backpack_public_api_test.js` | 13.0KB | 公共API测试,测试行情等公共接口 | 260 | | `backpack_ws_tester.js` | 5.6KB | WebSocket测试器,测试连接和订阅 | 261 | | `test.js` | 0.05KB | 简单测试文件 | 262 | 263 | 生产环境主要使用src目录下的代码,通过`start_modular_trading.js`或`src/index.js`启动。根目录下的测试脚本主要用于开发和调试过程。 -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Backpack 自动化递增买入交易系统 2 | 3 | 这是一个基于Backpack交易所API的自动化交易系统,专注于实现递增买入策略并自动止盈。 4 | 5 | ## 目录结构 6 | 7 | 系统采用模块化架构设计,目录结构如下: 8 | 9 | ``` 10 | ├── src/ # 源代码目录 11 | │ ├── config/ # 配置模块 12 | │ │ └── configLoader.js # 配置加载器 13 | │ ├── core/ # 核心业务逻辑 14 | │ │ ├── orderManager.js # 订单管理服务 15 | │ │ ├── priceMonitor.js # 价格监控器 16 | │ │ └── tradingStrategy.js # 交易策略 17 | │ ├── models/ # 数据模型 18 | │ │ ├── Order.js # 订单模型 19 | │ │ └── TradeStats.js # 交易统计模型 20 | │ ├── network/ # 网络相关 21 | │ │ └── webSocketManager.js # WebSocket管理器 22 | │ ├── services/ # 服务层 23 | │ │ └── backpackService.js # Backpack API服务 24 | │ ├── utils/ # 工具类 25 | │ │ ├── formatter.js # 格式化工具 26 | │ │ ├── logger.js # 日志工具 27 | │ │ └── timeUtils.js # 时间工具 28 | │ ├── app.js # 应用程序主类 29 | │ └── index.js # 程序入口 30 | ├── backpack_trading_config.json # 交易配置文件 31 | ├── test_create_orders_auto.js # 原始交易脚本(保留兼容) 32 | ├── start_auto_trading.js # 自动化启动脚本 33 | ├── start_modular_trading.js # 模块化版本启动脚本 34 | └── README.md # 项目文档 35 | ``` 36 | 37 | ## 配置说明 38 | 39 | 系统通过`backpack_trading_config.json`文件进行配置,主要配置项包括: 40 | 41 | ### API配置 42 | ```json 43 | "api": { 44 | "privateKey": "YOUR_PRIVATE_KEY", 45 | "publicKey": "YOUR_PUBLIC_KEY" 46 | } 47 | ``` 48 | 49 | ### 交易配置 50 | ```json 51 | "trading": { 52 | "tradingCoin": "SOL", // 交易币种 53 | "totalAmount": 500, // 总投资金额(USDC) 54 | "orderCount": 10, // 订单数量 55 | "maxDropPercentage": 5, // 最大下跌百分比 56 | "incrementPercentage": 1.5, // 订单间隔递增百分比 57 | "takeProfitPercentage": 2 // 止盈百分比 58 | } 59 | ``` 60 | 61 | ### 功能开关 62 | ```json 63 | "actions": { 64 | "sellNonUsdcAssets": false, // 是否卖出非USDC资产 65 | "cancelAllOrders": true, // 启动时是否撤销现有订单 66 | "restartAfterTakeProfit": true, // 止盈后是否自动重启 67 | "autoRestartNoFill": true, // 无订单成交时是否自动重启 68 | "executeTrade": true, // 是否执行交易 69 | "cancelOrdersOnExit": true // 退出时是否撤销未成交订单 70 | } 71 | ``` 72 | 73 | ### 高级配置 74 | ```json 75 | "advanced": { 76 | "checkOrdersIntervalMinutes": 5, // 检查订单状态间隔(分钟) 77 | "monitorIntervalSeconds": 15, // 价格监控间隔(秒) 78 | "noFillRestartMinutes": 60, // 无订单成交重启时间(分钟) 79 | "minOrderAmount": 10, // 最小订单金额(USDC) 80 | "priceTickSize": 0.01, // 价格最小变动单位 81 | "sellNonUsdcMinValue": 10, // 非USDC资产最小卖出价值 82 | } 83 | ``` 84 | 85 | ### WebSocket配置 86 | ```json 87 | "websocket": { 88 | "url": "wss://ws.backpack.exchange" // WebSocket端点 89 | } 90 | ``` 91 | 92 | ### 精度配置 93 | ```json 94 | "minQuantities": { 95 | "DEFAULT": 0.01, 96 | "SOL": 0.1, 97 | "JUP": 1, 98 | "BTC": 0.00001, 99 | "ETH": 0.001 100 | }, 101 | "quantityPrecisions": { 102 | "DEFAULT": 2, 103 | "SOL": 2, 104 | "JUP": 0, 105 | "BTC": 5, 106 | "ETH": 4 107 | }, 108 | "pricePrecisions": { 109 | "DEFAULT": 2, 110 | "SOL": 2, 111 | "JUP": 3, 112 | "BTC": 0, 113 | "ETH": 2 114 | } 115 | ``` 116 | 117 | ## 系统功能 118 | 119 | 1. **递增买入策略**:基于配置创建多个价格递减的买入订单,随价格下跌增加买入量 120 | 2. **实时价格监控**:使用WebSocket连接实时监控市场价格变动 121 | 3. **自动止盈**:达到预设止盈点自动卖出获利 122 | 4. **统计分析**:实时计算和显示交易统计数据,包括成交量、均价和盈亏情况 123 | 5. **失败重试**:订单创建失败时自动重试 124 | 6. **安全退出**:优雅处理进程退出,支持自动撤单 125 | 7. **自动重启**:无订单成交或止盈后自动重启新一轮交易 126 | 127 | ## 使用方法 128 | 129 | ### 安装依赖 130 | ```bash 131 | npm install 132 | ``` 133 | 134 | ### 配置 135 | 编辑`backpack_trading_config.json`文件,设置您的API密钥和交易参数。 136 | 137 | ### 运行 138 | ```bash 139 | # 直接运行模块化版本 140 | node src/index.js 141 | 142 | # 或使用模块化版自动启动脚本(带重启功能) 143 | node start_modular_trading.js 144 | 145 | # 或使用原有脚本(保持兼容性) 146 | node start_auto_trading.js 147 | ``` 148 | 149 | ## 注意事项 150 | 151 | 1. **API密钥安全**:请确保您的API密钥安全,不要在公共环境中泄露 152 | 2. **投资风险**:加密货币交易存在高风险,请根据您的风险承受能力谨慎投资 153 | 3. **测试验证**:建议先使用小额资金进行测试,确认系统正常工作后再增加投资金额 154 | 4. **监控运行**:系统运行期间建议定期检查,确保一切正常 155 | 156 | ## 交易策略说明 157 | 158 | 本系统实现的递增买入策略基于以下原理: 159 | 160 | 1. 在当前价格以下设置多个买入订单,价格逐步降低 161 | 2. 随着价格降低,买入金额逐步增加,形成递增买入 162 | 3. 当价格回升至平均买入价以上一定比例时,自动卖出获利 163 | 164 | 这种策略适合在震荡行情中使用,可以降低平均买入成本,提高盈利机会。 165 | 166 | ## 开发与贡献 167 | 168 | ### 模块化设计 169 | 系统采用模块化设计,使代码结构清晰、易于维护和扩展: 170 | 171 | - **配置模块**:负责加载和验证配置 172 | - **核心模块**:实现交易策略、订单管理和价格监控 173 | - **模型模块**:定义数据模型和状态 174 | - **网络模块**:处理WebSocket通信 175 | - **服务模块**:封装API调用 176 | - **工具模块**:提供通用功能 177 | 178 | ### 添加新功能 179 | 如需添加新功能或支持新的交易策略,建议按以下步骤进行: 180 | 181 | 1. 在对应模块下创建新的组件 182 | 2. 在配置中添加相关参数 183 | 3. 在应用层集成新功能 184 | 4. 充分测试后再投入使用 185 | 186 | ## 免责声明 187 | 188 | 本项目仅供学习和研究使用,作者不对使用本系统进行交易造成的任何损失负责。使用前请充分了解加密货币交易的风险,并自行承担全部责任。 189 | 190 | ## 许可证 191 | 192 | MIT 193 | 194 | ## 项目文件说明 195 | 196 | 以下是项目中所有重要的JavaScript文件及其简要说明: 197 | 198 | ### 核心代码文件(src目录) 199 | 200 | #### 主要入口文件 201 | | 文件 | 大小 | 说明 | 202 | |------|------|------| 203 | | `src/index.js` | 4.4KB | 程序主入口,负责启动应用并处理程序生命周期 | 204 | | `src/app.js` | 27.5KB | 应用程序核心逻辑,协调各组件工作 | 205 | 206 | #### 核心功能模块 207 | | 文件 | 大小 | 说明 | 208 | |------|------|------| 209 | | `src/core/orderManager.js` | 12.7KB | 订单管理器,负责订单的创建、取消和跟踪 | 210 | | `src/core/priceMonitor.js` | 8.7KB | 价格监控器,负责实时监控价格变动 | 211 | | `src/core/tradingStrategy.js` | 6.0KB | 交易策略,实现递增买入和止盈策略 | 212 | 213 | #### 数据模型 214 | | 文件 | 大小 | 说明 | 215 | |------|------|------| 216 | | `src/models/Order.js` | 5.5KB | 订单模型,定义订单数据结构和操作 | 217 | | `src/models/TradeStats.js` | 4.3KB | 交易统计,跟踪和计算交易统计数据 | 218 | 219 | #### 网络服务 220 | | 文件 | 大小 | 说明 | 221 | |------|------|------| 222 | | `src/network/webSocketManager.js` | 12.2KB | WebSocket管理器,处理实时价格数据订阅 | 223 | | `src/services/backpackService.js` | 9.9KB | Backpack交易所API服务,封装API调用 | 224 | 225 | #### 工具类 226 | | 文件 | 大小 | 说明 | 227 | |------|------|------| 228 | | `src/utils/logger.js` | 5.7KB | 日志记录工具,处理日志输出和保存 | 229 | | `src/utils/formatter.js` | 5.0KB | 数据格式化工具,处理价格和数量格式 | 230 | | `src/utils/timeUtils.js` | 3.1KB | 时间工具,提供时间相关功能 | 231 | 232 | #### 配置管理 233 | | 文件 | 大小 | 说明 | 234 | |------|------|------| 235 | | `src/config/configLoader.js` | 3.5KB | 配置加载器,负责读取和验证配置 | 236 | 237 | ### 根目录脚本文件 238 | 239 | #### 启动脚本 240 | | 文件 | 大小 | 说明 | 241 | |------|------|------| 242 | | `start_modular_trading.js` | 3.6KB | 模块化交易启动脚本,支持自动重启 | 243 | | `start_auto_trading.js` | 1.5KB | 自动交易启动脚本(兼容旧版) | 244 | 245 | #### Backpack交易所API相关 246 | | 文件 | 大小 | 说明 | 247 | |------|------|------| 248 | | `backpack_api.js` | 24.9KB | Backpack API包装器,提供API调用功能 | 249 | | `backpack_client.js` | 16.8KB | Backpack客户端,底层API客户端实现 | 250 | 251 | #### 测试和工具脚本 252 | | 文件 | 大小 | 说明 | 253 | |------|------|------| 254 | | `test_create_orders_auto.js` | 134.3KB | 自动创建订单测试脚本(完整实现) | 255 | | `test_websocket.js` | 23.1KB | WebSocket测试脚本,测试行情订阅 | 256 | | `test_websocket2.js` | 16.8KB | WebSocket测试脚本2,替代实现 | 257 | | `backpack_price_reader.js` | 9.4KB | 价格读取工具,独立获取价格数据 | 258 | | `btc_order_test.js` | 11.6KB | BTC订单测试,测试BTC交易对订单 | 259 | | `backpack_public_api_test.js` | 13.0KB | 公共API测试,测试行情等公共接口 | 260 | | `backpack_ws_tester.js` | 5.6KB | WebSocket测试器,测试连接和订阅 | 261 | | `test.js` | 0.05KB | 简单测试文件 | 262 | 263 | 生产环境主要使用src目录下的代码,通过`start_modular_trading.js`或`src/index.js`启动。根目录下的测试脚本主要用于开发和调试过程。 -------------------------------------------------------------------------------- /src/services/backpackService.js: -------------------------------------------------------------------------------- 1 | const { BackpackClient } = require('../../backpack_exchange-main/backpack_client'); 2 | const { log } = require('../utils/logger'); 3 | const TimeUtils = require('../utils/timeUtils'); 4 | const axios = require('axios'); 5 | const crypto = require('crypto'); 6 | 7 | /** 8 | * Backpack交易所API服务类 - 负责处理API调用 9 | */ 10 | class BackpackService { 11 | /** 12 | * 构造函数 13 | * @param {Object} config - 配置对象 14 | * @param {Object} logger - 日志对象 15 | */ 16 | constructor(config, logger) { 17 | this.config = config; 18 | this.logger = logger; 19 | this.privateKey = config.api.privateKey; 20 | this.publicKey = config.api.publicKey; 21 | this.tradingCoin = config.trading?.tradingCoin || 'BTC'; 22 | this.symbol = `${this.tradingCoin}_USDC`; 23 | 24 | // 初始化官方BackpackClient 25 | this.client = new BackpackClient(this.privateKey, this.publicKey); 26 | } 27 | 28 | /** 29 | * 执行API请求,自动处理重试逻辑 30 | * @param {Function} apiCall - API调用函数 31 | * @param {number} maxRetries - 最大重试次数 32 | * @param {number} retryDelay - 重试间隔(毫秒) 33 | * @returns {Promise} - API响应 34 | */ 35 | async executeWithRetry(apiCall, maxRetries = 3, retryDelay = 2000) { 36 | let lastError; 37 | 38 | for (let attempt = 1; attempt <= maxRetries; attempt++) { 39 | try { 40 | return await apiCall(); 41 | } catch (error) { 42 | lastError = error; 43 | 44 | // 构建详细的错误日志 45 | let errorMessage = `API调用失败(尝试 ${attempt}/${maxRetries}): ${error.message}`; 46 | 47 | // 记录日志到logger或console 48 | if (this.logger && typeof this.logger.log === 'function') { 49 | this.logger.log(errorMessage, true); 50 | 51 | // 记录更多细节信息 52 | if (error.response) { 53 | const statusCode = error.response.status || 'unknown'; 54 | const responseBody = JSON.stringify(error.response.data || {}); 55 | this.logger.log(`响应代码 ${statusCode} (${error.response.statusText || 'No status text'})`, true); 56 | this.logger.log(`响应体: ${responseBody}`, true); 57 | 58 | // 尝试提取更具体的错误信息 59 | if (error.response.data) { 60 | const data = error.response.data; 61 | if (data.message) { 62 | this.logger.log(`错误消息: ${data.message}`, true); 63 | } 64 | if (data.code) { 65 | this.logger.log(`错误代码: ${data.code}`, true); 66 | } 67 | if (data.error) { 68 | this.logger.log(`错误详情: ${JSON.stringify(data.error)}`, true); 69 | } 70 | } 71 | } 72 | 73 | // 记录请求信息(如果有) 74 | if (error.request) { 75 | this.logger.log(`请求方法: ${error.request.method}`, true); 76 | this.logger.log(`请求URL: ${error.request.path}`, true); 77 | } 78 | } else { 79 | console.log(errorMessage); 80 | if (error.response) { 81 | console.log(`响应状态: ${error.response.status}`); 82 | console.log(`响应数据: ${JSON.stringify(error.response.data || {})}`); 83 | } 84 | } 85 | 86 | if (attempt < maxRetries) { 87 | const logMethod = this.logger?.log || console.log; 88 | logMethod(`${retryDelay/1000}秒后重试...`); 89 | await new Promise(resolve => setTimeout(resolve, retryDelay)); 90 | } 91 | } 92 | } 93 | 94 | throw lastError; 95 | } 96 | 97 | /** 98 | * 获取行情数据 99 | * @param {string} symbol - 交易对 100 | * @returns {Promise} 行情数据 101 | */ 102 | async getTicker(symbol = this.symbol) { 103 | try { 104 | // 记录API调用详情,用于调试 105 | this.logger?.log(`获取${symbol}行情数据...`); 106 | 107 | const result = await this.executeWithRetry(() => 108 | this.client.Ticker({ symbol }) 109 | ); 110 | 111 | // 记录接收到的数据 112 | if (result) { 113 | this.logger?.log(`获取到${symbol}行情: 最新价=${result.lastPrice}`); 114 | } else { 115 | this.logger?.log(`获取${symbol}行情响应数据为空`); 116 | } 117 | 118 | return result; 119 | } catch (error) { 120 | this.logger?.log(`获取行情失败: ${error.message}`); 121 | throw error; 122 | } 123 | } 124 | 125 | /** 126 | * 获取账户余额 127 | * @returns {Promise} 账户余额 128 | */ 129 | async getBalances() { 130 | try { 131 | return await this.executeWithRetry(() => 132 | this.client.Balance() 133 | ); 134 | } catch (error) { 135 | this.logger?.log(`获取余额失败: ${error.message}`); 136 | throw error; 137 | } 138 | } 139 | 140 | /** 141 | * 获取所有未成交订单 142 | * @param {string} symbol - 交易对 143 | * @returns {Promise} 未成交订单列表 144 | */ 145 | async getOpenOrders(symbol = this.symbol) { 146 | try { 147 | return await this.executeWithRetry(() => 148 | this.client.GetOpenOrders({ symbol }) 149 | ); 150 | } catch (error) { 151 | this.logger?.log(`获取未成交订单失败: ${error.message}`); 152 | throw error; 153 | } 154 | } 155 | 156 | /** 157 | * 获取订单详情 158 | * @param {string} orderId - 订单ID 159 | * @returns {Promise} 订单详情 160 | */ 161 | async getOrderDetails(orderId) { 162 | try { 163 | return await this.executeWithRetry(() => 164 | this.client.GetOrder({ orderId }) 165 | ); 166 | } catch (error) { 167 | this.logger?.log(`获取订单详情失败: ${error.message}`); 168 | throw error; 169 | } 170 | } 171 | 172 | /** 173 | * 创建订单 174 | * @param {Object} params - 订单参数 175 | * @returns {Promise} 创建结果 176 | */ 177 | async createOrder(params) { 178 | try { 179 | return await this.executeWithRetry(() => 180 | this.client.ExecuteOrder(params) 181 | ); 182 | } catch (error) { 183 | // 使用专门的API错误记录方法 184 | if (this.logger && typeof this.logger.logApiError === 'function') { 185 | this.logger.logApiError(error, "创建订单失败", params); 186 | } else { 187 | // 增强错误日志 188 | if (this.logger && typeof this.logger.log === 'function') { 189 | this.logger.log(`创建订单失败: ${error.message}`, true); 190 | 191 | // 记录详细的订单参数 192 | this.logger.log(`创建订单失败详情 - 参数: ${JSON.stringify(params)}`, true); 193 | 194 | // 记录错误对象的详细信息 195 | if (error.response) { 196 | this.logger.log(`错误响应状态: ${error.response.status}`, true); 197 | this.logger.log(`错误响应数据: ${JSON.stringify(error.response.data || {})}`, true); 198 | } 199 | 200 | // 记录原始错误对象 201 | this.logger.log(`原始错误: ${JSON.stringify(error.toString())}`, true); 202 | 203 | // 尝试解析更深层次的错误 204 | if (error.code) { 205 | this.logger.log(`错误代码: ${error.code}`, true); 206 | } 207 | } else { 208 | console.error(`创建订单失败: ${error.message}`); 209 | console.error(`参数: ${JSON.stringify(params)}`); 210 | if (error.response) { 211 | console.error(`响应: ${JSON.stringify(error.response)}`); 212 | } 213 | } 214 | } 215 | throw error; 216 | } 217 | } 218 | 219 | /** 220 | * 取消订单 221 | * @param {string} orderId - 订单ID 222 | * @returns {Promise} 取消结果 223 | */ 224 | async cancelOrder(orderId) { 225 | try { 226 | return await this.executeWithRetry(() => 227 | this.client.CancelOrder({ orderId }) 228 | ); 229 | } catch (error) { 230 | this.logger?.log(`取消订单失败: ${error.message}`); 231 | throw error; 232 | } 233 | } 234 | 235 | /** 236 | * 取消所有未成交订单 237 | * @param {string} symbol - 交易对 238 | * @returns {Promise} 取消结果 239 | */ 240 | async cancelAllOrders(symbol = this.symbol) { 241 | try { 242 | return await this.executeWithRetry(() => 243 | this.client.CancelOpenOrders({ symbol }) 244 | ); 245 | } catch (error) { 246 | // 确保logger存在再使用 247 | if (this.logger && typeof this.logger.log === 'function') { 248 | this.logger.log(`取消所有订单失败: ${error.message}`); 249 | } else { 250 | // 使用全局log函数或console.log 251 | if (typeof log === 'function') { 252 | log(`取消所有订单失败: ${error.message}`); 253 | } else { 254 | console.log(`取消所有订单失败: ${error.message}`); 255 | } 256 | } 257 | throw error; 258 | } 259 | } 260 | 261 | /** 262 | * 创建买入订单 263 | * @param {number} price - 价格 264 | * @param {number} quantity - 数量 265 | * @param {string} symbol - 交易对 266 | * @returns {Promise} 订单结果 267 | */ 268 | async createBuyOrder(price, quantity, symbol = this.symbol) { 269 | const orderParams = { 270 | symbol, 271 | side: 'Bid', // 注意:必须使用'Bid'而不是'BUY' 272 | orderType: 'Limit', // 注意:必须使用'Limit'而不是'LIMIT' 273 | timeInForce: 'GTC', 274 | price: price.toString(), 275 | quantity: quantity.toString() 276 | }; 277 | 278 | return this.createOrder(orderParams); 279 | } 280 | 281 | /** 282 | * 创建卖出订单 283 | * @param {number} price - 价格 284 | * @param {number} quantity - 数量 285 | * @param {string} symbol - 交易对 286 | * @returns {Promise} 订单结果 287 | */ 288 | async createSellOrder(price, quantity, symbol = this.symbol) { 289 | const orderParams = { 290 | symbol, 291 | side: 'Ask', // 注意:必须使用'Ask'而不是'SELL' 292 | orderType: 'Limit', // 注意:必须使用'Limit'而不是'LIMIT' 293 | timeInForce: 'GTC', 294 | price: price.toString(), 295 | quantity: quantity.toString() 296 | }; 297 | 298 | return this.createOrder(orderParams); 299 | } 300 | 301 | /** 302 | * 获取持仓信息 303 | * @param {string} coin - 货币符号 304 | * @returns {Promise} 持仓信息 305 | */ 306 | async getPosition(coin) { 307 | try { 308 | const balances = await this.getBalances(); 309 | return balances.find(balance => balance.asset === coin) || { asset: coin, available: '0', total: '0' }; 310 | } catch (error) { 311 | this.logger?.log(`获取持仓失败: ${error.message}`); 312 | throw error; 313 | } 314 | } 315 | } 316 | 317 | module.exports = BackpackService; -------------------------------------------------------------------------------- /src/core/orderManager.js: -------------------------------------------------------------------------------- 1 | const { OrderManager } = require('../models/Order'); 2 | const TradeStats = require('../models/TradeStats'); 3 | const BackpackService = require('../services/backpackService'); 4 | const { log } = require('../utils/logger'); 5 | const TimeUtils = require('../utils/timeUtils'); 6 | const Formatter = require('../utils/formatter'); 7 | 8 | /** 9 | * 订单管理器 - 管理交易订单的创建、取消和查询 10 | */ 11 | class OrderManagerService { 12 | /** 13 | * 构造函数 14 | * @param {Object} config - 配置对象 15 | * @param {BackpackService} backpackService - 交易API服务 16 | */ 17 | constructor(config, backpackService) { 18 | this.config = config; 19 | this.backpackService = backpackService; 20 | this.orderManager = new OrderManager(); 21 | this.tradeStats = new TradeStats(); 22 | this.symbol = null; 23 | this.tradingCoin = null; 24 | } 25 | 26 | /** 27 | * 初始化订单管理器 28 | * @param {string} symbol - 交易对 29 | * @param {string} tradingCoin - 交易币种 30 | */ 31 | initialize(symbol, tradingCoin) { 32 | this.symbol = symbol; 33 | this.tradingCoin = tradingCoin; 34 | this.orderManager.reset(); 35 | this.tradeStats.reset(); 36 | log(`订单管理器已初始化: ${this.symbol}`); 37 | } 38 | 39 | /** 40 | * 重置订单管理器状态 41 | */ 42 | reset() { 43 | this.orderManager.reset(); 44 | this.tradeStats.reset(); 45 | log('订单管理器已重置'); 46 | } 47 | 48 | /** 49 | * 创建买入订单 50 | * @param {Array} orders - 订单列表 51 | * @returns {Object} - 创建结果 52 | */ 53 | async createBuyOrders(orders) { 54 | if (!this.symbol || !this.tradingCoin) { 55 | throw new Error('订单管理器尚未初始化'); 56 | } 57 | 58 | // 显示计划创建的订单 59 | log('\n=== 计划创建的订单 ==='); 60 | let totalOrderAmount = 0; 61 | orders.forEach((order, index) => { 62 | log(`订单 ${index + 1}: 价格=${order.price} USDC, 数量=${order.quantity} ${this.tradingCoin}, 金额=${order.amount.toFixed(2)} USDC`); 63 | totalOrderAmount += order.amount; 64 | }); 65 | log(`总订单金额: ${totalOrderAmount.toFixed(2)} USDC`); 66 | 67 | // 重置统计信息以确保干净的数据 68 | this.tradeStats.reset(); 69 | this.orderManager.reset(); 70 | 71 | // 创建订单 72 | log('\n=== 开始创建订单 ==='); 73 | let successCount = 0; 74 | 75 | // 保存计划创建的订单总数 76 | const plannedOrderCount = orders.length; 77 | 78 | // 创建订单循环 79 | let retryAttempts = 0; 80 | const MAX_RETRY_ATTEMPTS = 5; 81 | let createdOrdersCount = 0; // 跟踪实际创建的订单数量 82 | 83 | while (successCount < plannedOrderCount && retryAttempts < MAX_RETRY_ATTEMPTS) { 84 | // 如果是重试,展示重试信息 85 | if (retryAttempts > 0) { 86 | log(`\n===== 自动重试创建订单 (第 ${retryAttempts}/${MAX_RETRY_ATTEMPTS} 次) =====`); 87 | log(`已成功创建 ${successCount}/${plannedOrderCount} 个订单,继续尝试创建剩余订单...`); 88 | } 89 | 90 | // 只处理未成功创建的订单 91 | const remainingOrders = orders.slice(successCount); 92 | 93 | for (const order of remainingOrders) { 94 | try { 95 | // 检查是否已存在相同参数的订单 96 | if (this.orderManager.hasOrderSignature(order.getSignature())) { 97 | log(`跳过重复订单创建,价格=${order.price}, 数量=${order.quantity}`); 98 | continue; 99 | } 100 | 101 | // 创建买入订单 102 | const response = await this.backpackService.createBuyOrder( 103 | this.symbol, 104 | order.price, 105 | order.quantity, 106 | this.tradingCoin 107 | ); 108 | 109 | if (response && response.id) { 110 | // 设置订单ID 111 | order.id = response.id; 112 | order.status = response.status || 'New'; 113 | 114 | // 添加到订单管理器 115 | this.orderManager.addOrder(order); 116 | 117 | successCount++; 118 | createdOrdersCount++; 119 | log(`成功创建第 ${successCount}/${plannedOrderCount} 个订单`); 120 | 121 | // 如果订单创建时已成交,更新统计信息 122 | if (order.status === 'Filled') { 123 | this.tradeStats.updateStats(order); 124 | } 125 | 126 | // 检查是否已创建足够数量的订单 127 | if (createdOrdersCount >= plannedOrderCount) { 128 | log(`已达到计划创建的订单数量: ${plannedOrderCount}`); 129 | break; 130 | } 131 | 132 | // 添加延迟避免API限制 133 | await TimeUtils.delay(3000); 134 | } 135 | } catch (error) { 136 | log(`创建订单失败: ${error.message}`, true); 137 | 138 | // 优先使用专门的API错误记录 139 | if (this.backpackService && this.backpackService.logger && 140 | typeof this.backpackService.logger.logApiError === 'function') { 141 | this.backpackService.logger.logApiError( 142 | error, 143 | "创建买入订单失败", 144 | { 145 | symbol: this.symbol, 146 | price: order.price, 147 | quantity: order.quantity, 148 | side: 'Bid', 149 | orderType: 'Limit', 150 | timeInForce: 'GTC' 151 | } 152 | ); 153 | } else { 154 | // 记录更详细的错误信息 155 | log(`创建订单失败详情:`, true); 156 | log(`- 订单: ${JSON.stringify({ 157 | symbol: this.symbol, 158 | price: order.price, 159 | quantity: order.quantity, 160 | amount: order.amount, 161 | side: order.side, 162 | orderType: order.orderType, 163 | timeInForce: order.timeInForce 164 | })}`, true); 165 | 166 | // 记录错误对象详情 167 | if (error.response) { 168 | log(`- 响应状态: ${error.response.status}`, true); 169 | log(`- 响应数据: ${JSON.stringify(error.response.data || {})}`, true); 170 | } 171 | 172 | // 记录具体的订单 173 | log(`创建订单失败: ${error.message}, 订单: ${JSON.stringify(order)}`, true); 174 | } 175 | 176 | // 如果是资金不足,跳过后续订单 177 | if (error.message.includes('Insufficient') || error.message.includes('insufficient')) { 178 | log('资金不足,停止创建更多订单', true); 179 | break; 180 | } else { 181 | // 其他错误,等待后继续尝试 182 | const waitTime = Math.min(3000 * (retryAttempts + 1), 15000); // 随重试次数增加等待时间 183 | log(`等待${waitTime/1000}秒后自动重试...`); 184 | await TimeUtils.delay(waitTime); 185 | } 186 | } 187 | } 188 | 189 | // 如果所有订单都创建成功,跳出循环 190 | if (successCount >= plannedOrderCount) { 191 | log(`✓ 成功创建所有 ${plannedOrderCount} 个订单!`); 192 | break; 193 | } 194 | 195 | // 增加重试次数 196 | retryAttempts++; 197 | 198 | // 如果还未达到最大重试次数,自动继续尝试 199 | if (successCount < plannedOrderCount && retryAttempts < MAX_RETRY_ATTEMPTS) { 200 | // 添加随重试次数增加的等待时间 201 | const waitTime = 5000 * retryAttempts; 202 | log(`将在${waitTime/1000}秒后自动重试创建剩余订单...`); 203 | await TimeUtils.delay(waitTime); 204 | } 205 | } 206 | 207 | // 查询并更新订单状态 208 | await this.queryOrdersAndUpdateStats(); 209 | 210 | return { 211 | success: successCount > 0, 212 | createdCount: successCount, 213 | plannedCount: plannedOrderCount, 214 | orders: this.orderManager.getAllOrders() 215 | }; 216 | } 217 | 218 | /** 219 | * 卖出所有持仓 220 | * @param {Object} tradingStrategy - 交易策略实例 221 | * @returns {Object} - 卖出结果 222 | */ 223 | async sellAllPosition(tradingStrategy) { 224 | try { 225 | // 获取当前持仓情况 226 | const position = await this.backpackService.getPosition(this.symbol); 227 | if (!position || parseFloat(position.quantity) <= 0) { 228 | log('没有可卖出的持仓'); 229 | return null; 230 | } 231 | 232 | // 获取当前市场价格 233 | const ticker = await this.backpackService.getTicker(this.symbol); 234 | const currentPrice = parseFloat(ticker.lastPrice); 235 | 236 | // 设置卖出价格 237 | const sellPrice = tradingStrategy.calculateOptimalSellPrice(currentPrice, this.tradingCoin); 238 | const quantity = Formatter.adjustQuantityToStepSize(parseFloat(position.quantity), this.tradingCoin, this.config); 239 | 240 | log(`准备卖出: ${quantity} ${this.tradingCoin}, 当前市场价=${currentPrice}, 卖出价=${sellPrice}`); 241 | 242 | // 创建卖出订单 243 | const response = await this.backpackService.createSellOrder( 244 | this.symbol, 245 | sellPrice, 246 | quantity, 247 | this.tradingCoin 248 | ); 249 | 250 | if (response && response.id) { 251 | log(`卖出订单创建成功: 订单ID=${response.id}, 状态=${response.status}`); 252 | 253 | // 检查订单是否完全成交 254 | let fullyFilled = response.status === 'Filled'; 255 | 256 | // 如果订单未完全成交,尝试再次以更低价格卖出剩余部分 257 | if (!fullyFilled) { 258 | log('订单未完全成交,检查剩余数量并尝试以更低价格卖出'); 259 | 260 | // 等待一小段时间,让订单有时间处理 261 | await TimeUtils.delay(2000); 262 | 263 | // 获取更新后的持仓 264 | const updatedPosition = await this.backpackService.getPosition(this.symbol); 265 | if (updatedPosition && parseFloat(updatedPosition.quantity) > 0) { 266 | const remainingQuantity = Formatter.adjustQuantityToStepSize(parseFloat(updatedPosition.quantity), this.tradingCoin, this.config); 267 | 268 | log(`仍有 ${remainingQuantity} ${this.tradingCoin} 未售出,尝试以更低价格卖出`); 269 | 270 | // 计算更低的卖出价格 271 | const lowerSellPrice = tradingStrategy.calculateSecondSellPrice(currentPrice, this.tradingCoin); 272 | 273 | // 创建第二次卖出订单 274 | const secondResponse = await this.backpackService.createSellOrder( 275 | this.symbol, 276 | lowerSellPrice, 277 | remainingQuantity, 278 | this.tradingCoin 279 | ); 280 | 281 | if (secondResponse && secondResponse.id) { 282 | log(`第二次卖出订单创建成功: 订单ID=${secondResponse.id}, 状态=${secondResponse.status}`); 283 | } 284 | } else { 285 | log(`所有 ${this.tradingCoin} 已售出`); 286 | } 287 | } 288 | 289 | return response; 290 | } else { 291 | throw new Error('卖出订单创建失败:响应中没有订单ID'); 292 | } 293 | } catch (error) { 294 | log(`卖出失败: ${error.message}`, true); 295 | return null; 296 | } 297 | } 298 | 299 | /** 300 | * 撤销所有未成交订单 301 | */ 302 | async cancelAllOrders() { 303 | try { 304 | log(`开始撤销 ${this.symbol} 交易对的所有未完成订单...`); 305 | const result = await this.backpackService.cancelAllOrders(this.symbol); 306 | log(`撤销订单结果: ${JSON.stringify(result)}`); 307 | return true; 308 | } catch (error) { 309 | log(`撤销订单失败: ${error.message}`, true); 310 | return false; 311 | } 312 | } 313 | 314 | /** 315 | * 查询订单并更新统计 316 | */ 317 | async queryOrdersAndUpdateStats() { 318 | try { 319 | log('查询当前交易周期新成交的订单...'); 320 | 321 | // 获取当前未成交订单 322 | const openOrders = await this.backpackService.getOpenOrders(this.symbol); 323 | const currentOpenOrderIds = new Set(openOrders.map(order => order.id)); 324 | 325 | // 遍历所有创建的订单,检查哪些已经不在未成交列表中 326 | const filledOrders = []; 327 | for (const orderId of this.orderManager.getAllCreatedOrderIds()) { 328 | if (!currentOpenOrderIds.has(orderId)) { 329 | const order = this.orderManager.getOrder(orderId); 330 | 331 | // 如果订单存在且未处理,则视为已成交 332 | if (order && !this.tradeStats.isOrderProcessed(orderId)) { 333 | order.status = 'Filled'; 334 | filledOrders.push(order); 335 | } 336 | } 337 | } 338 | 339 | // 更新统计信息 340 | for (const order of filledOrders) { 341 | this.tradeStats.updateStats(order); 342 | } 343 | 344 | // 更新订单管理器中的待处理订单ID列表 345 | this.orderManager.updatePendingOrderIds(Array.from(currentOpenOrderIds)); 346 | 347 | return filledOrders.length > 0; 348 | } catch (error) { 349 | log(`查询订单历史并更新统计失败: ${error.message}`, true); 350 | return false; 351 | } 352 | } 353 | 354 | /** 355 | * 获取订单统计信息 356 | */ 357 | getStats() { 358 | return this.tradeStats; 359 | } 360 | 361 | /** 362 | * 获取订单管理器 363 | */ 364 | getOrderManager() { 365 | return this.orderManager; 366 | } 367 | } 368 | 369 | module.exports = OrderManagerService; -------------------------------------------------------------------------------- /src/core/priceMonitor.js: -------------------------------------------------------------------------------- 1 | const WebSocketManager = require('../network/webSocketManager'); 2 | const { log } = require('../utils/logger'); 3 | const TimeUtils = require('../utils/timeUtils'); 4 | 5 | /** 6 | * 价格监控类 - 负责监控价格变化和触发事件 7 | */ 8 | class PriceMonitor { 9 | /** 10 | * 构造函数 11 | * @param {Object} options - 配置选项 12 | * @param {Object} options.config - 全局配置 13 | * @param {Function} options.onPriceUpdate - 价格更新回调 14 | * @param {Function} options.onPriceData - 价格数据收到回调 15 | * @param {Object} options.logger - 日志记录器 16 | */ 17 | constructor(options = {}) { 18 | this.config = options.config; 19 | this.onPriceUpdate = options.onPriceUpdate || (() => {}); 20 | this.onPriceData = options.onPriceData || (() => {}); 21 | this.logger = options.logger; 22 | 23 | // 初始化WebSocket管理器 24 | this.wsManager = new WebSocketManager({ 25 | wsUrl: this.config?.websocket?.url || 'wss://ws.backpack.exchange', 26 | config: this.config, 27 | onMessage: this.handleMessage.bind(this), 28 | onPriceUpdate: this.handleWebSocketPriceUpdate.bind(this), 29 | logger: this.logger 30 | }); 31 | 32 | // 测试回调是否正确设置 33 | if (typeof this.wsManager.onPriceUpdate !== 'function') { 34 | this.logger?.log('警告: WebSocketManager.onPriceUpdate 未正确设置'); 35 | } else { 36 | this.logger?.log('WebSocketManager.onPriceUpdate 已正确设置'); 37 | } 38 | 39 | // 价格数据 40 | this.lastPrice = 0; 41 | this.currentPrice = 0; 42 | this.priceSource = 'WebSocket'; 43 | this.lastUpdateTime = null; 44 | 45 | // 监控状态 46 | this.monitoring = false; 47 | this.symbol = null; 48 | this.checkInterval = null; 49 | 50 | // 添加重试计数 51 | this.reconnectAttempts = 0; 52 | this.maxReconnectAttempts = 10; 53 | } 54 | 55 | /** 56 | * 启动价格监控 57 | * @param {string} symbol - 交易对符号 58 | * @returns {boolean} 是否成功启动 59 | */ 60 | startMonitoring(symbol) { 61 | if (this.monitoring) { 62 | log(`已经在监控 ${this.symbol} 的价格`); 63 | return true; 64 | } 65 | 66 | this.symbol = symbol; 67 | this.monitoring = true; 68 | this.startMonitoringTime = Date.now(); // 添加监控开始时间 69 | this.reconnectAttempts = 0; // 重置重连计数 70 | 71 | // 确保symbol使用正确的格式 72 | // 不做转换,直接使用相同的格式 73 | log(`原始交易对: ${symbol}`); 74 | 75 | // 关闭现有的WebSocket连接 76 | if (this.wsManager) { 77 | this.wsManager.closeWebSocket(); 78 | } 79 | 80 | // 重新初始化WebSocketManager以确保回调函数正确设置 81 | this.wsManager = new WebSocketManager({ 82 | wsUrl: this.config?.websocket?.url || 'wss://ws.backpack.exchange', 83 | config: this.config, 84 | onMessage: this.handleMessage.bind(this), 85 | onPriceUpdate: this.handleWebSocketPriceUpdate.bind(this), 86 | logger: this.logger 87 | }); 88 | 89 | // 启动WebSocket 90 | log(`启动对 ${symbol} 的价格监控...`); 91 | const websocket = this.wsManager.setupPriceWebSocket(symbol); 92 | 93 | // 启动定期检查,确保价格数据正常 94 | this.startPeriodicCheck(); 95 | 96 | return websocket !== null; 97 | } 98 | 99 | /** 100 | * 停止价格监控 101 | */ 102 | stopMonitoring() { 103 | if (!this.monitoring) return; 104 | 105 | log('停止价格监控...'); 106 | 107 | // 停止WebSocket 108 | this.wsManager.closeWebSocket(); 109 | 110 | // 清除定期检查 111 | if (this.checkInterval) { 112 | clearInterval(this.checkInterval); 113 | this.checkInterval = null; 114 | } 115 | 116 | // 重置状态 117 | this.monitoring = false; 118 | this.symbol = null; 119 | this.lastPrice = 0; 120 | this.currentPrice = 0; 121 | this.lastUpdateTime = null; 122 | } 123 | 124 | /** 125 | * 启动定期检查 126 | */ 127 | startPeriodicCheck() { 128 | // 清除现有的定期检查 129 | if (this.checkInterval) { 130 | clearInterval(this.checkInterval); 131 | } 132 | 133 | // 每15秒检查一次价格数据状态,降低间隔增加及时性 134 | this.checkInterval = setInterval(() => { 135 | this.checkPriceDataStatus(); 136 | }, 15000); 137 | 138 | // 首次启动时立即执行一次检查 139 | setTimeout(() => { 140 | this.checkPriceDataStatus(); 141 | }, 5000); 142 | } 143 | 144 | /** 145 | * 检查价格数据状态 146 | */ 147 | checkPriceDataStatus() { 148 | if (!this.monitoring) return; 149 | 150 | const now = Date.now(); 151 | 152 | // 如果有上次更新时间,检查价格数据是否过时 153 | if (this.lastUpdateTime) { 154 | const dataAge = now - this.lastUpdateTime; 155 | 156 | // 如果价格数据超过30秒未更新,记录警告并尝试重连 157 | if (dataAge > 30000) { 158 | this.logger.log(`警告: 价格数据已 ${Math.floor(dataAge / 1000)} 秒未更新`, true); 159 | 160 | // 如果WebSocket未连接或数据太旧,尝试重连 161 | if (!this.wsManager.isConnected() || dataAge > 60000) { 162 | this.reconnectAttempts++; 163 | this.logger.log(`尝试重新连接WebSocket... (尝试 ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); 164 | 165 | // 如果超过最大重试次数,尝试通过API获取价格 166 | if (this.reconnectAttempts > this.maxReconnectAttempts) { 167 | this.logger.log('WebSocket重连失败次数过多,尝试通过API获取价格'); 168 | this.fetchPriceFromApi(); 169 | } else { 170 | this.wsManager.setupPriceWebSocket(this.symbol); 171 | } 172 | } 173 | } 174 | } else { 175 | // 如果没有上次更新时间,可能是首次启动或数据未初始化 176 | this.logger.log('等待首次价格数据更新...'); 177 | 178 | // 如果启动后5秒还没有收到数据,尝试通过API获取价格 179 | if (now - this.startMonitoringTime > 5000) { 180 | this.logger.log('WebSocket数据延迟,尝试通过API获取初始价格'); 181 | this.fetchPriceFromApi(); 182 | } 183 | } 184 | } 185 | 186 | /** 187 | * 处理WebSocket消息 188 | * @param {Object} data - 消息数据 189 | */ 190 | handleMessage(data) { 191 | try { 192 | // 只有0.1%的消息会被记录到日志文件,大幅减少日志量 193 | if (Math.random() < 0.001 && typeof this.logger?.logToFile === 'function') { 194 | this.logger.logToFile(`收到WebSocket消息: ${JSON.stringify(data).substring(0, 200)}...`); 195 | } 196 | 197 | // 将原始消息传递给外部处理函数 198 | if (typeof this.onPriceData === 'function') { 199 | this.onPriceData(data); 200 | } 201 | } catch (error) { 202 | if (typeof this.logger?.log === 'function') { 203 | this.logger.log(`处理WebSocket消息失败: ${error.message}`); 204 | } else { 205 | console.log(`处理WebSocket消息失败: ${error.message}`); 206 | } 207 | } 208 | } 209 | 210 | /** 211 | * 处理WebSocket价格更新回调 212 | * @param {string} symbol - 交易对符号 213 | * @param {number} price - 价格 214 | * @param {Date} time - 时间戳 215 | */ 216 | handleWebSocketPriceUpdate(symbol, price, time) { 217 | try { 218 | // 确保参数有效 219 | if (!symbol || !price || isNaN(price) || price <= 0) { 220 | this.logger?.log(`收到无效的WebSocket价格更新: symbol=${symbol}, price=${price}`); 221 | return; 222 | } 223 | 224 | this.logger?.log(`收到WebSocket价格更新: ${symbol} = ${price} USDC (时间: ${time ? time.toLocaleTimeString() : 'unknown'})`); 225 | 226 | // 更新内部状态 227 | this.lastPrice = this.currentPrice; 228 | this.currentPrice = price; 229 | this.lastUpdateTime = time ? time.getTime() : Date.now(); 230 | 231 | // 构建价格信息对象 232 | const priceInfo = { 233 | price, 234 | symbol: symbol || this.symbol, 235 | source: 'WebSocket', 236 | updateTime: this.lastUpdateTime, 237 | change: this.lastPrice > 0 ? ((price - this.lastPrice) / this.lastPrice) * 100 : 0 238 | }; 239 | 240 | // 记录价格信息 241 | this.logger?.logToFile(`WebSocket价格更新: ${JSON.stringify(priceInfo)}`); 242 | 243 | // 将价格信息传递给外部处理函数 244 | if (typeof this.onPriceUpdate === 'function') { 245 | try { 246 | this.onPriceUpdate(priceInfo); 247 | this.logger?.log(`价格信息已传递给应用程序`); 248 | } catch (callbackError) { 249 | this.logger?.log(`调用价格回调函数失败: ${callbackError.message}`); 250 | } 251 | } else { 252 | this.logger?.log(`警告: onPriceUpdate回调未设置或不是函数`); 253 | } 254 | } catch (error) { 255 | this.logger?.log(`处理WebSocket价格回调失败: ${error.message}`); 256 | } 257 | } 258 | 259 | /** 260 | * 处理价格更新 261 | * @param {number} price - 价格 262 | * @param {string} symbol - 交易对符号 263 | */ 264 | handlePriceUpdate(price, symbol) { 265 | try { 266 | if (!this.isPriceValid(price)) { 267 | this.logger?.log(`忽略无效价格: ${price}`); 268 | return; 269 | } 270 | 271 | // 仅在价格有明显变化时才记录到日志文件 272 | const previousPrice = this.currentPrice; 273 | const priceChangePercent = previousPrice ? ((price - previousPrice) / previousPrice) * 100 : 0; 274 | 275 | // 价格变化超过0.1%时才在终端显示 276 | if (Math.abs(priceChangePercent) > 0.1) { 277 | this.logger?.log(`价格更新: ${price} USDC (${priceChangePercent > 0 ? '+' : ''}${priceChangePercent.toFixed(2)}%) (来源: WebSocket)`); 278 | } else { 279 | // 小变化只记录到日志文件 280 | this.logger?.logToFile(`处理价格更新: ${price} USDC (来源: WebSocket)`); 281 | } 282 | 283 | this.lastPrice = this.currentPrice; 284 | this.currentPrice = price; 285 | this.lastUpdateTime = Date.now(); 286 | 287 | // 计算价格变化百分比 288 | let change = 0; 289 | if (this.lastPrice > 0) { 290 | change = ((price - this.lastPrice) / this.lastPrice) * 100; 291 | } 292 | 293 | // 构建价格信息对象 294 | const priceInfo = { 295 | price, 296 | symbol: symbol || this.symbol, 297 | source: this.priceSource, 298 | updateTime: this.lastUpdateTime, 299 | change 300 | }; 301 | 302 | // 仅在有明显价格变化时才记录详细信息 303 | if (Math.abs(change) > 0.05) { 304 | this.logger?.logToFile(`价格信息: ${JSON.stringify(priceInfo)}`); 305 | } 306 | 307 | // 将价格信息传递给外部处理函数 308 | if (typeof this.onPriceUpdate === 'function') { 309 | this.onPriceUpdate(priceInfo); 310 | } else { 311 | this.logger?.logToFile('警告: onPriceUpdate回调未设置或不是函数'); 312 | } 313 | } catch (error) { 314 | this.logger?.log(`处理价格更新失败: ${error.message}`); 315 | } 316 | } 317 | 318 | /** 319 | * 获取当前价格信息 320 | * @returns {Object|null} 价格信息对象或null 321 | */ 322 | getCurrentPriceInfo() { 323 | if (!this.currentPrice || this.currentPrice <= 0) { 324 | return null; 325 | } 326 | 327 | return { 328 | price: this.currentPrice, 329 | symbol: this.symbol, 330 | source: this.priceSource, 331 | updateTime: this.lastUpdateTime 332 | }; 333 | } 334 | 335 | /** 336 | * 检查价格数据是否有效 337 | * @param {number} timeoutSeconds - 超时秒数 338 | * @returns {boolean} 是否有效 339 | */ 340 | isPriceDataValid(timeoutSeconds = 60) { 341 | if (!this.currentPrice || this.currentPrice <= 0 || !this.lastUpdateTime) { 342 | return false; 343 | } 344 | 345 | const dataAge = (Date.now() - this.lastUpdateTime) / 1000; 346 | return dataAge <= timeoutSeconds; 347 | } 348 | 349 | /** 350 | * 是否正在监控 351 | * @returns {boolean} 是否正在监控 352 | */ 353 | isMonitoring() { 354 | return this.monitoring; 355 | } 356 | 357 | /** 358 | * 处理WebSocket消息 359 | * @param {number} price - 最新价格 360 | */ 361 | handleWebSocketMessage(price) { 362 | if (!this.isPriceValid(price)) { 363 | // 这是异常情况,应保留在终端显示 364 | if (Math.random() < 0.1) { 365 | this.logger.logToFile(`收到无效的价格数据: ${price}`); 366 | } 367 | return; 368 | } 369 | 370 | // 首次收到价格数据 371 | if (!this.priceData) { 372 | // 首次收到数据是重要事件,保留在终端 373 | this.logger.log(`首次收到价格数据: ${price}`); 374 | } 375 | 376 | // 保存有效的价格数据 377 | const previousPrice = this.priceData ? this.priceData.price : 0; 378 | 379 | this.priceData = { 380 | price, 381 | source: 'WebSocket', 382 | updateTime: new Date(), 383 | increase: previousPrice > 0 ? ((price - previousPrice) / previousPrice) * 100 : 0 384 | }; 385 | 386 | // 触发价格更新事件 387 | if (typeof this.onPriceUpdate === 'function') { 388 | this.onPriceUpdate(this.priceData); 389 | } 390 | } 391 | 392 | /** 393 | * 验证价格数据是否有效 394 | * @param {number} price - 价格 395 | * @returns {boolean} 是否有效 396 | */ 397 | isPriceValid(price) { 398 | // 价格必须是有效数字且大于0 399 | return !isNaN(price) && Number.isFinite(price) && price > 0; 400 | } 401 | 402 | /** 403 | * 从API获取价格 404 | */ 405 | async fetchPriceFromApi() { 406 | try { 407 | if (!this.config || !this.symbol) { 408 | this.logger.log('无法从API获取价格: 配置或交易对未定义'); 409 | return; 410 | } 411 | 412 | // 这里可以使用BackpackService获取价格,但简单演示就直接获取 413 | this.logger.log(`尝试从API获取${this.symbol}价格...`); 414 | 415 | // 使用Node.js内置的https模块 416 | const https = require('https'); 417 | const symbol = this.symbol.replace('_', ''); 418 | const url = `https://api.backpack.exchange/api/v1/ticker/price?symbol=${symbol}`; 419 | 420 | // 使用Promise封装HTTP请求 421 | const response = await new Promise((resolve, reject) => { 422 | https.get(url, (res) => { 423 | let data = ''; 424 | 425 | // 接收数据片段 426 | res.on('data', (chunk) => { 427 | data += chunk; 428 | }); 429 | 430 | // 接收完成 431 | res.on('end', () => { 432 | if (res.statusCode === 200) { 433 | try { 434 | const parsedData = JSON.parse(data); 435 | resolve(parsedData); 436 | } catch (e) { 437 | reject(new Error(`解析JSON失败: ${e.message}`)); 438 | } 439 | } else { 440 | reject(new Error(`API请求失败: ${res.statusCode} ${res.statusMessage}`)); 441 | } 442 | }); 443 | }).on('error', (e) => { 444 | reject(new Error(`请求失败: ${e.message}`)); 445 | }); 446 | }); 447 | 448 | if (response && response.price) { 449 | const price = parseFloat(response.price); 450 | this.logger.log(`API获取价格成功: ${price} USDC`); 451 | 452 | // 更新价格数据 453 | this.handlePriceUpdate(price, this.symbol); 454 | } else { 455 | throw new Error('API返回的数据格式不正确'); 456 | } 457 | } catch (error) { 458 | this.logger.log(`从API获取价格失败: ${error.message}`); 459 | } 460 | } 461 | } 462 | 463 | module.exports = PriceMonitor; -------------------------------------------------------------------------------- /src/network/webSocketManager.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const { defaultLogger } = require('../utils/logger'); 3 | 4 | /** 5 | * WebSocket管理器类 - 负责处理与交易所的WebSocket连接 6 | */ 7 | class WebSocketManager { 8 | /** 9 | * 构造函数 10 | * @param {Object} options - 配置选项 11 | * @param {Object} options.config - 配置对象 12 | * @param {Object} options.logger - 日志记录器 13 | * @param {Function} options.onMessage - 消息处理回调 14 | * @param {Function} options.onPrice - 价格更新回调 15 | */ 16 | constructor(options = {}) { 17 | // 优先使用配置中的WebSocket URL,然后是选项中的URL,最后使用默认值 18 | this.wsUrl = options.config?.websocket?.url || options.wsUrl || 'wss://ws.backpack.exchange'; 19 | this.config = options.config || {}; 20 | this.ws = null; 21 | this.connectionActive = false; 22 | this.heartbeatInterval = null; 23 | this.reconnectTimeout = null; 24 | this.logger = options.logger || console; 25 | this.onMessage = options.onMessage || (() => {}); 26 | 27 | // 修复onPriceUpdate回调 - 确保正确设置 28 | this.onPriceUpdate = options.onPriceUpdate || (() => {}); 29 | 30 | // 验证和记录回调函数设置情况 31 | if (typeof this.onPriceUpdate === 'function') { 32 | this.logger.log('WebSocketManager: onPriceUpdate回调已设置'); 33 | } else { 34 | this.logger.log('警告: WebSocketManager.onPriceUpdate未正确设置'); 35 | } 36 | 37 | // 价格更新控制 38 | this.lastLoggedPrice = null; 39 | this.lastLogTime = 0; 40 | this.logThrottleMs = 1000; // 每秒最多记录一次价格 41 | this.logPriceChange = 0.01; // 记录百分比变化超过1%的价格 42 | 43 | // 记录控制 44 | this.shouldLog = true; 45 | this.logHeartbeats = false; 46 | } 47 | 48 | /** 49 | * 重置日志控制参数 50 | */ 51 | resetLogControl() { 52 | this.lastLoggedPrice = null; 53 | this.lastLogTime = 0; 54 | } 55 | 56 | /** 57 | * 设置价格WebSocket连接 58 | * @param {string} symbol - 交易对符号 59 | */ 60 | setupPriceWebSocket(symbol) { 61 | // 关闭现有连接 62 | if (this.ws) { 63 | this.closeWebSocket(); 64 | } 65 | 66 | this.logger.log(`开始建立WebSocket连接: ${this.wsUrl}`); 67 | 68 | try { 69 | // 创建WebSocket连接 70 | this.ws = new WebSocket(this.wsUrl); 71 | 72 | // 连接打开时的处理 73 | this.ws.on('open', () => { 74 | this.connectionActive = true; 75 | this.logger.log('WebSocket连接已建立'); 76 | 77 | // 订阅行情频道 78 | this.subscribeTicker(symbol); 79 | 80 | // 设置心跳 81 | this.setupHeartbeat(); 82 | }); 83 | 84 | // 接收消息时的处理 85 | this.ws.on('message', (data) => { 86 | try { 87 | const now = new Date(); 88 | let message = {}; 89 | 90 | try { 91 | message = JSON.parse(data.toString()); 92 | // 记录接收到的消息类型到日志文件 93 | if (Math.random() < 0.2) { // 增加采样率到20%以便更好地调试 94 | this.logger.logToFile(`收到WS消息类型: ${JSON.stringify(Object.keys(message))}`); 95 | this.logger.logToFile(`消息内容: ${data.toString().substring(0, 150)}...`); 96 | } 97 | } catch (parseError) { 98 | this.logger.log(`解析WebSocket消息失败: ${parseError.message}`); 99 | return; 100 | } 101 | 102 | // 调用消息回调 103 | if (typeof this.onMessage === 'function') { 104 | this.onMessage(message); 105 | } 106 | 107 | // 处理PONG响应 108 | if (message.result === 'PONG') { 109 | this.logger.logToFile('收到PONG心跳响应'); 110 | return; 111 | } 112 | 113 | // 处理订阅成功响应 114 | if (message.result === null && message.id) { 115 | this.logger.log(`订阅确认: ID=${message.id}`); 116 | return; 117 | } 118 | 119 | // 处理价格数据 - 尝试多种可能的格式 120 | if ( 121 | (message.channel === 'ticker' && message.data) || 122 | (message.e === 'ticker') || 123 | (message.type === 'ticker') || 124 | (message.stream && message.stream.includes('ticker') && message.data) || 125 | (message.s && message.c) || // Binance格式 126 | (message.symbol && message.price) || // 通用格式 127 | (message.data && message.data.s && message.data.c) // 嵌套格式 128 | ) { 129 | this.logger.log(`找到价格数据消息: ${JSON.stringify(message).substring(0, 100)}...`); 130 | this.processPriceData(message, symbol, now); 131 | } else { 132 | // 记录未识别的消息类型 133 | if (Math.random() < 0.1) { 134 | this.logger.logToFile(`未识别的消息格式: ${JSON.stringify(message).substring(0, 100)}...`); 135 | } 136 | } 137 | } catch (error) { 138 | this.logger.log(`处理WebSocket消息错误: ${error.message}`); 139 | } 140 | }); 141 | 142 | // 连接关闭时的处理 143 | this.ws.on('close', () => { 144 | this.connectionActive = false; 145 | this.logger.log('WebSocket连接已关闭'); 146 | 147 | // 清理心跳 148 | if (this.heartbeatInterval) { 149 | clearInterval(this.heartbeatInterval); 150 | this.heartbeatInterval = null; 151 | } 152 | 153 | // 尝试重连 154 | if (!this.reconnectTimeout) { 155 | this.reconnectTimeout = setTimeout(() => { 156 | this.logger.log('尝试重新连接WebSocket...'); 157 | this.reconnectTimeout = null; 158 | this.setupPriceWebSocket(symbol); 159 | }, 5000); 160 | } 161 | }); 162 | 163 | // 错误处理 164 | this.ws.on('error', (error) => { 165 | this.logger.log(`WebSocket错误: ${error.message}`); 166 | }); 167 | } catch (error) { 168 | this.logger.log(`建立WebSocket连接失败: ${error.message}`); 169 | } 170 | } 171 | 172 | /** 173 | * 处理价格数据 174 | * @param {Object} data - 价格数据 175 | * @param {string} symbol - 交易对符号 176 | * @param {Date} now - 当前时间 177 | */ 178 | processPriceData(data, symbol, now) { 179 | try { 180 | // 记录原始数据以便调试 181 | this.logger.logToFile(`处理价格数据: ${JSON.stringify(data).substring(0, 200)}...`); 182 | 183 | let tickerSymbol; 184 | let lastPrice; 185 | 186 | // 处理不同格式的数据 187 | if (data && data.data && data.data.s && data.data.c) { 188 | // Backpack 格式 189 | this.logger.log('识别为Backpack嵌套格式数据'); 190 | tickerSymbol = data.data.s; 191 | lastPrice = parseFloat(data.data.c); 192 | } else if (data && data.s && data.c) { 193 | // Binance 格式 194 | this.logger.log('识别为Binance格式数据'); 195 | tickerSymbol = data.s; 196 | lastPrice = parseFloat(data.c); 197 | } else if (data && data.symbol && data.price) { 198 | // 标准格式 199 | this.logger.log('识别为标准格式数据'); 200 | tickerSymbol = data.symbol; 201 | lastPrice = parseFloat(data.price); 202 | } else if (data && data.data && typeof data.data === 'object') { 203 | // 尝试从data字段中提取 204 | const nestedData = data.data; 205 | if (nestedData.s && nestedData.c) { 206 | this.logger.log('从嵌套data对象提取价格数据'); 207 | tickerSymbol = nestedData.s; 208 | lastPrice = parseFloat(nestedData.c); 209 | } else if (nestedData.symbol && nestedData.price) { 210 | this.logger.log('从嵌套data对象提取标准格式价格数据'); 211 | tickerSymbol = nestedData.symbol; 212 | lastPrice = parseFloat(nestedData.price); 213 | } 214 | } else if (data && data.result && data.result.data) { 215 | // Backpack可能的另一种格式 216 | const resultData = data.result.data; 217 | if (Array.isArray(resultData) && resultData.length > 0) { 218 | const firstItem = resultData[0]; 219 | if (firstItem.s && firstItem.c) { 220 | this.logger.log('从result.data数组提取价格数据'); 221 | tickerSymbol = firstItem.s; 222 | lastPrice = parseFloat(firstItem.c); 223 | } 224 | } 225 | } else { 226 | // 未知格式 - 记录详细信息以便调试 227 | this.logger.log(`未识别的数据格式: ${JSON.stringify(data).substring(0, 100)}...`); 228 | return; 229 | } 230 | 231 | // 处理提取到的价格和符号 232 | if (tickerSymbol && !isNaN(lastPrice) && lastPrice > 0) { 233 | this.logger.log(`成功提取价格数据: 交易对=${tickerSymbol}, 价格=${lastPrice}`); 234 | 235 | // 标准化符号格式 236 | const normalizedSymbol = symbol.replace('-', '_').toUpperCase(); 237 | const normalizedTickerSymbol = tickerSymbol.replace('-', '_').toUpperCase(); 238 | 239 | // 确认交易对匹配 240 | if (normalizedTickerSymbol.includes(normalizedSymbol) || normalizedSymbol.includes(normalizedTickerSymbol)) { 241 | this.handlePriceUpdate(tickerSymbol, lastPrice, symbol, now); 242 | } else { 243 | this.logger.log(`交易对不匹配: 收到=${normalizedTickerSymbol}, 订阅=${normalizedSymbol}`); 244 | } 245 | } else { 246 | this.logger.log(`提取的价格数据无效: 交易对=${tickerSymbol}, 价格=${lastPrice}`); 247 | } 248 | } catch (error) { 249 | this.logger.log(`处理价格数据出错: ${error.message}, 原始数据: ${JSON.stringify(data).substring(0, 100)}...`); 250 | } 251 | } 252 | 253 | /** 254 | * 订阅行情频道 255 | * @param {string} symbol - 交易对符号 256 | */ 257 | subscribeTicker(symbol) { 258 | if (!this.connectionActive || !this.ws) { 259 | this.logger.log('WebSocket未连接,无法订阅行情'); 260 | return false; 261 | } 262 | 263 | try { 264 | // 确保使用正确的格式 265 | const formattedSymbol = symbol.toUpperCase(); 266 | 267 | // 使用多种订阅格式提高成功率 268 | const subscriptions = [ 269 | // 标准格式 270 | { 271 | method: "SUBSCRIBE", 272 | params: [`ticker.${formattedSymbol}`], 273 | id: Date.now() 274 | }, 275 | // 备用格式 276 | { 277 | method: "SUBSCRIBE", 278 | params: [`ticker@${formattedSymbol.replace('_', '')}`], 279 | id: Date.now() + 1 280 | }, 281 | // 再一种备用格式 282 | { 283 | op: "subscribe", 284 | channel: "ticker", 285 | market: formattedSymbol, 286 | id: Date.now() + 2 287 | } 288 | ]; 289 | 290 | // 发送所有订阅格式 291 | for (const sub of subscriptions) { 292 | this.logger.log(`发送订阅请求: ${JSON.stringify(sub)}`); 293 | this.ws.send(JSON.stringify(sub)); 294 | } 295 | 296 | // 请求立即获取一次价格 297 | const getTickerMsg = { 298 | method: "GET_TICKER", 299 | params: { 300 | symbol: formattedSymbol 301 | }, 302 | id: Date.now() + 3 303 | }; 304 | this.logger.log(`请求当前价格: ${JSON.stringify(getTickerMsg)}`); 305 | this.ws.send(JSON.stringify(getTickerMsg)); 306 | 307 | this.logger.log(`已订阅行情: ${formattedSymbol}`); 308 | 309 | return true; 310 | } catch (error) { 311 | this.logger.log(`订阅行情失败: ${error.message}`); 312 | return false; 313 | } 314 | } 315 | 316 | /** 317 | * 设置心跳 318 | */ 319 | setupHeartbeat() { 320 | if (this.heartbeatInterval) { 321 | clearInterval(this.heartbeatInterval); 322 | } 323 | 324 | this.heartbeatInterval = setInterval(() => { 325 | if (this.ws && this.connectionActive) { 326 | try { 327 | // 发送心跳消息 328 | const pingMsg = JSON.stringify({ "op": "ping" }); 329 | this.ws.send(pingMsg); 330 | 331 | // 心跳信息只记录到日志文件,不在终端显示 332 | if (this.logHeartbeats && Math.random() < 0.1) { 333 | this.logger.logToFile('已发送心跳'); 334 | } 335 | } catch (error) { 336 | // 心跳失败是重要错误,保留在终端输出 337 | this.logger.log(`发送心跳失败: ${error.message}`); 338 | } 339 | } 340 | }, 30000); // 每30秒发送一次心跳 341 | } 342 | 343 | /** 344 | * 关闭WebSocket连接 345 | */ 346 | closeWebSocket() { 347 | // 清理心跳 348 | if (this.heartbeatInterval) { 349 | clearInterval(this.heartbeatInterval); 350 | this.heartbeatInterval = null; 351 | } 352 | 353 | // 清理重连定时器 354 | if (this.reconnectTimeout) { 355 | clearTimeout(this.reconnectTimeout); 356 | this.reconnectTimeout = null; 357 | } 358 | 359 | // 关闭连接 360 | if (this.ws) { 361 | try { 362 | this.ws.terminate(); 363 | this.ws = null; 364 | this.connectionActive = false; 365 | this.logger.log('WebSocket连接已关闭'); 366 | } catch (error) { 367 | this.logger.log(`关闭WebSocket连接失败: ${error.message}`); 368 | } 369 | } 370 | } 371 | 372 | /** 373 | * 获取连接状态 374 | * @returns {boolean} 是否已连接 375 | */ 376 | isConnected() { 377 | return this.connectionActive; 378 | } 379 | 380 | /** 381 | * 处理价格更新 382 | * @param {string} tickerSymbol - 交易对符号 383 | * @param {number} lastPrice - 最新价格 384 | * @param {string} symbol - 订阅的原始交易对符号 385 | * @param {Date} now - 当前时间 386 | */ 387 | handlePriceUpdate(tickerSymbol, lastPrice, symbol, now) { 388 | try { 389 | // 避免处理无效数据 390 | if (!tickerSymbol || !lastPrice || isNaN(lastPrice) || lastPrice <= 0) { 391 | return; 392 | } 393 | 394 | // 记录价格更新(如果价格变化显著才在终端显示) 395 | const timeStr = now.toLocaleTimeString(); 396 | 397 | // 显著价格变化或首次接收价格数据时在终端显示 398 | const hasSignificantChange = this.previousPrice && 399 | Math.abs(lastPrice - this.previousPrice) / this.previousPrice > 0.001; 400 | 401 | if (hasSignificantChange || !this.previousPrice) { 402 | // 只在首次接收或有明显变化时在终端显示 403 | if (!this.previousPrice) { 404 | this.logger.log(`首次接收价格数据: ${lastPrice} USDC`); 405 | } else if (hasSignificantChange) { 406 | const changePercent = ((lastPrice - this.previousPrice) / this.previousPrice) * 100; 407 | if (Math.abs(changePercent) > 0.05) { 408 | this.logger.log(`价格变动: ${lastPrice} USDC (${changePercent > 0 ? '+' : ''}${changePercent.toFixed(2)}%)`); 409 | } 410 | } 411 | } else { 412 | // 小变化只记录到日志文件 413 | this.logger.logToFile(`${timeStr} - ${tickerSymbol}: ${lastPrice}`); 414 | } 415 | 416 | // 更新最后成功接收的价格数据 417 | this.lastPriceData = { 418 | symbol: tickerSymbol, 419 | price: lastPrice, 420 | time: now 421 | }; 422 | 423 | this.previousPrice = lastPrice; 424 | 425 | // 通知外部回调 - 重要:确保回调函数的存在性和参数正确 426 | this.logger.log(`准备调用外部价格回调: symbol=${tickerSymbol}, price=${lastPrice}`); 427 | if (typeof this.onPriceUpdate === 'function') { 428 | try { 429 | this.onPriceUpdate(tickerSymbol, lastPrice, now); 430 | this.logger.log(`外部价格回调调用成功`); 431 | } catch (callbackError) { 432 | this.logger.log(`调用价格回调函数失败: ${callbackError.message}`); 433 | } 434 | } else { 435 | this.logger.log(`警告: onPriceUpdate回调未设置或不是函数`); 436 | } 437 | 438 | // 重置失败计数 439 | this.failureCount = 0; 440 | } catch (error) { 441 | this.logger.log(`处理价格更新失败: ${error.message}`); 442 | } 443 | } 444 | } 445 | 446 | module.exports = WebSocketManager; -------------------------------------------------------------------------------- /backpack_exchange-main/backpack_client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.BackpackClient = void 0; 7 | const got_1 = __importDefault(require("got")); 8 | const crypto_1 = __importDefault(require("crypto")); 9 | const qs_1 = __importDefault(require("qs")); 10 | const ws_1 = __importDefault(require("ws")); 11 | const BACKOFF_EXPONENT = 1.5; 12 | const DEFAULT_TIMEOUT_MS = 5000; 13 | const BASE_URL = "https://api.backpack.exchange/"; 14 | // const BASE_URL = "https://api.cf.backpack.exchange/"; 15 | // 执行对应操作的命令 16 | const instructions = { 17 | public: new Map([ 18 | ["assets", { url: `${BASE_URL}api/v1/assets`, method: "GET" }], 19 | ["markets", { url: `${BASE_URL}api/v1/markets`, method: "GET" }], 20 | ["ticker", { url: `${BASE_URL}api/v1/ticker`, method: "GET" }], 21 | ["depth", { url: `${BASE_URL}api/v1/depth`, method: "GET" }], 22 | ["klines", { url: `${BASE_URL}api/v1/klines`, method: "GET" }], 23 | ["status", { url: `${BASE_URL}api/v1/status`, method: "GET" }], 24 | ["ping", { url: `${BASE_URL}api/v1/ping`, method: "GET" }], 25 | ["time", { url: `${BASE_URL}api/v1/time`, method: "GET" }], 26 | ["trades", { url: `${BASE_URL}api/v1/trades`, method: "GET" }], 27 | [ 28 | "tradesHistory", 29 | { url: `${BASE_URL}api/v1/trades/history`, method: "GET" }, 30 | ], 31 | ]), 32 | private: new Map([ 33 | ["balanceQuery", { url: `${BASE_URL}api/v1/capital`, method: "GET" }], 34 | [ 35 | "depositAddressQuery", 36 | { url: `${BASE_URL}wapi/v1/capital/deposit/address`, method: "GET" }, 37 | ], 38 | [ 39 | "depositQueryAll", 40 | { url: `${BASE_URL}wapi/v1/capital/deposits`, method: "GET" }, 41 | ], 42 | [ 43 | "fillHistoryQueryAll", 44 | { url: `${BASE_URL}wapi/v1/history/fills`, method: "GET" }, 45 | ], 46 | ["orderCancel", { url: `${BASE_URL}api/v1/order`, method: "DELETE" }], 47 | ["orderCancelAll", { url: `${BASE_URL}api/v1/orders`, method: "DELETE" }], 48 | ["orderExecute", { url: `${BASE_URL}api/v1/order`, method: "POST" }], 49 | [ 50 | "orderHistoryQueryAll", 51 | { url: `${BASE_URL}wapi/v1/history/orders`, method: "GET" }, 52 | ], 53 | ["orderQuery", { url: `${BASE_URL}api/v1/order`, method: "GET" }], 54 | ["orderQueryAll", { url: `${BASE_URL}api/v1/orders`, method: "GET" }], 55 | ["positionQuery", { url: `${BASE_URL}api/v1/position`, method: "GET" }], 56 | [ 57 | "withdraw", 58 | { url: `${BASE_URL}wapi/v1/capital/withdrawals`, method: "POST" }, 59 | ], 60 | [ 61 | "withdrawalQueryAll", 62 | { url: `${BASE_URL}wapi/v1/capital/withdrawals`, method: "GET" }, 63 | ], 64 | ]), 65 | }; 66 | //解码私钥成pkcs8编码的私钥 67 | const toPkcs8der = (rawB64) => { 68 | try { 69 | // 检查私钥格式 70 | if (!rawB64 || typeof rawB64 !== 'string') { 71 | throw new Error('私钥不能为空且必须是字符串'); 72 | } 73 | 74 | // 移除可能的空格和换行符 75 | rawB64 = rawB64.trim().replace(/\s+/g, ''); 76 | 77 | // 解码base64 78 | const rawPrivate = Buffer.from(rawB64, "base64"); 79 | if (rawPrivate.length < 32) { 80 | throw new Error('私钥长度不足'); 81 | } 82 | 83 | // 只取前32字节 84 | const privateKeyBytes = rawPrivate.subarray(0, 32); 85 | 86 | // 添加ED25519私钥前缀 87 | const prefixPrivateEd25519 = Buffer.from("302e020100300506032b657004220420", "hex"); 88 | const der = Buffer.concat([prefixPrivateEd25519, privateKeyBytes]); 89 | 90 | return crypto_1.default.createPrivateKey({ key: der, format: "der", type: "pkcs8" }); 91 | } catch (error) { 92 | throw new Error(`私钥处理错误: ${error.message}`); 93 | } 94 | }; 95 | //解码公钥成spki编码的公钥 96 | const toSpki = (rawB64) => { 97 | try { 98 | // 检查公钥格式 99 | if (!rawB64 || typeof rawB64 !== 'string') { 100 | throw new Error('公钥不能为空且必须是字符串'); 101 | } 102 | 103 | // 移除可能的空格和换行符 104 | rawB64 = rawB64.trim().replace(/\s+/g, ''); 105 | 106 | // 解码base64 107 | const rawPublic = Buffer.from(rawB64, "base64"); 108 | if (rawPublic.length < 32) { 109 | throw new Error('公钥长度不足'); 110 | } 111 | 112 | // 添加ED25519公钥前缀 113 | const prefixPublicEd25519 = Buffer.from("302a300506032b6570032100", "hex"); 114 | const der = Buffer.concat([prefixPublicEd25519, rawPublic]); 115 | 116 | return crypto_1.default.createPublicKey({ key: der, format: "der", type: "spki" }); 117 | } catch (error) { 118 | throw new Error(`公钥处理错误: ${error.message}`); 119 | } 120 | }; 121 | /** 122 | * 生成签名方法 getMessageSignature 123 | * https://docs.backpack.exchange/#section/Authentication/Signing-requests 124 | * @param {Object} request params as an object 125 | * @param {UInt8Array} privateKey 126 | * @param {number} timestamp Unix time in ms that the request was sent 127 | * @param {string} instruction 128 | * @param {number} window Time window in milliseconds that the request is valid for 129 | * @return {string} base64 encoded signature to include on request 130 | */ 131 | const getMessageSignature = (request, privateKey, timestamp, instruction, window) => { 132 | function alphabeticalSort(a, b) { 133 | return a.localeCompare(b); 134 | } 135 | const message = qs_1.default.stringify(request, { sort: alphabeticalSort }); 136 | const headerInfo = { timestamp, window: window ?? DEFAULT_TIMEOUT_MS }; 137 | const headerMessage = qs_1.default.stringify(headerInfo); 138 | const messageToSign = "instruction=" + 139 | instruction + 140 | "&" + 141 | (message ? message + "&" : "") + 142 | headerMessage; 143 | const signature = crypto_1.default.sign(null, Buffer.from(messageToSign), toPkcs8der(privateKey)); 144 | return signature.toString("base64"); 145 | }; 146 | // 请求方法 rawRequest(命令,请求头,请求参数) 147 | const rawRequest = async (instruction, headers, data) => { 148 | const { url, method } = instructions.private.has(instruction) 149 | ? instructions.private.get(instruction) 150 | : instructions.public.get(instruction); 151 | let fullUrl = url; 152 | headers["User-Agent"] = "Backpack Typescript API Client"; 153 | headers["Content-Type"] = 154 | method == "GET" 155 | ? "application/x-www-form-urlencoded" 156 | : "application/json; charset=utf-8"; 157 | const options = { headers }; 158 | if (method == "GET") { 159 | Object.assign(options, { method }); 160 | fullUrl = 161 | url + (Object.keys(data).length > 0 ? "?" + qs_1.default.stringify(data) : ""); 162 | } 163 | else if (method == "POST" || method == "DELETE") { 164 | Object.assign(options, { 165 | method, 166 | body: JSON.stringify(data), 167 | }); 168 | } 169 | const response = await (0, got_1.default)(fullUrl, options); 170 | const contentType = response.headers["content-type"]; 171 | if (contentType?.includes("application/json")) { 172 | const parsed = JSON.parse(response.body, function (_key, value) { 173 | if (value instanceof Array && value.length == 0) { 174 | return value; 175 | } 176 | if (isNaN(Number(value))) { 177 | return value; 178 | } 179 | return Number(value); 180 | }); 181 | if (parsed.error && parsed.error.length) { 182 | const error = parsed.error 183 | .filter((e) => e.startsWith("E")) 184 | .map((e) => e.substr(1)); 185 | if (!error.length) { 186 | throw new Error("Backpack API returned an unknown error"); 187 | } 188 | throw new Error(`url=${url} body=${options["body"]} err=${error.join(", ")}`); 189 | } 190 | return parsed; 191 | } 192 | else if (contentType?.includes("text/plain")) { 193 | return response.body; 194 | } 195 | else { 196 | return response; 197 | } 198 | }; 199 | /** 200 | * 初始化BackpackClient 填入私钥和公钥 201 | * BackpackClient connects to the Backpack API 202 | * @param {string} privateKey base64 encoded 203 | * @param {string} publicKey base64 encoded 204 | */ 205 | class BackpackClient { 206 | constructor(privateKey, publicKey) { 207 | this.config = { privateKey, publicKey }; 208 | // Verify that the keys are a correct pair before sending any requests. Ran 209 | // into errors before with that which were not obvious. 210 | const pubkeyFromPrivateKey = crypto_1.default 211 | .createPublicKey(toPkcs8der(privateKey)) 212 | .export({ format: "der", type: "spki" }) 213 | .toString("base64"); 214 | const pubkey = toSpki(publicKey) 215 | .export({ format: "der", type: "spki" }) 216 | .toString("base64"); 217 | if (pubkeyFromPrivateKey != pubkey) { 218 | throw new Error("错误的秘钥对,请检查私钥公钥是否匹配"); 219 | } 220 | } 221 | /** 222 | * 发送公共或私有API请求 223 | * This method makes a public or private API request. 224 | * @param {String} method 方法名 The API method (public or private) 225 | * @param {Object} params Arguments to pass to the api call 226 | * @param {Number} retrysLeft 227 | * @return {Object} The response object 重试请求的次数 228 | */ 229 | async api(method, params, retrysLeft = 10) { 230 | try { 231 | if (instructions.public.has(method)) { 232 | return await this.publicMethod(method, params); 233 | } 234 | else if (instructions.private.has(method)) { 235 | return await this.privateMethod(method, params); 236 | } 237 | } 238 | catch (e) { 239 | if (retrysLeft > 0) { 240 | const numTry = 11 - retrysLeft; 241 | const backOff = Math.pow(numTry, BACKOFF_EXPONENT); 242 | console.warn("BPX api error", { 243 | method, 244 | numTry, 245 | backOff, 246 | }, e.toString(), e.response && e.response.body ? e.response.body : ''); 247 | await new Promise((resolve) => setTimeout(resolve, backOff * 1000)); 248 | return await this.api(method, params, retrysLeft - 1); 249 | } 250 | else { 251 | throw e; 252 | } 253 | } 254 | throw new Error(method + " is not a valid API method."); 255 | } 256 | /** 257 | * 发送公共API请求 258 | * This method makes a public API request. 259 | * @param {String} instruction The API method (public or private) 260 | * @param {Object} params Arguments to pass to the api call 261 | * @return {Object} The response object 262 | */ 263 | async publicMethod(instruction, params = {}) { 264 | const response = await rawRequest(instruction, {}, params); 265 | return response; 266 | } 267 | /** 268 | * 发送私有API请求 269 | * This method makes a private API request. 270 | * @param {String} instruction The API method (public or private) 271 | * @param {Object} params Arguments to pass to the api call 272 | * @return {Object} The response object 273 | */ 274 | async privateMethod(instruction, params = {}) { 275 | const timestamp = Date.now(); 276 | const signature = getMessageSignature(params, this.config.privateKey, timestamp, instruction); 277 | const headers = { 278 | "X-Timestamp": timestamp, 279 | "X-Window": this.config.timeout ?? DEFAULT_TIMEOUT_MS, 280 | "X-API-Key": this.config.publicKey, 281 | "X-Signature": signature, 282 | }; 283 | const response = await rawRequest(instruction, headers, params); 284 | return response; 285 | } 286 | /** 287 | * https://docs.backpack.exchange/#tag/Capital/operation/get_balances 288 | */ 289 | async Balance() { 290 | return this.api("balanceQuery"); 291 | } 292 | /** 293 | * https://docs.backpack.exchange/#tag/Futures/operation/get_positions 294 | */ 295 | async Position() { 296 | return this.api("positionQuery"); 297 | } 298 | /** 299 | * https://docs.backpack.exchange/#tag/Capital/operation/get_deposits 300 | */ 301 | async Deposits(params) { 302 | return this.api("depositQueryAll", params); 303 | } 304 | /** 305 | * https://docs.backpack.exchange/#tag/Capital/operation/get_deposit_address 306 | */ 307 | async DepositAddress(params) { 308 | return this.api("depositAddressQuery", params); 309 | } 310 | /** 311 | * https://docs.backpack.exchange/#tag/Capital/operation/get_withdrawals 312 | */ 313 | async Withdrawals(params) { 314 | return this.api("withdrawalQueryAll", params); 315 | } 316 | /** 317 | * https://docs.backpack.exchange/#tag/Capital/operation/request_withdrawal 318 | */ 319 | async Withdraw(params) { 320 | this.api("withdraw", params); 321 | } 322 | /** 323 | * https://docs.backpack.exchange/#tag/History/operation/get_order_history 324 | */ 325 | async OrderHistory(params) { 326 | return this.api("orderHistoryQueryAll", params); 327 | } 328 | /** 329 | * https://docs.backpack.exchange/#tag/History/operation/get_fills 330 | */ 331 | async FillHistory(params) { 332 | return this.api("fillHistoryQueryAll", params); 333 | } 334 | /** 335 | * https://docs.backpack.exchange/#tag/Markets/operation/get_assets 336 | */ 337 | async Assets() { 338 | return this.api("assets"); 339 | } 340 | /** 341 | * https://docs.backpack.exchange/#tag/Markets/operation/get_markets 342 | */ 343 | async Markets() { 344 | return this.api("markets"); 345 | } 346 | /** 347 | * https://docs.backpack.exchange/#tag/Markets/operation/get_ticker 348 | */ 349 | async Ticker(params) { 350 | return this.api("ticker", params); 351 | } 352 | /** 353 | * https://docs.backpack.exchange/#tag/Markets/operation/get_depth 354 | */ 355 | async Depth(params) { 356 | return this.api("depth", params); 357 | } 358 | /** 359 | * https://docs.backpack.exchange/#tag/Markets/operation/get_klines 360 | */ 361 | async KLines(params) { 362 | return this.api("klines", params); 363 | } 364 | /** 365 | * https://docs.backpack.exchange/#tag/Order/operation/get_order 366 | */ 367 | async GetOrder(params) { 368 | return this.api("orderQuery", params); 369 | } 370 | /** 371 | * https://docs.backpack.exchange/#tag/Order/operation/execute_order 372 | */ 373 | async ExecuteOrder(params) { 374 | return this.api("orderExecute", params, 0); 375 | } 376 | /** 377 | * https://docs.backpack.exchange/#tag/Order/operation/cancel_order 378 | */ 379 | async CancelOrder(params) { 380 | return this.api("orderCancel", params); 381 | } 382 | /** 383 | * https://docs.backpack.exchange/#tag/Order/operation/get_open_orders 384 | */ 385 | async GetOpenOrders(params) { 386 | return this.api("orderQueryAll", params); 387 | } 388 | /** 389 | * https://docs.backpack.exchange/#tag/Order/operation/cancel_open_orders 390 | */ 391 | async CancelOpenOrders(params) { 392 | return this.api("orderCancelAll", params); 393 | } 394 | /** 395 | * https://docs.backpack.exchange/#tag/System/operation/get_status 396 | */ 397 | async Status() { 398 | return this.api("status"); 399 | } 400 | /** 401 | * https://docs.backpack.exchange/#tag/System/operation/ping 402 | */ 403 | async Ping() { 404 | return this.api("ping"); 405 | } 406 | /** 407 | * https://docs.backpack.exchange/#tag/System/operation/get_time 408 | */ 409 | async Time() { 410 | return this.api("time"); 411 | } 412 | /** 413 | * https://docs.backpack.exchange/#tag/Trades/operation/get_recent_trades 414 | */ 415 | async RecentTrades(params) { 416 | return this.api("trades", params); 417 | } 418 | /** 419 | * https://docs.backpack.exchange/#tag/Trades/operation/get_historical_trades 420 | */ 421 | async HistoricalTrades(params) { 422 | return this.api("tradesHistory", params); 423 | } 424 | /** 425 | * https://docs.backpack.exchange/#tag/Streams/Private 426 | * @return {Object} Websocket Websocket connecting to order update stream 427 | */ 428 | subscribeOrderUpdate() { 429 | const privateStream = new ws_1.default('wss://ws.backpack.exchange'); 430 | const timestamp = Date.now(); 431 | const window = 5000; 432 | const signature = getMessageSignature({}, this.config.privateKey, timestamp, "subscribe", window); 433 | const subscriptionData = { 434 | method: 'SUBSCRIBE', 435 | params: ["account.orderUpdate"], 436 | "signature": [this.config.publicKey, signature, timestamp.toString(), window.toString()] 437 | }; 438 | privateStream.onopen = (_) => { 439 | console.log('Connected to BPX Websocket'); 440 | privateStream.send(JSON.stringify(subscriptionData)); 441 | }; 442 | privateStream.onerror = (error) => { 443 | console.log(`Websocket Error ${error}`); 444 | }; 445 | return privateStream; 446 | } 447 | } 448 | exports.BackpackClient = BackpackClient; -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const BackpackService = require('./services/backpackService'); 2 | const PriceMonitor = require('./core/priceMonitor'); 3 | const TradingStrategy = require('./core/tradingStrategy'); 4 | const OrderManagerService = require('./core/orderManager'); 5 | const { Order, OrderManager } = require('./models/Order'); 6 | const TradeStats = require('./models/TradeStats'); 7 | const { log, defaultLogger } = require('./utils/logger'); 8 | const TimeUtils = require('./utils/timeUtils'); 9 | const Formatter = require('./utils/formatter'); 10 | 11 | /** 12 | * 应用程序类 - 协调各个组件工作 13 | */ 14 | class TradingApp { 15 | /** 16 | * 构造函数 17 | * @param {Object} config - 配置对象 18 | */ 19 | constructor(config) { 20 | this.config = config; 21 | this.logger = defaultLogger; 22 | 23 | // 初始化组件 24 | this.backpackService = new BackpackService(config, this.logger); 25 | this.tradingStrategy = new TradingStrategy(this.logger, this.config); 26 | this.orderManager = new OrderManager(); 27 | this.tradeStats = new TradeStats(); 28 | 29 | // 初始化订单管理服务 30 | this.orderManagerService = new OrderManagerService(config, this.backpackService); 31 | 32 | // 初始化价格监控器 33 | this.priceMonitor = new PriceMonitor({ 34 | config: config, 35 | onPriceUpdate: this.handlePriceUpdate.bind(this), 36 | logger: this.logger 37 | }); 38 | 39 | // 应用状态 40 | this.running = false; 41 | this.symbol = null; 42 | this.tradingCoin = null; 43 | this.currentPriceInfo = null; 44 | this.monitoringInterval = null; 45 | this.scriptStartTime = new Date(); 46 | this.cycleLogFile = null; 47 | this.lastDisplayTime = 0; 48 | this.displayInitialized = false; 49 | this.takeProfitTriggered = false; 50 | this.needRestart = false; // 标记是否需要重启 51 | } 52 | 53 | /** 54 | * 处理价格更新 55 | * @param {Object} priceInfo - 价格信息 56 | */ 57 | handlePriceUpdate(priceInfo) { 58 | // 确保从WebSocket接收到的价格能够被更新到应用状态 59 | this.currentPriceInfo = priceInfo; 60 | 61 | // 计算价格涨幅 62 | if (priceInfo && this.tradeStats.averagePrice > 0) { 63 | const priceIncrease = ((priceInfo.price - this.tradeStats.averagePrice) / this.tradeStats.averagePrice) * 100; 64 | this.currentPriceInfo.increase = priceIncrease; 65 | 66 | // 如果价格变化大,记录到终端 67 | if (Math.abs(priceIncrease) > 0.1) { 68 | const direction = priceIncrease >= 0 ? '上涨' : '下跌'; 69 | log(`相对均价${direction}: ${Math.abs(priceIncrease).toFixed(2)}% (当前: ${priceInfo.price.toFixed(2)}, 均价: ${this.tradeStats.averagePrice.toFixed(2)})`); 70 | } 71 | 72 | // 检查是否达到止盈条件 - 只有当有成交的买单时才检查止盈 73 | if (this.tradeStats.filledOrders > 0 && this.running && !this.takeProfitTriggered) { 74 | const takeProfitPercentage = this.config.trading.takeProfitPercentage; 75 | 76 | // 检查是否达到止盈条件 77 | const takeProfitReached = this.tradingStrategy.isTakeProfitTriggered( 78 | priceInfo.price, 79 | this.tradeStats.averagePrice, 80 | takeProfitPercentage 81 | ); 82 | 83 | if (takeProfitReached) { 84 | log(`\n===== 止盈条件达成!=====`); 85 | log(`当前价格: ${priceInfo.price} USDC`); 86 | log(`平均买入价: ${this.tradeStats.averagePrice.toFixed(2)} USDC`); 87 | log(`涨幅: ${priceIncrease.toFixed(2)}% >= 止盈点: ${takeProfitPercentage}%`); 88 | log('准备卖出获利...'); 89 | 90 | // 设置止盈触发标志,避免重复触发 91 | this.takeProfitTriggered = true; 92 | 93 | // 执行止盈操作 94 | this.executeTakeProfit(); 95 | } 96 | } 97 | } 98 | 99 | // 更新显示(限制频率) 100 | const now = Date.now(); 101 | if (!this.lastDisplayTime || (now - this.lastDisplayTime) > 15000) { 102 | this.displayAccountInfo(); 103 | this.lastDisplayTime = now; 104 | } 105 | } 106 | 107 | /** 108 | * 执行止盈操作 109 | */ 110 | async executeTakeProfit() { 111 | try { 112 | // 先取消所有未成交的买单 113 | await this.cancelAllOrders(); 114 | 115 | // 执行卖出操作 116 | await this.sellAllPosition(); 117 | 118 | // 清除监控间隔 119 | if (this.monitoringInterval) { 120 | clearInterval(this.monitoringInterval); 121 | this.monitoringInterval = null; 122 | } 123 | 124 | // 检查是否需要在止盈后自动重启 125 | if (this.config.actions.restartAfterTakeProfit) { 126 | log('\n===== 止盈后自动重启交易 ====='); 127 | // 设置需要重启标志 128 | this.needRestart = true; 129 | } 130 | } catch (error) { 131 | log(`执行止盈操作时出错: ${error.message}`, true); 132 | } 133 | } 134 | 135 | /** 136 | * 初始化交易环境 137 | */ 138 | async initialize() { 139 | try { 140 | log('正在初始化交易环境...'); 141 | 142 | // 读取并设置配置 143 | this.config = this.config || {}; 144 | this.tradingCoin = this.config.trading?.tradingCoin || this.config.tradingCoin || 'BTC'; 145 | this.symbol = `${this.tradingCoin}_USDC`; 146 | this.apiSymbol = this.symbol; // 使用相同的格式,不需要转换 147 | 148 | log(`交易对: ${this.apiSymbol}`); 149 | 150 | // 初始化服务和管理器 151 | // TimeUtils是静态类,不需要实例化 152 | this.orderManager = new OrderManager(this.logger); 153 | 154 | // 确保传递logger给所有服务 155 | this.backpackService = new BackpackService(this.config, this.logger); 156 | this.priceMonitor = new PriceMonitor({ 157 | config: this.config, 158 | onPriceUpdate: this.handlePriceUpdate.bind(this), 159 | logger: this.logger 160 | }); 161 | this.tradingStrategy = new TradingStrategy(this.logger, this.config); 162 | 163 | log('所有服务初始化完成'); 164 | 165 | // 记录应用启动时间 166 | this.startTime = new Date(); 167 | log(`程序启动时间: ${this.startTime.toLocaleString()}`); 168 | 169 | // 初始化状态变量 170 | this.running = false; 171 | this.needRestart = false; 172 | this.lastTradeTime = new Date(); 173 | this.lastStatusLogTime = new Date(); 174 | 175 | // 设置价格监控回调 176 | this.priceMonitor.onPriceUpdate = (priceInfo) => this.handlePriceUpdate(priceInfo); 177 | 178 | // 尝试获取初始价格 179 | try { 180 | const ticker = await this.backpackService.getTicker(this.apiSymbol); 181 | if (ticker && ticker.lastPrice) { 182 | log(`初始价格: ${ticker.lastPrice} USDC (来源: API)`); 183 | this.currentPrice = parseFloat(ticker.lastPrice); 184 | } else { 185 | log('警告: 无法获取初始价格'); 186 | } 187 | } catch (error) { 188 | log(`获取初始价格失败: ${error.message}`); 189 | } 190 | 191 | return true; 192 | } catch (error) { 193 | log(`初始化失败: ${error.message}`); 194 | return false; 195 | } 196 | } 197 | 198 | /** 199 | * 启动交易应用 200 | */ 201 | async start() { 202 | try { 203 | if (this.running) { 204 | log('应用程序已经在运行中'); 205 | return false; 206 | } 207 | 208 | // 初始化环境 209 | const initialized = await this.initialize(); 210 | if (!initialized) { 211 | log('初始化失败,应用程序无法启动', true); 212 | return false; 213 | } 214 | 215 | // 启动价格监控 216 | this.priceMonitor.startMonitoring(this.symbol); 217 | 218 | // 添加轮询检查机制,每5秒检查一次价格数据,避免WebSocket回调失败的情况 219 | this.priceCheckInterval = setInterval(() => { 220 | try { 221 | // 直接从priceMonitor获取价格数据 222 | if (this.priceMonitor.currentPrice > 0) { 223 | const priceInfo = { 224 | price: this.priceMonitor.currentPrice, 225 | symbol: this.symbol, 226 | source: 'WebSocket轮询', 227 | updateTime: this.priceMonitor.lastUpdateTime || Date.now() 228 | }; 229 | 230 | log(`轮询获取价格: ${priceInfo.price} USDC`); 231 | 232 | // 更新当前价格信息 233 | this.currentPriceInfo = priceInfo; 234 | 235 | // 计算涨跌幅 236 | if (this.tradeStats.averagePrice > 0) { 237 | const priceIncrease = ((priceInfo.price - this.tradeStats.averagePrice) / this.tradeStats.averagePrice) * 100; 238 | this.currentPriceInfo.increase = priceIncrease; 239 | } 240 | 241 | // 更新显示 242 | this.displayAccountInfo(); 243 | } 244 | // 如果priceMonitor没有价格数据,但WebSocketManager有 245 | else if (this.priceMonitor.wsManager && 246 | this.priceMonitor.wsManager.lastPriceData && 247 | this.priceMonitor.wsManager.lastPriceData.price > 0) { 248 | 249 | const wsData = this.priceMonitor.wsManager.lastPriceData; 250 | const priceInfo = { 251 | price: wsData.price, 252 | symbol: wsData.symbol || this.symbol, 253 | source: 'WebSocketManager轮询', 254 | updateTime: wsData.time || Date.now() 255 | }; 256 | 257 | log(`轮询从WebSocketManager获取价格: ${priceInfo.price} USDC`); 258 | 259 | // 更新当前价格信息 260 | this.currentPriceInfo = priceInfo; 261 | 262 | // 计算涨跌幅 263 | if (this.tradeStats.averagePrice > 0) { 264 | const priceIncrease = ((priceInfo.price - this.tradeStats.averagePrice) / this.tradeStats.averagePrice) * 100; 265 | this.currentPriceInfo.increase = priceIncrease; 266 | } 267 | 268 | // 更新显示 269 | this.displayAccountInfo(); 270 | } 271 | } catch (error) { 272 | log(`价格轮询错误: ${error.message}`, true); 273 | } 274 | }, 5000); 275 | 276 | this.running = true; 277 | 278 | // 返回成功 279 | return true; 280 | } catch (error) { 281 | log(`启动应用程序失败: ${error.message}`, true); 282 | this.stop(); 283 | return false; 284 | } 285 | } 286 | 287 | /** 288 | * 停止交易应用 289 | */ 290 | stop() { 291 | if (!this.running) return; 292 | 293 | log('正在停止应用程序...'); 294 | 295 | // 停止价格监控 296 | this.priceMonitor.stopMonitoring(); 297 | 298 | // 清除监控间隔 299 | if (this.monitoringInterval) { 300 | clearInterval(this.monitoringInterval); 301 | this.monitoringInterval = null; 302 | } 303 | 304 | // 清除价格检查间隔 305 | if (this.priceCheckInterval) { 306 | clearInterval(this.priceCheckInterval); 307 | this.priceCheckInterval = null; 308 | } 309 | 310 | this.running = false; 311 | log('应用程序已停止'); 312 | } 313 | 314 | /** 315 | * 撤销所有未成交订单 316 | */ 317 | async cancelAllOrders() { 318 | if (!this.running) { 319 | log('应用程序未运行,无法撤销订单'); 320 | return false; 321 | } 322 | 323 | try { 324 | log(`开始撤销 ${this.symbol} 交易对的所有未完成订单...`); 325 | const result = await this.backpackService.cancelAllOrders(this.symbol); 326 | log(`撤销订单结果: ${JSON.stringify(result)}`); 327 | return true; 328 | } catch (error) { 329 | log(`撤销订单失败: ${error.message}`, true); 330 | return false; 331 | } 332 | } 333 | 334 | /** 335 | * 执行交易操作 336 | */ 337 | async executeTrade() { 338 | try { 339 | log('开始执行交易策略...'); 340 | 341 | // 检查当前价格 342 | if (!this.currentPrice || this.currentPrice <= 0) { 343 | log('警告: 当前价格无效,无法执行交易'); 344 | return false; 345 | } 346 | 347 | log(`当前价格: ${this.currentPrice} USDC`); 348 | 349 | // 取消所有现有订单 350 | try { 351 | await this.backpackService.cancelAllOrders(this.apiSymbol); 352 | log('已取消所有现有订单'); 353 | } catch (error) { 354 | log(`取消所有订单失败: ${error.message}`); 355 | } 356 | 357 | // 从配置中获取交易参数 358 | const maxDropPercentage = this.config.trading.maxDropPercentage; 359 | const totalAmount = this.config.trading.totalAmount; 360 | const orderCount = this.config.trading.orderCount; 361 | const incrementPercentage = this.config.trading.incrementPercentage; 362 | const minOrderAmount = this.config.advanced?.minOrderAmount || 10; 363 | 364 | // 确保所有交易参数都有效 365 | if (!maxDropPercentage || !totalAmount || !orderCount || !incrementPercentage) { 366 | log('警告: 交易参数无效,请检查配置文件', true); 367 | return false; 368 | } 369 | 370 | // 计算阶梯订单 371 | const orders = this.tradingStrategy.calculateIncrementalOrders( 372 | this.currentPrice, 373 | maxDropPercentage, 374 | totalAmount, 375 | orderCount, 376 | incrementPercentage, 377 | minOrderAmount, 378 | this.tradingCoin, 379 | this.apiSymbol 380 | ); 381 | 382 | if (!orders || orders.length === 0) { 383 | log('警告: 没有生成有效的订单'); 384 | return false; 385 | } 386 | 387 | log(`已生成 ${orders.length} 个阶梯买单`); 388 | 389 | // 创建订单 390 | let successCount = 0; 391 | for (const order of orders) { 392 | try { 393 | // 检查是否已存在相同的订单 394 | const orderSignature = `${order.symbol}_${order.price}_${order.quantity}`; 395 | if (this.orderManager.hasOrderSignature(orderSignature)) { 396 | log(`跳过重复订单: ${orderSignature}`); 397 | continue; 398 | } 399 | 400 | // 创建订单 401 | const result = await this.backpackService.createOrder({ 402 | symbol: this.apiSymbol, 403 | side: 'Bid', 404 | orderType: 'Limit', 405 | price: order.price.toFixed(2), 406 | quantity: order.quantity.toFixed(6) 407 | }); 408 | 409 | if (result && result.id) { 410 | // 添加到订单管理器 411 | const newOrder = new Order({ 412 | id: result.id, 413 | symbol: order.symbol, 414 | side: 'Bid', 415 | price: order.price, 416 | quantity: order.quantity, 417 | status: result.status || 'New' 418 | }); 419 | 420 | // 添加订单到管理器,并记录这个签名 421 | this.orderManager.addOrder(newOrder); 422 | this.orderManager.createdOrderSignatures.add(orderSignature); 423 | 424 | // 增加总订单计数 425 | this.tradeStats.totalOrders++; 426 | 427 | // 在终端显示订单创建信息(确保显示) 428 | log(`订单已创建: ${result.id} - ${order.quantity} ${this.tradingCoin} @ ${order.price} USDC`); 429 | successCount++; 430 | } else { 431 | log(`订单创建失败: ${JSON.stringify(order)}`); 432 | } 433 | } catch (error) { 434 | log(`创建订单失败: ${error.message}, 订单: ${JSON.stringify(order)}`); 435 | } 436 | } 437 | 438 | log(`成功创建 ${successCount}/${orders.length} 个订单`); 439 | 440 | // 更新最后交易时间 441 | this.lastTradeTime = new Date(); 442 | 443 | // 启动止盈监控 444 | this.startTakeProfitMonitoring(); 445 | 446 | return successCount > 0; 447 | } catch (error) { 448 | log(`执行交易操作失败: ${error.message}`); 449 | if (error.stack) { 450 | log(`错误堆栈: ${error.stack}`); 451 | } 452 | return false; 453 | } 454 | } 455 | 456 | /** 457 | * 查询订单并更新统计 458 | */ 459 | async queryOrdersAndUpdateStats() { 460 | try { 461 | log('查询当前交易周期新成交的订单...'); 462 | 463 | // 获取当前未成交订单 464 | const openOrders = await this.backpackService.getOpenOrders(this.symbol); 465 | const currentOpenOrderIds = new Set(openOrders.map(order => order.id)); 466 | 467 | // 遍历所有创建的订单,检查哪些已经不在未成交列表中 468 | const filledOrders = []; 469 | for (const orderId of this.orderManager.getAllCreatedOrderIds()) { 470 | if (!currentOpenOrderIds.has(orderId)) { 471 | const order = this.orderManager.getOrder(orderId); 472 | 473 | // 如果订单存在且未处理,则视为已成交 474 | if (order && !this.tradeStats.isOrderProcessed(orderId)) { 475 | // 将订单标记为已成交 476 | order.status = 'Filled'; 477 | 478 | // 确保设置正确的成交数量和金额 479 | // 如果订单已成交,应该将全部数量和金额标记为已成交 480 | if (order.filledQuantity <= 0) { 481 | order.filledQuantity = order.quantity; 482 | } 483 | 484 | if (order.filledAmount <= 0) { 485 | order.filledAmount = order.price * order.quantity; 486 | } 487 | 488 | // 添加到已成交订单列表 489 | filledOrders.push(order); 490 | 491 | // 记录订单成交信息 492 | log(`订单已成交: ${orderId} - ${order.quantity} ${this.tradingCoin} @ ${order.price} USDC`); 493 | } 494 | } 495 | } 496 | 497 | // 更新统计信息 498 | for (const order of filledOrders) { 499 | const result = this.tradeStats.updateStats(order); 500 | if (result) { 501 | // 如果统计更新成功,记录成交信息 502 | log(`更新交易统计: 成交订单数=${this.tradeStats.filledOrders}, 均价=${this.tradeStats.averagePrice.toFixed(2)} USDC`); 503 | } 504 | } 505 | 506 | // 更新订单管理器中的待处理订单ID列表 507 | this.orderManager.updatePendingOrderIds(Array.from(currentOpenOrderIds)); 508 | 509 | return filledOrders.length > 0; 510 | } catch (error) { 511 | log(`查询订单历史并更新统计失败: ${error.message}`, true); 512 | return false; 513 | } 514 | } 515 | 516 | /** 517 | * 开始监控止盈条件 518 | */ 519 | async startTakeProfitMonitoring() { 520 | if (!this.running) { 521 | log('应用程序未运行,无法开始监控止盈条件'); 522 | return false; 523 | } 524 | 525 | // 获取止盈百分比 526 | const takeProfitPercentage = this.config.trading.takeProfitPercentage; 527 | log(`\n开始监控止盈条件 (${takeProfitPercentage}%)...`); 528 | 529 | // 首次显示账户信息 530 | this.displayAccountInfo(); 531 | 532 | // 启动监控间隔 533 | if (this.monitoringInterval) { 534 | clearInterval(this.monitoringInterval); 535 | } 536 | 537 | // 监控变量 538 | let monitoringAttempts = 0; 539 | this.takeProfitTriggered = false; 540 | let lastOrderCheckTime = Date.now(); 541 | 542 | // 无订单成交自动重启相关变量 543 | const autoRestartNoFill = this.config.actions.autoRestartNoFill === true; 544 | const noFillRestartMinutes = this.config.advanced.noFillRestartMinutes || 60; 545 | const noFillRestartMs = noFillRestartMinutes * 60 * 1000; 546 | const initialStartTime = Date.now(); 547 | let hadFilledOrders = this.tradeStats.filledOrders > 0; 548 | 549 | if (autoRestartNoFill) { 550 | log(`启用无订单成交自动重启: 如果 ${noFillRestartMinutes} 分钟内没有订单成交,将自动重启脚本`); 551 | } 552 | 553 | // 添加心跳计时器 554 | const heartbeatInterval = setInterval(() => { 555 | const timeNow = new Date().toLocaleString(); 556 | this.logger.logToFile(`心跳检查: 脚本正在运行 ${timeNow}`); 557 | }, 60000); 558 | 559 | this.monitoringInterval = setInterval(async () => { 560 | try { 561 | monitoringAttempts++; 562 | 563 | // 记录每一轮监控的开始 564 | const cycleStartTime = Date.now(); 565 | this.logger.logToFile(`开始第 ${monitoringAttempts} 轮订单监控检查`); 566 | 567 | // 更新显示 568 | this.displayAccountInfo(); 569 | 570 | // 每次检查前都更新统计数据,确保使用最新的订单状态 571 | let hasFilledOrders = false; 572 | try { 573 | hasFilledOrders = await this.queryOrdersAndUpdateStats(); 574 | } catch (statsError) { 575 | this.logger.logToFile(`更新订单统计时出错: ${statsError.message}`, true); 576 | } 577 | 578 | // 如果之前没有成交订单,但现在有了,则记录这一状态变化 579 | if (!hadFilledOrders && hasFilledOrders) { 580 | this.logger.logToFile(`检测到首次订单成交,自动重启计时器已取消`); 581 | hadFilledOrders = true; 582 | } 583 | 584 | // 检查是否需要因无订单成交而重启 585 | if (autoRestartNoFill && !hadFilledOrders) { 586 | const runningTimeMs = Date.now() - initialStartTime; 587 | 588 | if (runningTimeMs >= noFillRestartMs) { 589 | log(`\n===== 无订单成交自动重启触发 =====`); 590 | log(`已运行 ${Math.floor(runningTimeMs / 60000)} 分钟无任何订单成交`); 591 | log(`根据配置,系统将重新开始交易...`); 592 | 593 | // 先取消所有未成交订单 594 | await this.cancelAllOrders(); 595 | 596 | clearInterval(heartbeatInterval); 597 | clearInterval(this.monitoringInterval); 598 | 599 | // 重置应用状态 600 | this.resetAppState(); 601 | 602 | // 设置需要重启标志 603 | this.needRestart = true; 604 | 605 | return true; 606 | } 607 | } 608 | 609 | // 定期检查未成交的订单状态 610 | const orderCheckIntervalMs = Math.max(1, this.config.advanced.checkOrdersIntervalMinutes || 10) * 60 * 1000; 611 | const checkTimeNow = Date.now(); 612 | 613 | if (checkTimeNow - lastOrderCheckTime > orderCheckIntervalMs) { 614 | await this.queryOrdersAndUpdateStats(); 615 | lastOrderCheckTime = checkTimeNow; 616 | } 617 | 618 | // 注:价格和止盈检查已经在handlePriceUpdate方法中处理 619 | 620 | } catch (error) { 621 | log(`监控过程中发生错误: ${error.message}`, true); 622 | // 出错后等待短一点的时间再继续,避免长时间卡住 623 | } 624 | }, this.config.advanced.monitorIntervalSeconds * 1000); 625 | } 626 | 627 | /** 628 | * 卖出所有持仓 629 | */ 630 | async sellAllPosition() { 631 | try { 632 | // 获取当前持仓情况 633 | const position = await this.backpackService.getPosition(this.symbol); 634 | if (!position || parseFloat(position.quantity) <= 0) { 635 | log('没有可卖出的持仓'); 636 | return null; 637 | } 638 | 639 | // 获取当前市场价格 640 | const ticker = await this.backpackService.getTicker(this.symbol); 641 | const currentPrice = parseFloat(ticker.lastPrice); 642 | 643 | // 设置卖出价格 644 | const sellPrice = this.tradingStrategy.calculateOptimalSellPrice(currentPrice, this.tradingCoin); 645 | const quantity = Formatter.adjustQuantityToStepSize(parseFloat(position.quantity), this.tradingCoin, this.config); 646 | 647 | log(`准备卖出: ${quantity} ${this.tradingCoin}, 当前市场价=${currentPrice}, 卖出价=${sellPrice}`); 648 | 649 | // 创建卖出订单 650 | const response = await this.backpackService.createSellOrder( 651 | this.symbol, 652 | sellPrice, 653 | quantity, 654 | this.tradingCoin 655 | ); 656 | 657 | if (response && response.id) { 658 | log(`卖出订单创建成功: 订单ID=${response.id}, 状态=${response.status}`); 659 | 660 | // 检查订单是否完全成交 661 | let fullyFilled = response.status === 'Filled'; 662 | 663 | // 如果订单未完全成交,尝试再次以更低价格卖出剩余部分 664 | if (!fullyFilled) { 665 | log('订单未完全成交,检查剩余数量并尝试以更低价格卖出'); 666 | 667 | // 等待一小段时间,让订单有时间处理 668 | await TimeUtils.delay(2000); 669 | 670 | // 获取更新后的持仓 671 | const updatedPosition = await this.backpackService.getPosition(this.symbol); 672 | if (updatedPosition && parseFloat(updatedPosition.quantity) > 0) { 673 | const remainingQuantity = Formatter.adjustQuantityToStepSize(parseFloat(updatedPosition.quantity), this.tradingCoin, this.config); 674 | 675 | log(`仍有 ${remainingQuantity} ${this.tradingCoin} 未售出,尝试以更低价格卖出`); 676 | 677 | // 计算更低的卖出价格 678 | const lowerSellPrice = this.tradingStrategy.calculateSecondSellPrice(currentPrice, this.tradingCoin); 679 | 680 | // 创建第二次卖出订单 681 | const secondResponse = await this.backpackService.createSellOrder( 682 | this.symbol, 683 | lowerSellPrice, 684 | remainingQuantity, 685 | this.tradingCoin 686 | ); 687 | 688 | if (secondResponse && secondResponse.id) { 689 | log(`第二次卖出订单创建成功: 订单ID=${secondResponse.id}, 状态=${secondResponse.status}`); 690 | } 691 | } else { 692 | log(`所有 ${this.tradingCoin} 已售出`); 693 | } 694 | } 695 | 696 | return response; 697 | } else { 698 | throw new Error('卖出订单创建失败:响应中没有订单ID'); 699 | } 700 | } catch (error) { 701 | log(`卖出失败: ${error.message}`, true); 702 | return null; 703 | } 704 | } 705 | 706 | /** 707 | * 显示账户信息 708 | */ 709 | displayAccountInfo() { 710 | try { 711 | // 准备数据 712 | const timeNow = new Date().toLocaleString(); 713 | const takeProfitPercentage = this.config.trading.takeProfitPercentage; 714 | const elapsedTime = TimeUtils.getElapsedTime(this.scriptStartTime); 715 | 716 | // 价格信息 717 | let priceInfo = "等待WebSocket数据..."; 718 | let priceChangeSymbol = ""; 719 | let percentProgress = "0"; 720 | 721 | // 获取当前的WebSocket连接状态 722 | let wsConnected = this.priceMonitor.isMonitoring(); 723 | 724 | // 显示WebSocket连接状态及上次更新时间 725 | let wsStatusInfo = wsConnected ? "已连接" : "连接中..."; 726 | 727 | // 如果有价格监控的上次更新时间,显示距离上次更新的时间 728 | if (this.priceMonitor.lastUpdateTime) { 729 | const lastUpdateTimeString = new Date(this.priceMonitor.lastUpdateTime).toLocaleTimeString(); 730 | const dataAge = Math.floor((Date.now() - this.priceMonitor.lastUpdateTime) / 1000); 731 | wsStatusInfo += ` (${lastUpdateTimeString}, ${dataAge}秒前)`; 732 | } 733 | 734 | // 尝试所有可能的来源获取价格数据 735 | let priceFound = false; 736 | 737 | // 1. 首先尝试使用已有的价格信息 738 | if (this.currentPriceInfo && this.currentPriceInfo.price) { 739 | const currentPrice = this.currentPriceInfo.price; 740 | priceInfo = `${currentPrice.toFixed(1)} USDC`; 741 | 742 | // 如果有价格数据来源,显示来源 743 | if (this.currentPriceInfo.source) { 744 | priceInfo += ` (来源: ${this.currentPriceInfo.source})`; 745 | } 746 | 747 | priceFound = true; 748 | } 749 | // 2. 如果没有价格信息,尝试从PriceMonitor获取 750 | else if (this.priceMonitor && this.priceMonitor.currentPrice > 0) { 751 | const currentPrice = this.priceMonitor.currentPrice; 752 | priceInfo = `${currentPrice.toFixed(1)} USDC (来源: 监控模块)`; 753 | 754 | // 更新到应用状态 755 | this.currentPriceInfo = { 756 | price: currentPrice, 757 | source: '监控模块', 758 | updateTime: this.priceMonitor.lastUpdateTime || Date.now() 759 | }; 760 | 761 | priceFound = true; 762 | } 763 | // 3. 如果仍然没有价格,尝试从WebSocketManager直接获取 764 | else if (this.priceMonitor && this.priceMonitor.wsManager && 765 | this.priceMonitor.wsManager.lastPriceData && 766 | this.priceMonitor.wsManager.lastPriceData.price > 0) { 767 | 768 | const wsPrice = this.priceMonitor.wsManager.lastPriceData; 769 | const currentPrice = wsPrice.price; 770 | priceInfo = `${currentPrice.toFixed(1)} USDC (来源: WebSocket直接获取)`; 771 | 772 | // 更新到应用状态 773 | this.currentPriceInfo = { 774 | price: currentPrice, 775 | source: 'WebSocket直接获取', 776 | updateTime: wsPrice.time || Date.now() 777 | }; 778 | 779 | priceFound = true; 780 | } 781 | // 4. 尝试从API获取最新价格 782 | else if (!priceFound) { 783 | try { 784 | this.backpackService.getTicker(this.symbol) 785 | .then(ticker => { 786 | if (ticker && ticker.lastPrice) { 787 | const apiPrice = parseFloat(ticker.lastPrice); 788 | // 只更新状态,不直接影响当前显示 789 | this.currentPriceInfo = { 790 | price: apiPrice, 791 | source: 'API请求', 792 | updateTime: Date.now() 793 | }; 794 | 795 | // 在下一次调用displayAccountInfo时会使用这个价格 796 | log(`从API获取到价格: ${apiPrice} USDC`); 797 | } 798 | }) 799 | .catch(error => { 800 | log(`API获取价格失败: ${error.message}`); 801 | }); 802 | } catch (apiError) { 803 | // 如果API请求失败,静默处理 804 | } 805 | } 806 | 807 | // 如果找到了价格数据并且有成交均价,计算涨跌幅和进度 808 | if (priceFound && this.tradeStats.averagePrice > 0) { 809 | const currentPrice = this.currentPriceInfo.price; 810 | // 计算涨跌幅 811 | const priceChange = ((currentPrice - this.tradeStats.averagePrice) / this.tradeStats.averagePrice) * 100; 812 | this.currentPriceInfo.increase = priceChange; 813 | 814 | const absChange = Math.abs(priceChange).toFixed(2); 815 | priceChangeSymbol = priceChange >= 0 ? "↑" : "↓"; 816 | 817 | // 计算离止盈目标的进度百分比 818 | if (priceChange > 0 && takeProfitPercentage > 0) { 819 | percentProgress = this.tradingStrategy.calculateProgressPercentage( 820 | currentPrice, 821 | this.tradeStats.averagePrice, 822 | takeProfitPercentage 823 | ).toFixed(0); 824 | } 825 | } 826 | 827 | // 计算盈亏情况 828 | let currentValue = 0; 829 | let profit = 0; 830 | let profitPercent = 0; 831 | 832 | if (this.tradeStats.filledOrders > 0 && this.currentPriceInfo && this.currentPriceInfo.price && this.tradeStats.totalFilledQuantity > 0) { 833 | currentValue = this.currentPriceInfo.price * this.tradeStats.totalFilledQuantity; 834 | profit = currentValue - this.tradeStats.totalFilledAmount; 835 | profitPercent = profit / this.tradeStats.totalFilledAmount * 100; 836 | } 837 | 838 | // 格式化并显示 839 | const data = { 840 | timeNow, 841 | symbol: this.symbol, 842 | scriptStartTime: this.scriptStartTime.toLocaleString(), 843 | elapsedTime, 844 | wsStatusInfo, 845 | priceInfo, 846 | priceChangeSymbol, 847 | increase: this.currentPriceInfo?.increase || 0, 848 | takeProfitPercentage, 849 | percentProgress, 850 | stats: this.tradeStats, 851 | tradingCoin: this.tradingCoin, 852 | currentValue, 853 | profit, 854 | profitPercent, 855 | priceSource: this.currentPriceInfo?.source 856 | }; 857 | 858 | // 格式化并显示 859 | const display = Formatter.formatAccountInfo(data); 860 | console.clear(); 861 | console.log(display); 862 | 863 | this.displayInitialized = true; 864 | } catch (error) { 865 | // 如果显示过程出错,回退到简单显示 866 | log(`显示信息时发生错误: ${error.message}`); 867 | // 简单显示函数 868 | console.log(`\n价格: ${this.currentPriceInfo?.price || '未知'} USDC`); 869 | console.log(`订单: ${this.tradeStats.filledOrders}/${this.tradeStats.totalOrders}`); 870 | console.log(`错误: ${error.message}`); 871 | } 872 | } 873 | 874 | /** 875 | * 显示统计信息 876 | */ 877 | displayStats() { 878 | const stats = this.tradeStats; 879 | 880 | log('\n=== 订单统计信息 ==='); 881 | log(`总挂单次数: ${stats.totalOrders}`); 882 | log(`已成交订单: ${stats.filledOrders}`); 883 | log(`总成交金额: ${stats.totalFilledAmount.toFixed(2)} USDC`); 884 | log(`总成交数量: ${stats.totalFilledQuantity.toFixed(6)}`); 885 | log(`平均成交价格: ${stats.averagePrice.toFixed(2)} USDC`); 886 | 887 | // 计算并显示盈亏情况 888 | if (stats.filledOrders > 0 && this.currentPriceInfo && this.currentPriceInfo.price && stats.totalFilledQuantity > 0) { 889 | const currentValue = this.currentPriceInfo.price * stats.totalFilledQuantity; 890 | const cost = stats.totalFilledAmount; 891 | const profit = currentValue - cost; 892 | const profitPercent = (profit / cost * 100); 893 | 894 | // 添加颜色指示 895 | const profitSymbol = profit >= 0 ? '+' : '-'; 896 | log(`当前持仓价值: ${currentValue.toFixed(2)} USDC`); 897 | log(`当前市场价格: ${this.currentPriceInfo.price.toFixed(2)} USDC (${profitSymbol}${Math.abs(this.currentPriceInfo.increase).toFixed(2)}%)`); 898 | log(`盈亏金额: ${profitSymbol}${Math.abs(profit).toFixed(2)} USDC`); 899 | log(`盈亏百分比: ${profitSymbol}${Math.abs(profitPercent).toFixed(2)}%`); 900 | } 901 | 902 | log(`最后更新时间: ${stats.lastUpdateTime ? stats.lastUpdateTime.toLocaleString() : '无'}`); 903 | log('==================\n'); 904 | } 905 | 906 | /** 907 | * 重置应用状态 908 | */ 909 | resetAppState() { 910 | // 重置全局配置的一些状态 911 | this.scriptStartTime = new Date(); 912 | this.tradeStats.reset(); 913 | this.orderManager.reset(); 914 | 915 | // 重置监控状态 916 | this.takeProfitTriggered = false; 917 | this.currentPriceInfo = null; 918 | this.displayInitialized = false; 919 | this.cycleLogFile = this.logger.createCycleLogFile(); 920 | 921 | log('已完全重置所有订单记录和统计数据'); 922 | } 923 | 924 | /** 925 | * 是否达到止盈条件 926 | */ 927 | isTakeProfitTriggered() { 928 | return this.takeProfitTriggered; 929 | } 930 | 931 | /** 932 | * 检查是否需要重启 933 | */ 934 | isRestartNeeded() { 935 | return this.needRestart; 936 | } 937 | 938 | /** 939 | * 检查应用是否正在运行 940 | */ 941 | isRunning() { 942 | return this.running; 943 | } 944 | } 945 | 946 | module.exports = TradingApp; --------------------------------------------------------------------------------