|
|
|
@ -1,10 +1,117 @@
|
|
|
|
// TQSDK数据源实现
|
|
|
|
// TQAPI数据源实现 - 使用Python服务
|
|
|
|
import { DataSource } from './DataSource';
|
|
|
|
import { DataSource } from './DataSource';
|
|
|
|
import TQSDK from 'tqsdk';
|
|
|
|
|
|
|
|
import WebSocket from 'ws';
|
|
|
|
// 简化的HTTP客户端
|
|
|
|
|
|
|
|
class HttpClient {
|
|
|
|
|
|
|
|
private baseUrl: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(baseUrl: string) {
|
|
|
|
|
|
|
|
this.baseUrl = baseUrl;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async get<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
|
|
|
|
|
|
|
|
let url = `${this.baseUrl}${endpoint}`;
|
|
|
|
|
|
|
|
if (params) {
|
|
|
|
|
|
|
|
const queryString = new URLSearchParams(params).toString();
|
|
|
|
|
|
|
|
url += `?${queryString}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('发送GET请求:', url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 设置20秒超时
|
|
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
|
|
|
|
console.error('GET请求超时,正在中止请求...');
|
|
|
|
|
|
|
|
controller.abort();
|
|
|
|
|
|
|
|
}, 20000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
console.log('正在发送GET请求...');
|
|
|
|
|
|
|
|
const start = Date.now();
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
signal: controller.signal
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const end = Date.now();
|
|
|
|
|
|
|
|
console.log(`GET请求完成,耗时: ${end - start}ms`);
|
|
|
|
|
|
|
|
console.log('GET请求响应状态:', response.status);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
|
|
|
console.error('GET请求失败:', errorText);
|
|
|
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('正在解析GET响应数据...');
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
console.log('GET请求响应数据:', data);
|
|
|
|
|
|
|
|
return data as T;
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
|
|
console.error('GET请求网络错误:', error.message || error);
|
|
|
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
|
|
|
console.error('错误堆栈:', error.stack);
|
|
|
|
|
|
|
|
throw new Error(`网络请求失败: ${error.message || error}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async post<T>(endpoint: string, data?: any): Promise<T> {
|
|
|
|
|
|
|
|
const url = `${this.baseUrl}${endpoint}`;
|
|
|
|
|
|
|
|
console.log('发送POST请求:', url, '数据:', data);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 设置20秒超时
|
|
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
|
|
|
|
console.error('POST请求超时,正在中止请求...');
|
|
|
|
|
|
|
|
controller.abort();
|
|
|
|
|
|
|
|
}, 20000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
console.log('正在发送请求...');
|
|
|
|
|
|
|
|
const start = Date.now();
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
|
|
|
|
|
|
signal: controller.signal
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const end = Date.now();
|
|
|
|
|
|
|
|
console.log(`请求完成,耗时: ${end - start}ms`);
|
|
|
|
|
|
|
|
console.log('POST请求响应状态:', response.status);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
|
|
|
console.error('POST请求失败:', errorText);
|
|
|
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('正在解析响应数据...');
|
|
|
|
|
|
|
|
const responseData = await response.json();
|
|
|
|
|
|
|
|
console.log('POST请求响应数据:', responseData);
|
|
|
|
|
|
|
|
return responseData as T;
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
|
|
console.error('POST请求网络错误:', error.message || error);
|
|
|
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
|
|
|
console.error('错误堆栈:', error.stack);
|
|
|
|
|
|
|
|
throw new Error(`网络请求失败: ${error.message || error}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class TQDataSource implements DataSource {
|
|
|
|
export class TQDataSource implements DataSource {
|
|
|
|
private tq: any = null;
|
|
|
|
private httpClient: HttpClient;
|
|
|
|
private initialized: boolean = false;
|
|
|
|
private initialized: boolean = false;
|
|
|
|
private config: {
|
|
|
|
private config: {
|
|
|
|
username?: string;
|
|
|
|
username?: string;
|
|
|
|
@ -12,113 +119,110 @@ export class TQDataSource implements DataSource {
|
|
|
|
timeout?: number;
|
|
|
|
timeout?: number;
|
|
|
|
retries?: number;
|
|
|
|
retries?: number;
|
|
|
|
maxConnections?: number;
|
|
|
|
maxConnections?: number;
|
|
|
|
|
|
|
|
pythonServiceUrl?: string;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
constructor(config: any = {}) {
|
|
|
|
constructor(config: any = {}) {
|
|
|
|
console.log('使用TQSDK数据源初始化...');
|
|
|
|
console.log('使用TQAPI数据源初始化...');
|
|
|
|
|
|
|
|
// 从配置中读取端口,默认8001
|
|
|
|
|
|
|
|
const port = config.pythonPort || 8001;
|
|
|
|
this.config = {
|
|
|
|
this.config = {
|
|
|
|
username: config.username || '',
|
|
|
|
username: config.username || '',
|
|
|
|
password: config.password || '',
|
|
|
|
password: config.password || '',
|
|
|
|
timeout: config.timeout || 30000,
|
|
|
|
timeout: config.timeout || 30000,
|
|
|
|
retries: config.retries || 3,
|
|
|
|
retries: config.retries || 3,
|
|
|
|
maxConnections: config.maxConnections || 5
|
|
|
|
maxConnections: config.maxConnections || 5,
|
|
|
|
|
|
|
|
pythonServiceUrl: config.pythonServiceUrl || `http://127.0.0.1:${port}/api`
|
|
|
|
};
|
|
|
|
};
|
|
|
|
console.log('TQSDK数据源配置:', this.config);
|
|
|
|
console.log('TQAPI数据源配置:', this.config);
|
|
|
|
|
|
|
|
// 测试Python服务URL是否正确
|
|
|
|
|
|
|
|
console.log('测试Python服务URL:', this.config.pythonServiceUrl);
|
|
|
|
|
|
|
|
this.httpClient = new HttpClient(this.config.pythonServiceUrl!);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async initialize(): Promise<boolean> {
|
|
|
|
async initialize(): Promise<boolean> {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
console.log('开始初始化TQSDK数据源...');
|
|
|
|
console.log('开始初始化TQAPI数据源...');
|
|
|
|
|
|
|
|
console.log('Python服务URL:', this.config.pythonServiceUrl);
|
|
|
|
// 创建TQSDK实例,使用配置的参数
|
|
|
|
console.log('连接参数:', {
|
|
|
|
console.log('创建TQSDK实例...');
|
|
|
|
username: this.config.username ? '***' : '',
|
|
|
|
|
|
|
|
password: this.config.password ? '***' : ''
|
|
|
|
// 初始化TQSDK,传入WebSocket对象
|
|
|
|
});
|
|
|
|
this.tq = new TQSDK({ autoInit: true }, { WebSocket });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('TQSDK实例创建成功,等待就绪...');
|
|
|
|
// 先测试Python服务是否可达
|
|
|
|
|
|
|
|
console.log('测试Python服务是否可达...');
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const testUrl = `${this.config.pythonServiceUrl}/../health`;
|
|
|
|
|
|
|
|
console.log('测试URL:', testUrl);
|
|
|
|
|
|
|
|
// 使用AbortController设置超时
|
|
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
|
|
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const testResponse = await fetch(testUrl, {
|
|
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
|
|
signal: controller.signal
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('Python服务健康检查响应:', testResponse.status);
|
|
|
|
|
|
|
|
if (testResponse.ok) {
|
|
|
|
|
|
|
|
console.log('Python服务可达');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
console.error('Python服务不可达,状态码:', testResponse.status);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
|
|
console.error('Python服务健康检查失败:', error.message || error);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 等待初始化完成,设置超时
|
|
|
|
// 连接到天勤服务器
|
|
|
|
const timeout = this.config.timeout || 10000;
|
|
|
|
console.log('连接到天勤服务器...');
|
|
|
|
console.log('等待TQSDK连接,超时时间:', timeout, 'ms');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
try {
|
|
|
|
// 设置超时
|
|
|
|
const response = await this.httpClient.post<any>('/connect', {
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
username: this.config.username,
|
|
|
|
console.error('TQSDK连接超时');
|
|
|
|
password: this.config.password
|
|
|
|
reject(new Error('TQSDK连接超时'));
|
|
|
|
|
|
|
|
}, timeout);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 监听就绪事件
|
|
|
|
|
|
|
|
this.tq?.on('ready', () => {
|
|
|
|
|
|
|
|
console.log('TQSDK就绪');
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
resolve(true);
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听错误事件
|
|
|
|
console.log('连接响应:', response);
|
|
|
|
this.tq?.on('error', (error: any) => {
|
|
|
|
|
|
|
|
console.error('TQSDK错误:', error);
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
reject(error);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
|
|
console.log('TQAPI连接成功');
|
|
|
|
this.initialized = true;
|
|
|
|
this.initialized = true;
|
|
|
|
console.log('TQSDK数据源初始化成功');
|
|
|
|
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('TQSDK数据源初始化失败:', error);
|
|
|
|
|
|
|
|
// 清理资源
|
|
|
|
|
|
|
|
if (this.tq) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (typeof this.tq.close === 'function') {
|
|
|
|
|
|
|
|
this.tq.close();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
console.log('TqApi实例没有close方法');
|
|
|
|
console.error('TQAPI连接失败:', response.message);
|
|
|
|
}
|
|
|
|
this.initialized = false;
|
|
|
|
} catch (closeError) {
|
|
|
|
return false;
|
|
|
|
console.error('关闭TQSDK连接失败:', closeError);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.tq = null;
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
|
|
console.error('连接请求失败:', error.message || error);
|
|
|
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('TQAPI数据源初始化失败:', error);
|
|
|
|
this.initialized = false;
|
|
|
|
this.initialized = false;
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getContractList(): Promise<any[]> {
|
|
|
|
async getContractList(): Promise<any[]> {
|
|
|
|
if (!this.initialized || !this.tq) {
|
|
|
|
if (!this.initialized) {
|
|
|
|
throw new Error('TQSDK数据源未初始化');
|
|
|
|
throw new Error('TQAPI数据源未初始化');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// 使用TQSDK的getQuotesByInput方法获取合约列表
|
|
|
|
console.log('获取合约列表...');
|
|
|
|
// 这里使用空字符串搜索,获取所有合约
|
|
|
|
const response = await this.httpClient.get<any>('/contracts');
|
|
|
|
const contracts = this.tq.getQuotesByInput('', {
|
|
|
|
|
|
|
|
future: true,
|
|
|
|
|
|
|
|
future_index: true,
|
|
|
|
|
|
|
|
future_cont: true,
|
|
|
|
|
|
|
|
option: false,
|
|
|
|
|
|
|
|
combine: false
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 转换合约格式
|
|
|
|
if (response.success && Array.isArray(response.data)) {
|
|
|
|
const futuresContracts = contracts.map((symbol: string) => {
|
|
|
|
console.log('获取合约列表成功,数量:', response.data.length);
|
|
|
|
try {
|
|
|
|
return response.data;
|
|
|
|
const quote = this.tq.getQuote(symbol);
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
console.error('获取合约列表失败:', response.message);
|
|
|
|
symbol: symbol,
|
|
|
|
return [];
|
|
|
|
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) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error('获取合约列表失败:', error);
|
|
|
|
console.error('获取合约列表失败:', error);
|
|
|
|
throw error;
|
|
|
|
throw error;
|
|
|
|
@ -126,23 +230,21 @@ export class TQDataSource implements DataSource {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getContractDetail(symbol: string): Promise<any> {
|
|
|
|
async getContractDetail(symbol: string): Promise<any> {
|
|
|
|
if (!this.initialized || !this.tq) {
|
|
|
|
if (!this.initialized) {
|
|
|
|
throw new Error('TQSDK数据源未初始化');
|
|
|
|
throw new Error('TQAPI数据源未初始化');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// 获取合约详情
|
|
|
|
console.log(`获取合约${symbol}详情...`);
|
|
|
|
const quote = this.tq.getQuote(symbol);
|
|
|
|
const response = await this.httpClient.get<any>(`/contract/${symbol}`);
|
|
|
|
return {
|
|
|
|
|
|
|
|
symbol: symbol,
|
|
|
|
if (response.success) {
|
|
|
|
name: quote.instrument_name,
|
|
|
|
console.log(`获取合约${symbol}详情成功`);
|
|
|
|
exchange: symbol.split('.')[0],
|
|
|
|
return response.data;
|
|
|
|
product_id: quote.product_id,
|
|
|
|
} else {
|
|
|
|
price_tick: quote.price_tick,
|
|
|
|
console.error(`获取合约${symbol}详情失败:`, response.message);
|
|
|
|
volume_multiple: quote.volume_multiple,
|
|
|
|
throw new Error(`获取合约${symbol}详情失败`);
|
|
|
|
margin_rate: quote.margin_rate,
|
|
|
|
}
|
|
|
|
expire_datetime: quote.expire_datetime
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`获取合约${symbol}详情失败:`, error);
|
|
|
|
console.error(`获取合约${symbol}详情失败:`, error);
|
|
|
|
throw error;
|
|
|
|
throw error;
|
|
|
|
@ -150,16 +252,38 @@ export class TQDataSource implements DataSource {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getKlineData(symbol: string, period: string, count: number): Promise<any[]> {
|
|
|
|
async getKlineData(symbol: string, period: string, count: number): Promise<any[]> {
|
|
|
|
if (!this.initialized || !this.tq) {
|
|
|
|
if (!this.initialized) {
|
|
|
|
throw new Error('TQSDK数据源未初始化');
|
|
|
|
throw new Error('TQAPI数据源未初始化');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// 转换周期格式
|
|
|
|
console.log('开始获取K线数据:', {
|
|
|
|
const duration = this.convertPeriodToDuration(period);
|
|
|
|
symbol: symbol,
|
|
|
|
// 获取K线数据
|
|
|
|
period: period,
|
|
|
|
const klines = this.tq.getKlines(symbol, duration);
|
|
|
|
count: count
|
|
|
|
return klines.data || [];
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 确保合约代码格式正确
|
|
|
|
|
|
|
|
let contractSymbol = symbol;
|
|
|
|
|
|
|
|
if (!contractSymbol.includes('.')) {
|
|
|
|
|
|
|
|
// 如果没有交易所前缀,尝试添加默认交易所
|
|
|
|
|
|
|
|
console.warn('合约代码缺少交易所前缀,尝试添加默认交易所');
|
|
|
|
|
|
|
|
contractSymbol = `SHFE.${contractSymbol}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('使用的合约代码:', contractSymbol);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const response = await this.httpClient.get<any>('/klines/' + contractSymbol, {
|
|
|
|
|
|
|
|
period: period,
|
|
|
|
|
|
|
|
count: count.toString()
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (response.success && Array.isArray(response.data)) {
|
|
|
|
|
|
|
|
console.log('获取K线数据成功,长度:', response.data.length);
|
|
|
|
|
|
|
|
return response.data;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
console.error('获取K线数据失败:', response.message);
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`获取合约${symbol}K线数据失败:`, error);
|
|
|
|
console.error(`获取合约${symbol}K线数据失败:`, error);
|
|
|
|
throw error;
|
|
|
|
throw error;
|
|
|
|
@ -167,28 +291,40 @@ export class TQDataSource implements DataSource {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getTickData(symbol: string): Promise<any> {
|
|
|
|
async getTickData(symbol: string): Promise<any> {
|
|
|
|
if (!this.initialized || !this.tq) {
|
|
|
|
if (!this.initialized) {
|
|
|
|
throw new Error('TQSDK数据源未初始化');
|
|
|
|
throw new Error('TQAPI数据源未初始化');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// 获取实时行情数据
|
|
|
|
console.log(`获取合约${symbol}实时行情数据...`);
|
|
|
|
const quote = this.tq.getQuote(symbol);
|
|
|
|
|
|
|
|
return {
|
|
|
|
// 确保合约代码格式正确
|
|
|
|
last_price: quote.last_price,
|
|
|
|
let contractSymbol = symbol;
|
|
|
|
price_change: quote.last_price - quote.pre_settlement,
|
|
|
|
if (!contractSymbol.includes('.')) {
|
|
|
|
pre_close: quote.pre_close,
|
|
|
|
// 如果没有交易所前缀,尝试添加默认交易所
|
|
|
|
open: quote.open,
|
|
|
|
console.warn('合约代码缺少交易所前缀,尝试添加默认交易所');
|
|
|
|
high: quote.high,
|
|
|
|
contractSymbol = `SHFE.${contractSymbol}`;
|
|
|
|
low: quote.low,
|
|
|
|
}
|
|
|
|
volume: quote.volume,
|
|
|
|
console.log('使用的合约代码:', contractSymbol);
|
|
|
|
open_interest: quote.open_interest,
|
|
|
|
|
|
|
|
bid_price1: quote.bid_price1,
|
|
|
|
console.log('正在发送GET请求到:', `/tick/${contractSymbol}`);
|
|
|
|
bid_volume1: quote.bid_volume1,
|
|
|
|
const start = Date.now();
|
|
|
|
ask_price1: quote.ask_price1,
|
|
|
|
const response = await this.httpClient.get<any>(`/tick/${contractSymbol}`);
|
|
|
|
ask_volume1: quote.ask_volume1,
|
|
|
|
const end = Date.now();
|
|
|
|
datetime: quote.datetime
|
|
|
|
console.log(`GET请求完成,耗时: ${end - start}ms`);
|
|
|
|
};
|
|
|
|
console.log('响应数据:', response);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
|
|
console.log(`获取合约${symbol}实时行情数据成功`);
|
|
|
|
|
|
|
|
// 计算价格变化
|
|
|
|
|
|
|
|
const tickData = response.data;
|
|
|
|
|
|
|
|
tickData.price_change = tickData.last_price - (tickData.pre_close || 0);
|
|
|
|
|
|
|
|
console.log('计算价格变化后的数据:', tickData);
|
|
|
|
|
|
|
|
return tickData;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
console.error(`获取合约${symbol}实时行情数据失败:`, response.message);
|
|
|
|
|
|
|
|
throw new Error(`获取合约${symbol}实时行情数据失败`);
|
|
|
|
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`获取合约${symbol}实时行情数据失败:`, error);
|
|
|
|
console.error(`获取合约${symbol}实时行情数据失败:`, error);
|
|
|
|
throw error;
|
|
|
|
throw error;
|
|
|
|
@ -196,8 +332,8 @@ export class TQDataSource implements DataSource {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getMarketOverview(): Promise<any[]> {
|
|
|
|
async getMarketOverview(): Promise<any[]> {
|
|
|
|
if (!this.initialized || !this.tq) {
|
|
|
|
if (!this.initialized) {
|
|
|
|
throw new Error('TQSDK数据源未初始化');
|
|
|
|
throw new Error('TQAPI数据源未初始化');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
@ -210,14 +346,21 @@ export class TQDataSource implements DataSource {
|
|
|
|
for (const contract of limitedContracts) {
|
|
|
|
for (const contract of limitedContracts) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const tick = await this.getTickData(contract.symbol);
|
|
|
|
const tick = await this.getTickData(contract.symbol);
|
|
|
|
|
|
|
|
// 确保所有值都是有效的数字
|
|
|
|
|
|
|
|
const price = typeof tick.last_price === 'number' ? tick.last_price : 0;
|
|
|
|
|
|
|
|
const change = typeof tick.price_change === 'number' ? tick.price_change : 0;
|
|
|
|
|
|
|
|
const pre_close = typeof tick.pre_close === 'number' && tick.pre_close !== 0 ? tick.pre_close : 1;
|
|
|
|
|
|
|
|
const volume = typeof tick.volume === 'number' ? tick.volume : 0;
|
|
|
|
|
|
|
|
const open_interest = typeof tick.open_interest === 'number' ? tick.open_interest : 0;
|
|
|
|
|
|
|
|
|
|
|
|
overview.push({
|
|
|
|
overview.push({
|
|
|
|
symbol: contract.symbol,
|
|
|
|
symbol: contract.symbol,
|
|
|
|
name: contract.name,
|
|
|
|
name: contract.name,
|
|
|
|
price: tick.last_price,
|
|
|
|
price: price,
|
|
|
|
change: tick.price_change,
|
|
|
|
change: change,
|
|
|
|
change_percent: tick.price_change / tick.pre_close * 100,
|
|
|
|
change_percent: change / pre_close * 100,
|
|
|
|
volume: tick.volume,
|
|
|
|
volume: volume,
|
|
|
|
open_interest: tick.open_interest
|
|
|
|
open_interest: open_interest
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`获取合约${contract.symbol}行情失败:`, error);
|
|
|
|
console.error(`获取合约${contract.symbol}行情失败:`, error);
|
|
|
|
@ -231,14 +374,13 @@ export class TQDataSource implements DataSource {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getHistoricalTrades(symbol: string, start: number, end: number): Promise<any[]> {
|
|
|
|
async getHistoricalTrades(symbol: string, start: number, end: number): Promise<any[]> {
|
|
|
|
if (!this.initialized || !this.tq) {
|
|
|
|
if (!this.initialized) {
|
|
|
|
throw new Error('TQSDK数据源未初始化');
|
|
|
|
throw new Error('TQAPI数据源未初始化');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// TQSDK Node.js版本可能不支持直接获取历史成交数据
|
|
|
|
// 目前Python服务未实现此功能
|
|
|
|
// 这里返回空数组,实际使用时需要根据TQSDK文档调整
|
|
|
|
console.warn('TQAPI服务暂未实现历史成交数据获取功能');
|
|
|
|
console.warn('TQSDK Node.js版本可能不支持直接获取历史成交数据');
|
|
|
|
|
|
|
|
return [];
|
|
|
|
return [];
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`获取合约${symbol}历史成交数据失败:`, error);
|
|
|
|
console.error(`获取合约${symbol}历史成交数据失败:`, error);
|
|
|
|
@ -247,45 +389,20 @@ export class TQDataSource implements DataSource {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async close(): Promise<void> {
|
|
|
|
async close(): Promise<void> {
|
|
|
|
if (this.tq) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// TQSDK Node.js版本可能没有close方法
|
|
|
|
console.log('关闭TQAPI连接...');
|
|
|
|
// 这里尝试关闭websocket连接
|
|
|
|
const response = await this.httpClient.post<any>('/disconnect');
|
|
|
|
if (this.tq.quotesWs && typeof this.tq.quotesWs.close === 'function') {
|
|
|
|
|
|
|
|
this.tq.quotesWs.close();
|
|
|
|
if (response.success) {
|
|
|
|
console.log('TQSDK行情WebSocket连接已关闭');
|
|
|
|
console.log('TQAPI连接已关闭');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
console.error('关闭TQAPI连接失败:', response.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
console.log('TQSDK连接已关闭');
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error('关闭TQSDK连接失败:', error);
|
|
|
|
console.error('关闭TQAPI连接失败:', error);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
this.tq = null;
|
|
|
|
|
|
|
|
this.initialized = false;
|
|
|
|
this.initialized = false;
|
|
|
|
console.log('TQSDK数据源已关闭');
|
|
|
|
console.log('TQAPI数据源已关闭');
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 转换周期格式为TQSDK所需的纳秒格式
|
|
|
|
|
|
|
|
private convertPeriodToDuration(period: string): number {
|
|
|
|
|
|
|
|
switch (period) {
|
|
|
|
|
|
|
|
case '1M':
|
|
|
|
|
|
|
|
return 60 * 1e9; // 1分钟
|
|
|
|
|
|
|
|
case '5M':
|
|
|
|
|
|
|
|
return 5 * 60 * 1e9; // 5分钟
|
|
|
|
|
|
|
|
case '15M':
|
|
|
|
|
|
|
|
return 15 * 60 * 1e9; // 15分钟
|
|
|
|
|
|
|
|
case '30M':
|
|
|
|
|
|
|
|
return 30 * 60 * 1e9; // 30分钟
|
|
|
|
|
|
|
|
case '1H':
|
|
|
|
|
|
|
|
return 60 * 60 * 1e9; // 1小时
|
|
|
|
|
|
|
|
case '4H':
|
|
|
|
|
|
|
|
return 4 * 60 * 60 * 1e9; // 4小时
|
|
|
|
|
|
|
|
case '1D':
|
|
|
|
|
|
|
|
return 24 * 60 * 60 * 1e9; // 1天
|
|
|
|
|
|
|
|
case '1W':
|
|
|
|
|
|
|
|
return 7 * 24 * 60 * 60 * 1e9; // 1周
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
return 60 * 1e9; // 默认1分钟
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|