parent
95e947b342
commit
4bafbcdd14
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +0,0 @@
|
|||||||
fastapi
|
|
||||||
uvicorn[standard]
|
|
||||||
tqsdk
|
|
||||||
python-dotenv
|
|
||||||
pydantic-settings
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
from pydantic import BaseModel, Field
|
|
||||||
from typing import List, Optional, Any
|
|
||||||
|
|
||||||
class ConnectRequest(BaseModel):
|
|
||||||
username: str = Field(..., description="天勤账号")
|
|
||||||
password: str = Field(..., description="天勤密码")
|
|
||||||
|
|
||||||
class ConnectResponse(BaseModel):
|
|
||||||
success: bool
|
|
||||||
message: str
|
|
||||||
|
|
||||||
class ContractResponse(BaseModel):
|
|
||||||
success: bool
|
|
||||||
data: List[Any] | Any
|
|
||||||
|
|
||||||
class TickResponse(BaseModel):
|
|
||||||
success: bool
|
|
||||||
data: Any
|
|
||||||
|
|
||||||
class KlineResponse(BaseModel):
|
|
||||||
success: bool
|
|
||||||
data: List[Any]
|
|
||||||
|
|
||||||
class DisconnectResponse(BaseModel):
|
|
||||||
success: bool
|
|
||||||
message: str
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
# 测试连接到天勤服务器
|
|
||||||
def test_connect():
|
|
||||||
url = "http://127.0.0.1:8001/api/connect"
|
|
||||||
data = {
|
|
||||||
"username": "windsdreamer",
|
|
||||||
"password": "1qazse42W3"
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(url, json=data, timeout=10)
|
|
||||||
print(f"Status code: {response.status_code}")
|
|
||||||
print(f"Response: {response.json()}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_connect()
|
|
||||||
Binary file not shown.
@ -1,26 +0,0 @@
|
|||||||
// 数据源抽象接口
|
|
||||||
export interface DataSource {
|
|
||||||
// 获取所有合约列表
|
|
||||||
getContractList(): Promise<any[]>;
|
|
||||||
|
|
||||||
// 获取单个合约详情
|
|
||||||
getContractDetail(symbol: string): Promise<any>;
|
|
||||||
|
|
||||||
// 获取K线数据
|
|
||||||
getKlineData(symbol: string, period: string, count: number): Promise<any[]>;
|
|
||||||
|
|
||||||
// 获取实时行情数据
|
|
||||||
getTickData(symbol: string): Promise<any>;
|
|
||||||
|
|
||||||
// 获取市场概览
|
|
||||||
getMarketOverview(): Promise<any[]>;
|
|
||||||
|
|
||||||
// 获取历史成交数据
|
|
||||||
getHistoricalTrades(symbol: string, start: number, end: number): Promise<any[]>;
|
|
||||||
|
|
||||||
// 初始化数据源
|
|
||||||
initialize(): Promise<boolean>;
|
|
||||||
|
|
||||||
// 关闭数据源连接
|
|
||||||
close(): Promise<void>;
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
// 数据源工厂类
|
|
||||||
import { DataSource } from './DataSource';
|
|
||||||
import { TQDataSource } from './TQDataSource';
|
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
|
|
||||||
// 数据源类型
|
|
||||||
export enum DataSourceType {
|
|
||||||
TQSDK = 'tqsdk',
|
|
||||||
MOCK = 'mock',
|
|
||||||
WIND = 'wind',
|
|
||||||
SINA = 'sina',
|
|
||||||
TEST = 'test'
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DataSourceFactory {
|
|
||||||
private static dataSources: Map<DataSourceType, DataSource> = new Map();
|
|
||||||
|
|
||||||
// 获取数据源实例
|
|
||||||
static async getDataSource(type: DataSourceType = DataSourceType.TQSDK, config: any = {}): Promise<DataSource> {
|
|
||||||
logger.log('获取数据源实例:', type);
|
|
||||||
if (!this.dataSources.has(type)) {
|
|
||||||
let dataSource: DataSource;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case DataSourceType.TQSDK:
|
|
||||||
dataSource = new TQDataSource(config.tqsdk || {});
|
|
||||||
break;
|
|
||||||
case DataSourceType.MOCK:
|
|
||||||
case DataSourceType.TEST:
|
|
||||||
// 导入模拟数据源
|
|
||||||
const { MockDataSource } = await import('./MockDataSource');
|
|
||||||
dataSource = new MockDataSource();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`不支持的数据源类型: ${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化数据源
|
|
||||||
const initialized = await dataSource.initialize();
|
|
||||||
if (!initialized) {
|
|
||||||
throw new Error(`数据源${type}初始化失败`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dataSources.set(type, dataSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.dataSources.get(type)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭所有数据源
|
|
||||||
static async closeAllDataSources(): Promise<void> {
|
|
||||||
for (const [type, dataSource] of this.dataSources) {
|
|
||||||
try {
|
|
||||||
await dataSource.close();
|
|
||||||
logger.log(`数据源${type}已关闭`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`关闭数据源${type}失败:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.dataSources.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换数据源
|
|
||||||
static async switchDataSource(type: DataSourceType): Promise<DataSource> {
|
|
||||||
// 关闭当前数据源
|
|
||||||
await this.closeAllDataSources();
|
|
||||||
// 获取新数据源
|
|
||||||
return this.getDataSource(type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
// 模拟数据源实现
|
|
||||||
import { DataSource } from './DataSource';
|
|
||||||
import { futuresList, generateFuturesOverview, generateFutureData, generateKlineData, riskAlerts } from '../../utils/mockData';
|
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
|
|
||||||
export class MockDataSource implements DataSource {
|
|
||||||
private initialized: boolean = false;
|
|
||||||
|
|
||||||
async initialize(): Promise<boolean> {
|
|
||||||
// 模拟初始化
|
|
||||||
this.initialized = true;
|
|
||||||
logger.log('模拟数据源初始化成功');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getContractList(): Promise<any[]> {
|
|
||||||
if (!this.initialized) {
|
|
||||||
throw new Error('模拟数据源未初始化');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回模拟合约列表
|
|
||||||
return futuresList.map(item => ({
|
|
||||||
symbol: item.code,
|
|
||||||
name: item.name,
|
|
||||||
exchange: this.getExchangeBySymbol(item.code),
|
|
||||||
product_class: 'futures',
|
|
||||||
price_tick: 0.01,
|
|
||||||
size: 1,
|
|
||||||
margin_rate: 0.05
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getContractDetail(symbol: string): Promise<any> {
|
|
||||||
if (!this.initialized) {
|
|
||||||
throw new Error('模拟数据源未初始化');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回模拟合约详情
|
|
||||||
const future = futuresList.find(item => item.code === symbol);
|
|
||||||
if (!future) {
|
|
||||||
throw new Error(`合约${symbol}不存在`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
symbol: future.code,
|
|
||||||
name: future.name,
|
|
||||||
exchange: this.getExchangeBySymbol(future.code),
|
|
||||||
product_class: 'futures',
|
|
||||||
price_tick: 0.01,
|
|
||||||
size: 1,
|
|
||||||
margin_rate: 0.05,
|
|
||||||
delivery_month: '202412',
|
|
||||||
trading_hours: '09:00-11:30, 13:30-15:00, 21:00-02:30',
|
|
||||||
pre_close: 2000,
|
|
||||||
open: 2005,
|
|
||||||
high: 2010,
|
|
||||||
low: 1995,
|
|
||||||
last_price: 2003,
|
|
||||||
volume: 10000,
|
|
||||||
open_interest: 50000
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getKlineData(symbol: string, period: string, count: number): Promise<any[]> {
|
|
||||||
if (!this.initialized) {
|
|
||||||
throw new Error('模拟数据源未初始化');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回模拟K线数据
|
|
||||||
const klineData = generateKlineData(count);
|
|
||||||
return klineData.map(item => ({
|
|
||||||
datetime: item.time * 1000000000, // 转换为纳秒
|
|
||||||
open: item.open,
|
|
||||||
high: item.high,
|
|
||||||
low: item.low,
|
|
||||||
close: item.close,
|
|
||||||
volume: item.volume,
|
|
||||||
open_interest: Math.floor(Math.random() * 100000) + 50000
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTickData(symbol: string): Promise<any> {
|
|
||||||
if (!this.initialized) {
|
|
||||||
throw new Error('模拟数据源未初始化');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回模拟实时行情数据
|
|
||||||
const future = futuresList.find(item => item.code === symbol);
|
|
||||||
if (!future) {
|
|
||||||
throw new Error(`合约${symbol}不存在`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pre_close = 2000;
|
|
||||||
const last_price = pre_close + (Math.random() * 20 - 10);
|
|
||||||
const price_change = last_price - pre_close;
|
|
||||||
|
|
||||||
return {
|
|
||||||
datetime: Date.now() * 1000000, // 转换为纳秒
|
|
||||||
last_price: +last_price.toFixed(2),
|
|
||||||
pre_close,
|
|
||||||
open: pre_close + (Math.random() * 10 - 5),
|
|
||||||
high: Math.max(last_price, pre_close + (Math.random() * 15 - 5)),
|
|
||||||
low: Math.min(last_price, pre_close + (Math.random() * 15 - 10)),
|
|
||||||
volume: Math.floor(Math.random() * 10000) + 5000,
|
|
||||||
open_interest: Math.floor(Math.random() * 100000) + 50000,
|
|
||||||
price_change: +price_change.toFixed(2),
|
|
||||||
bid_price1: last_price - 0.01,
|
|
||||||
bid_volume1: Math.floor(Math.random() * 100) + 50,
|
|
||||||
ask_price1: last_price + 0.01,
|
|
||||||
ask_volume1: Math.floor(Math.random() * 100) + 50
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMarketOverview(): Promise<any[]> {
|
|
||||||
if (!this.initialized) {
|
|
||||||
throw new Error('模拟数据源未初始化');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回模拟市场概览
|
|
||||||
const overview = generateFuturesOverview();
|
|
||||||
return overview.map(item => ({
|
|
||||||
symbol: item.code,
|
|
||||||
name: item.name,
|
|
||||||
price: item.currentPrice,
|
|
||||||
change: item.changePercent,
|
|
||||||
change_percent: item.changePercent,
|
|
||||||
volume: Math.floor(Math.random() * 100000) + 50000,
|
|
||||||
open_interest: Math.floor(Math.random() * 1000000) + 500000
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getHistoricalTrades(symbol: string, start: number, end: number): Promise<any[]> {
|
|
||||||
if (!this.initialized) {
|
|
||||||
throw new Error('模拟数据源未初始化');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回模拟历史成交数据
|
|
||||||
const trades = [];
|
|
||||||
const count = 100; // 生成100条模拟数据
|
|
||||||
const basePrice = 2000;
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const price = basePrice + (Math.random() * 10 - 5);
|
|
||||||
const volume = Math.floor(Math.random() * 100) + 10;
|
|
||||||
const timestamp = start + Math.floor((end - start) * (i / count));
|
|
||||||
|
|
||||||
trades.push({
|
|
||||||
datetime: timestamp * 1000000, // 转换为纳秒
|
|
||||||
price: +price.toFixed(2),
|
|
||||||
volume,
|
|
||||||
direction: Math.random() > 0.5 ? 'BUY' : 'SELL'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return trades;
|
|
||||||
}
|
|
||||||
|
|
||||||
async close(): Promise<void> {
|
|
||||||
// 模拟关闭
|
|
||||||
this.initialized = false;
|
|
||||||
logger.log('模拟数据源已关闭');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据合约代码获取交易所
|
|
||||||
private getExchangeBySymbol(symbol: string): string {
|
|
||||||
// 简单的交易所映射
|
|
||||||
const exchangeMap: Record<string, string> = {
|
|
||||||
'AU': 'SHFE',
|
|
||||||
'AG': 'SHFE',
|
|
||||||
'CU': 'SHFE',
|
|
||||||
'NI': 'SHFE',
|
|
||||||
'SN': 'SHFE',
|
|
||||||
'AL': 'SHFE',
|
|
||||||
'ZN': 'SHFE',
|
|
||||||
'FG': 'CZCE',
|
|
||||||
'SJS': 'CZCE',
|
|
||||||
'SCA': 'CZCE',
|
|
||||||
'JM': 'DCE',
|
|
||||||
'RB': 'SHFE',
|
|
||||||
'ALO': 'SHFE',
|
|
||||||
'MA': 'DCE',
|
|
||||||
'PVC': 'DCE',
|
|
||||||
'FU': 'SHFE',
|
|
||||||
'SC': 'INE',
|
|
||||||
'L': 'DCE',
|
|
||||||
'NR': 'SHFE',
|
|
||||||
'BU': 'SHFE',
|
|
||||||
'LU': 'INE',
|
|
||||||
'P': 'DCE',
|
|
||||||
'LC': 'SHFE',
|
|
||||||
'SI': 'SHFE',
|
|
||||||
'PGS': 'SHFE',
|
|
||||||
'IC': 'CFFEX',
|
|
||||||
'IM': 'CFFEX',
|
|
||||||
'IH': 'CFFEX'
|
|
||||||
};
|
|
||||||
|
|
||||||
return exchangeMap[symbol] || 'SHFE';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
# TQDataSource Python 实现方案
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
将当前的 TQSDK Node.js 接口改为使用天勤接口的 Python 封装 tqapi。通过创建一个 Python 服务来处理 tqapi 的调用,并修改现有的 Node.js 代码与之通信。
|
|
||||||
|
|
||||||
## 实现方案
|
|
||||||
|
|
||||||
### 1. Python 服务
|
|
||||||
|
|
||||||
使用 FastAPI 创建一个 Python 服务,提供以下 HTTP 接口:
|
|
||||||
|
|
||||||
| 方法 | 路径 | 功能 | 请求体 | 响应体 |
|
|
||||||
|------|------|------|--------|--------|
|
|
||||||
| POST | /api/connect | 连接到天勤服务器 | `{"username": "...", "password": "..."}` | `{"success": true, "message": "连接成功"}` |
|
|
||||||
| GET | /api/contracts | 获取合约列表 | N/A | `{"success": true, "data": [{...}, {...}]}` |
|
|
||||||
| GET | /api/contract/{symbol} | 获取合约详情 | N/A | `{"success": true, "data": {...}}` |
|
|
||||||
| GET | /api/klines/{symbol} | 获取 K 线数据 | 查询参数: period, count | `{"success": true, "data": [{...}, {...}]}` |
|
|
||||||
| GET | /api/tick/{symbol} | 获取 tick 数据 | N/A | `{"success": true, "data": {...}}` |
|
|
||||||
| POST | /api/disconnect | 断开连接 | N/A | `{"success": true, "message": "断开成功"}` |
|
|
||||||
|
|
||||||
### 2. Node.js 代码修改
|
|
||||||
|
|
||||||
修改现有的 TQDataSource 实现,改为调用 Python 服务的 HTTP 接口:
|
|
||||||
|
|
||||||
- 初始化时,调用 /api/connect 接口
|
|
||||||
- 获取合约列表时,调用 /api/contracts 接口
|
|
||||||
- 获取合约详情时,调用 /api/contract/{symbol} 接口
|
|
||||||
- 获取 K 线数据时,调用 /api/klines/{symbol} 接口
|
|
||||||
- 获取 tick 数据时,调用 /api/tick/{symbol} 接口
|
|
||||||
- 关闭时,调用 /api/disconnect 接口
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- Python 3.8+
|
|
||||||
- FastAPI
|
|
||||||
- TqApi (天勤量化)
|
|
||||||
- Node.js 16+
|
|
||||||
- axios (HTTP 客户端)
|
|
||||||
|
|
||||||
## 优势
|
|
||||||
|
|
||||||
1. 使用官方推荐的 Python 封装,接口更稳定
|
|
||||||
2. 充分利用 Python 生态系统的优势
|
|
||||||
3. 与 Node.js 代码解耦,便于维护
|
|
||||||
4. 可以独立部署和扩展 Python 服务
|
|
||||||
|
|
||||||
## 实现步骤
|
|
||||||
|
|
||||||
1. 创建 Python 服务项目
|
|
||||||
2. 实现 Python 服务的核心功能
|
|
||||||
3. 测试 Python 服务的接口
|
|
||||||
4. 修改 Node.js 代码,与 Python 服务通信
|
|
||||||
5. 测试整体功能
|
|
||||||
6. 优化性能和错误处理
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 需要确保 Python 服务和 Node.js 服务在同一网络环境中
|
|
||||||
2. 需要处理好错误和异常情况
|
|
||||||
3. 需要考虑性能优化,特别是在获取大量数据时
|
|
||||||
4. 需要确保安全性,特别是在处理敏感信息时
|
|
||||||
Loading…
Reference in new issue