|
|
|
|
@ -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<boolean> {
|
|
|
|
|
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<void> {
|
|
|
|
|
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分钟
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|