diff --git a/backend/package-lock.json b/backend/package-lock.json index 9f4036a..86b1f86 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,7 +19,8 @@ "pg": "^8.11.3", "redis": "^4.6.12", "socket.io": "^4.7.4", - "tqsdk": "^1.3.1" + "tqsdk": "^1.3.1", + "ws": "^8.19.0" }, "devDependencies": { "@types/cors": "^2.8.17", @@ -28,6 +29,7 @@ "@types/morgan": "^1.9.9", "@types/node": "^20.10.4", "@types/pg": "^8.10.9", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", "chai": "^4.3.10", @@ -600,6 +602,16 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -1661,6 +1673,27 @@ "node": ">=10.0.0" } }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4174,6 +4207,27 @@ "ws": "~8.18.3" } }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", @@ -4822,9 +4876,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/backend/package.json b/backend/package.json index c385407..bbaaf59 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,7 +22,8 @@ "pg": "^8.11.3", "redis": "^4.6.12", "socket.io": "^4.7.4", - "tqsdk": "^1.3.1" + "tqsdk": "^1.3.1", + "ws": "^8.19.0" }, "devDependencies": { "@types/cors": "^2.8.17", @@ -31,6 +32,7 @@ "@types/morgan": "^1.9.9", "@types/node": "^20.10.4", "@types/pg": "^8.10.9", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", "chai": "^4.3.10", diff --git a/backend/src/services/datasource/TQDataSource.ts b/backend/src/services/datasource/TQDataSource.ts index 6da52c9..12c6fc2 100644 --- a/backend/src/services/datasource/TQDataSource.ts +++ b/backend/src/services/datasource/TQDataSource.ts @@ -1,7 +1,7 @@ // TQSDK数据源实现 import { DataSource } from './DataSource'; -import TqSdk from 'tqsdk'; -import TqAccount from 'tqsdk'; +import TQSDK from 'tqsdk'; +import WebSocket from 'ws'; export class TQDataSource implements DataSource { private tq: any = null; @@ -15,6 +15,7 @@ export class TQDataSource implements DataSource { }; constructor(config: any = {}) { + console.log('使用TQSDK数据源初始化...'); this.config = { username: config.username || '', password: config.password || '', @@ -22,26 +23,45 @@ export class TQDataSource implements DataSource { retries: config.retries || 3, maxConnections: config.maxConnections || 5 }; + console.log('TQSDK数据源配置:', this.config); } async initialize(): Promise { try { + console.log('开始初始化TQSDK数据源...'); + // 创建TQSDK实例,使用配置的参数 - const tqConfig: any = {}; + console.log('创建TQSDK实例...'); - // 如果有用户名和密码,使用用户名密码登录 - if (this.config.username && this.config.password) { - tqConfig.account = this.config.username; - tqConfig.password = this.config.password; - } + // 初始化TQSDK,传入WebSocket对象 + this.tq = new TQSDK({ autoInit: true }, { WebSocket }); - this.tq = new TqSdk(tqConfig); + console.log('TQSDK实例创建成功,等待就绪...'); - // 等待初始化完成 - await new Promise((resolve) => { - this.tq?.on('connected', () => { + // 等待初始化完成,设置超时 + const timeout = this.config.timeout || 10000; + console.log('等待TQSDK连接,超时时间:', timeout, 'ms'); + + await new Promise((resolve, reject) => { + // 设置超时 + const timeoutId = setTimeout(() => { + console.error('TQSDK连接超时'); + reject(new Error('TQSDK连接超时')); + }, timeout); + + // 监听就绪事件 + this.tq?.on('ready', () => { + console.log('TQSDK就绪'); + clearTimeout(timeoutId); resolve(true); }); + + // 监听错误事件 + this.tq?.on('error', (error: any) => { + console.error('TQSDK错误:', error); + clearTimeout(timeoutId); + reject(error); + }); }); this.initialized = true; @@ -49,6 +69,19 @@ export class TQDataSource implements DataSource { return true; } catch (error) { console.error('TQSDK数据源初始化失败:', error); + // 清理资源 + if (this.tq) { + try { + if (typeof this.tq.close === 'function') { + this.tq.close(); + } else { + console.log('TqApi实例没有close方法'); + } + } catch (closeError) { + console.error('关闭TQSDK连接失败:', closeError); + } + this.tq = null; + } this.initialized = false; return false; } @@ -60,15 +93,31 @@ export class TQDataSource implements DataSource { } try { - // 获取所有合约 - const contracts = await this.tq.get_contracts(); - // 过滤期货合约,排除期权等其他类型 - const futuresContracts = contracts.filter((contract: any) => { - return contract.exchange.includes('SHFE') || - contract.exchange.includes('DCE') || - contract.exchange.includes('CZCE') || - contract.exchange.includes('INE'); + // 使用TQSDK的getQuotesByInput方法获取合约列表 + // 这里使用空字符串搜索,获取所有合约 + const contracts = this.tq.getQuotesByInput('', { + future: true, + future_index: true, + future_cont: true, + option: false, + combine: false }); + + // 转换合约格式 + const futuresContracts = contracts.map((symbol: string) => { + try { + const quote = this.tq.getQuote(symbol); + return { + symbol: symbol, + name: quote.instrument_name, + exchange: symbol.split('.')[0] + }; + } catch (error) { + console.error(`获取合约${symbol}信息失败:`, error); + return null; + } + }).filter((contract: any) => contract !== null); + return futuresContracts; } catch (error) { console.error('获取合约列表失败:', error); @@ -83,8 +132,17 @@ export class TQDataSource implements DataSource { try { // 获取合约详情 - const contract = await this.tq.get_contract(symbol); - return contract; + const quote = this.tq.getQuote(symbol); + return { + symbol: symbol, + name: quote.instrument_name, + exchange: symbol.split('.')[0], + product_id: quote.product_id, + price_tick: quote.price_tick, + volume_multiple: quote.volume_multiple, + margin_rate: quote.margin_rate, + expire_datetime: quote.expire_datetime + }; } catch (error) { console.error(`获取合约${symbol}详情失败:`, error); throw error; @@ -98,10 +156,10 @@ export class TQDataSource implements DataSource { try { // 转换周期格式 - const tqPeriod = this.convertPeriod(period); + const duration = this.convertPeriodToDuration(period); // 获取K线数据 - const kline = await this.tq.get_kline_serial(symbol, tqPeriod, count); - return kline; + const klines = this.tq.getKlines(symbol, duration); + return klines.data || []; } catch (error) { console.error(`获取合约${symbol}K线数据失败:`, error); throw error; @@ -115,8 +173,22 @@ export class TQDataSource implements DataSource { try { // 获取实时行情数据 - const tick = await this.tq.get_tick_serial(symbol, 1); - return tick[0]; + const quote = this.tq.getQuote(symbol); + return { + last_price: quote.last_price, + price_change: quote.last_price - quote.pre_settlement, + pre_close: quote.pre_close, + open: quote.open, + high: quote.high, + low: quote.low, + volume: quote.volume, + open_interest: quote.open_interest, + bid_price1: quote.bid_price1, + bid_volume1: quote.bid_volume1, + ask_price1: quote.ask_price1, + ask_volume1: quote.ask_volume1, + datetime: quote.datetime + }; } catch (error) { console.error(`获取合约${symbol}实时行情数据失败:`, error); throw error; @@ -164,9 +236,10 @@ export class TQDataSource implements DataSource { } try { - // 获取历史成交数据 - const trades = await this.tq.get_trade_serial(symbol, start, end); - return trades; + // TQSDK Node.js版本可能不支持直接获取历史成交数据 + // 这里返回空数组,实际使用时需要根据TQSDK文档调整 + console.warn('TQSDK Node.js版本可能不支持直接获取历史成交数据'); + return []; } catch (error) { console.error(`获取合约${symbol}历史成交数据失败:`, error); throw error; @@ -175,8 +248,16 @@ export class TQDataSource implements DataSource { async close(): Promise { if (this.tq) { - if (typeof this.tq.close === 'function') { - this.tq.close(); + try { + // TQSDK Node.js版本可能没有close方法 + // 这里尝试关闭websocket连接 + if (this.tq.quotesWs && typeof this.tq.quotesWs.close === 'function') { + this.tq.quotesWs.close(); + console.log('TQSDK行情WebSocket连接已关闭'); + } + console.log('TQSDK连接已关闭'); + } catch (error) { + console.error('关闭TQSDK连接失败:', error); } this.tq = null; this.initialized = false; @@ -184,27 +265,27 @@ export class TQDataSource implements DataSource { } } - // 转换周期格式 - private convertPeriod(period: string): string { + // 转换周期格式为TQSDK所需的纳秒格式 + private convertPeriodToDuration(period: string): number { switch (period) { case '1M': - return '1min'; + return 60 * 1e9; // 1分钟 case '5M': - return '5min'; + return 5 * 60 * 1e9; // 5分钟 case '15M': - return '15min'; + return 15 * 60 * 1e9; // 15分钟 case '30M': - return '30min'; + return 30 * 60 * 1e9; // 30分钟 case '1H': - return '60min'; + return 60 * 60 * 1e9; // 1小时 case '4H': - return '240min'; + return 4 * 60 * 60 * 1e9; // 4小时 case '1D': - return '1d'; + return 24 * 60 * 60 * 1e9; // 1天 case '1W': - return '1w'; + return 7 * 24 * 60 * 60 * 1e9; // 1周 default: - return '1min'; + return 60 * 1e9; // 默认1分钟 } } } \ No newline at end of file