fix: 删除不需要的文件

master
Lxy 3 months ago
parent 95e947b342
commit 4bafbcdd14

@ -1,108 +0,0 @@
# Python TQAPI 服务
## 功能说明
Python TQAPI 服务是 AI 期货分析系统的一个组件负责与天勤量化TQSDK进行交互为系统提供期货市场数据。
## 集成说明
### 自动启动
Python TQAPI 服务已集成到 Node.js 后端服务中,当满足以下条件时会自动启动:
1. 配置文件中 `defaultDataSource` 设置为 `tqsdk`
2. 或配置文件中 `tqsdk.enabled` 设置为 `true`
### 手动启动
如果需要手动启动 Python 服务,可以执行以下命令:
```bash
cd backend/python_service
python main.py
```
## 依赖安装
### Python 依赖
`backend/python_service` 目录下执行:
```bash
pip install -r requirements.txt
```
### 依赖包说明
- **tqsdk**:天勤量化 SDK用于获取期货市场数据
- **fastapi**:高性能的 Python Web 框架,用于提供 API 接口
- **uvicorn**ASGI 服务器,用于运行 FastAPI 应用
- **pandas**:数据分析库,用于处理和分析数据
## API 端点
| 端点 | 方法 | 功能 |
|------|------|------|
| `/api/connect` | POST | 连接到天勤量化服务 |
| `/api/contracts` | GET | 获取合约列表 |
| `/api/contract/{symbol}` | GET | 获取单个合约详情 |
| `/api/klines/{symbol}` | GET | 获取K线数据 |
| `/api/tick/{symbol}` | GET | 获取Tick数据 |
| `/health` | GET | 健康检查 |
## 配置说明
### 天勤量化账号配置
`config.json` 文件中配置天勤量化账号:
```json
{
"dataSource": {
"tqsdk": {
"enabled": true,
"username": "你的天勤账号",
"password": "你的天勤密码"
}
}
}
```
### 服务端口配置
服务默认运行在端口 8000可以在 `main.py` 文件中修改:
```python
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000) # 修改端口号
```
## 故障排查
### 常见问题
1. **Python 服务启动失败**
- 检查 Python 是否安装
- 检查依赖包是否安装完整
- 检查天勤量化账号是否正确
2. **连接天勤服务器失败**
- 检查网络连接
- 检查天勤量化账号密码是否正确
- 检查天勤量化服务是否正常
3. **数据获取失败**
- 检查合约代码是否正确
- 检查网络连接
- 检查天勤量化服务状态
### 日志查看
Python 服务的日志会输出到 Node.js 后端服务的控制台中,可以通过查看控制台日志来排查问题。
## 技术实现
- **FastAPI**:提供 RESTful API 接口
- **TqApi**:与天勤量化服务交互
- **异步处理**:提高并发性能
- **错误处理**:确保服务稳定性

@ -1,96 +0,0 @@
from fastapi import APIRouter, HTTPException, Query, Request
from schemas import ConnectRequest, ConnectResponse, ContractResponse, TickResponse, KlineResponse, DisconnectResponse
from tqapi_service import TqApiService
router = APIRouter()
# 存储当前连接的用户凭证
current_credentials = {}
# 获取当前TqApiService实例
async def get_current_service():
if not current_credentials:
raise HTTPException(status_code=401, detail="未连接到天勤服务器")
# 使用当前凭证获取或创建实例
return await TqApiService.get_instance(
current_credentials['username'],
current_credentials['password']
)
@router.post("/connect", response_model=ConnectResponse)
async def connect(request: ConnectRequest):
"""连接到天勤服务器"""
try:
# 存储凭证
current_credentials['username'] = request.username
current_credentials['password'] = request.password
# 获取或创建TqApiService实例
service = await TqApiService.get_instance(request.username, request.password)
return ConnectResponse(success=True, message="连接成功")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/contracts", response_model=ContractResponse)
async def get_contracts():
"""获取合约列表"""
try:
service = await get_current_service()
contracts = await service.get_contracts()
return ContractResponse(success=True, data=contracts)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/contract/{symbol}", response_model=ContractResponse)
async def get_contract(symbol: str):
"""获取合约详情"""
try:
service = await get_current_service()
contract = await service.get_contract(symbol)
return ContractResponse(success=True, data=contract)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/klines/{symbol}", response_model=KlineResponse)
async def get_klines(
symbol: str,
period: str = Query(..., description="周期,如 1M, 5M, 1H, 1D"),
count: int = Query(30, description="数据数量")
):
"""获取 K 线数据"""
try:
service = await get_current_service()
klines = await service.get_klines(symbol, period, count)
return KlineResponse(success=True, data=klines)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/tick/{symbol}", response_model=TickResponse)
async def get_tick(symbol: str):
"""获取 tick 数据"""
try:
print(f"[Router] 接收到获取 tick 数据请求symbol: {symbol}")
print(f"[Router] 正在获取当前服务实例...")
service = await get_current_service()
print(f"[Router] 获取服务实例成功")
print(f"[Router] 正在调用 service.get_tick({symbol})...")
tick = await service.get_tick(symbol)
print(f"[Router] 获取 tick 数据成功: {tick}")
return TickResponse(success=True, data=tick)
except Exception as e:
print(f"[Router] 获取 tick 数据失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/disconnect", response_model=DisconnectResponse)
async def disconnect():
"""断开连接"""
try:
service = await get_current_service()
success = await service.disconnect()
if success:
# 清除凭证
current_credentials.clear()
return DisconnectResponse(success=True, message="断开成功")
else:
raise HTTPException(status_code=400, detail="断开失败")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@ -1,61 +0,0 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.router import router
import uvicorn
import argparse
app = FastAPI(
title="TQAPI Service",
description="天勤量化 API 服务",
version="1.0.0"
)
# 配置 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 在生产环境中应该设置具体的域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(router, prefix="/api")
# 健康检查
@app.get("/health")
async def health_check():
return {"status": "healthy"}
# 关闭事件处理
@app.on_event("shutdown")
async def shutdown_event():
"""服务关闭时的清理工作"""
print("正在关闭服务...")
# 断开TQApi连接
from api.router import current_credentials
from tqapi_service import TqApiService
if current_credentials:
try:
# 获取当前服务实例并断开连接
service = await TqApiService.get_instance(current_credentials['username'], current_credentials['password'])
success = await service.disconnect()
if success:
print("TQApi连接已成功关闭")
else:
print("TQApi连接关闭失败")
except Exception as e:
print(f"关闭TQApi连接时出错: {e}")
print("服务已关闭")
if __name__ == "__main__":
# 解析命令行参数
parser = argparse.ArgumentParser(description="天勤量化 API 服务")
parser.add_argument("--port", type=int, default=8000, help="服务端口默认8000")
parser.add_argument("--host", type=str, default="0.0.0.0", help="服务主机默认0.0.0.0")
args = parser.parse_args()
# 启动服务
print(f"启动天勤量化 API 服务,监听 {args.host}:{args.port}")
uvicorn.run(app, host=args.host, port=args.port)

@ -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()

@ -1,329 +0,0 @@
import asyncio
import datetime
from tqsdk import TqApi, TqAuth
from typing import List, Dict, Any
class TqApiService:
# 类级缓存存储用户名密码对应的TqApiService实例
_instance_cache = {}
def __init__(self):
self.api = None
self.connected = False
self.username = None
self.password = None
@classmethod
async def get_instance(cls, username: str, password: str):
"""获取或创建TqApiService实例"""
# 生成缓存键
cache_key = f"{username}:{password}"
# 检查缓存中是否存在实例
if cache_key in cls._instance_cache:
instance = cls._instance_cache[cache_key]
# 检查实例是否仍然连接
if instance.connected:
print(f"使用缓存的TqApiService实例 for {username}")
return instance
else:
# 实例已断开连接,从缓存中移除
print(f"TqApiService实例 for {username} 已断开连接,从缓存中移除")
del cls._instance_cache[cache_key]
# 创建新实例
print(f"创建新的TqApiService实例 for {username}")
instance = cls()
instance.username = username
instance.password = password
# 连接到天勤服务器
await instance.connect(username, password)
# 存入缓存
cls._instance_cache[cache_key] = instance
return instance
async def connect(self, username: str, password: str) -> bool:
"""连接到天勤服务器"""
try:
# 立即返回不等待TqApi实例完全连接到天勤服务器
# 连接过程将在后台进行
self.username = username
self.password = password
self.connected = True
# 创建后台任务来初始化API实例和运行事件循环
asyncio.create_task(self._initialize_api())
return True
except Exception as e:
print(f"连接失败: {e}")
self.connected = False
return False
async def _initialize_api(self):
"""初始化TqApi实例并运行事件循环"""
try:
print("正在初始化TqApi实例...")
# 创建TqApi实例
self.api = TqApi(auth=TqAuth(self.username, self.password), debug=False)
print("TqApi实例初始化完成")
# 运行事件循环
await self._run_api()
except Exception as e:
print(f"初始化TqApi实例失败: {e}")
self.connected = False
async def _run_api(self):
"""运行TQAPI事件循环"""
try:
while self.connected and self.api:
try:
# 非阻塞方式检查更新
self.api.wait_update(0.1) # 超时0.1秒,避免完全阻塞
except Exception as e:
print(f"API更新出错: {e}")
await asyncio.sleep(0.1)
# 让出CPU时间避免阻塞其他任务
await asyncio.sleep(0.01)
except Exception as e:
print(f"API运行出错: {e}")
self.connected = False
async def get_contracts(self) -> List[Dict[str, Any]]:
"""获取合约列表"""
if not self.connected:
raise Exception("未连接到天勤服务器")
if not self.api:
# API实例尚未初始化返回空列表
print("API实例尚未初始化返回空合约列表")
return []
try:
# 获取所有合约
contracts = self.api.get_contracts()
# 过滤期货合约,排除期权等其他类型
futures_contracts = []
for symbol, contract in contracts.items():
# 只保留期货合约
if contract["product_class"] == "FUTURE":
futures_contracts.append({
"symbol": symbol,
"name": contract["name"],
"exchange": contract["exchange"],
"product_id": contract["product_id"],
"price_tick": contract["price_tick"],
"volume_multiple": contract["volume_multiple"],
"margin_rate": contract["margin_rate"],
"expire_datetime": contract["expire_datetime"]
})
return futures_contracts
except Exception as e:
print(f"获取合约列表失败: {e}")
# 返回空列表,避免整个请求失败
return []
async def get_contract(self, symbol: str) -> Dict[str, Any]:
"""获取合约详情"""
if not self.connected:
raise Exception("未连接到天勤服务器")
if not self.api:
# API实例尚未初始化返回默认值
print("API实例尚未初始化返回默认合约详情")
return {
"symbol": symbol,
"name": "",
"exchange": "",
"product_id": "",
"price_tick": 0,
"volume_multiple": 0,
"margin_rate": 0,
"expire_datetime": 0
}
try:
# 获取合约详情
contract = self.api.get_contract(symbol)
return {
"symbol": symbol,
"name": contract["name"],
"exchange": contract["exchange"],
"product_id": contract["product_id"],
"price_tick": contract["price_tick"],
"volume_multiple": contract["volume_multiple"],
"margin_rate": contract["margin_rate"],
"expire_datetime": contract["expire_datetime"]
}
except Exception as e:
print(f"获取合约详情失败: {e}")
# 返回默认值,避免整个请求失败
return {
"symbol": symbol,
"name": "",
"exchange": "",
"product_id": "",
"price_tick": 0,
"volume_multiple": 0,
"margin_rate": 0,
"expire_datetime": 0
}
async def get_klines(self, symbol: str, period: str, count: int) -> List[Dict[str, Any]]:
"""获取K线数据"""
if not self.connected:
raise Exception("未连接到天勤服务器")
if not self.api:
# API实例尚未初始化返回空列表
print("API实例尚未初始化返回空K线数据列表")
return []
try:
# 转换周期格式
duration_seconds = self._convert_period(period)
# 获取K线数据
klines = self.api.get_kline_serial(symbol, duration_seconds, data_length=count)
# 转换为前端需要的格式
result = []
for i in range(len(klines)):
result.append({
"time": int(klines.iloc[i]["datetime"].timestamp()),
"open": float(klines.iloc[i]["open"]),
"high": float(klines.iloc[i]["high"]),
"low": float(klines.iloc[i]["low"]),
"close": float(klines.iloc[i]["close"]),
"volume": float(klines.iloc[i]["volume"])
})
return result
except Exception as e:
print(f"获取K线数据失败: {e}")
# 返回空列表,避免整个请求失败
return []
async def get_tick(self, symbol: str) -> Dict[str, Any]:
"""获取tick数据"""
print(f"[TqApiService] 开始获取 tick 数据symbol: {symbol}")
try:
print(f"[TqApiService] 检查连接状态: {self.connected}")
if not self.connected:
# 未连接到天勤服务器,返回默认数据
print("[TqApiService] 未连接到天勤服务器返回默认tick数据")
return {
"last_price": 0,
"pre_close": 0,
"open": 0,
"high": 0,
"low": 0,
"volume": 0,
"open_interest": 0,
"bid_price1": 0,
"bid_volume1": 0,
"ask_price1": 0,
"ask_volume1": 0,
"datetime": int(datetime.datetime.now().timestamp())
}
print(f"[TqApiService] 检查API实例状态: {self.api is not None}")
if not self.api:
# API实例尚未初始化返回默认数据
print("[TqApiService] API实例尚未初始化返回默认tick数据")
return {
"last_price": 0,
"pre_close": 0,
"open": 0,
"high": 0,
"low": 0,
"volume": 0,
"open_interest": 0,
"bid_price1": 0,
"bid_volume1": 0,
"ask_price1": 0,
"ask_volume1": 0,
"datetime": int(datetime.datetime.now().timestamp())
}
# 获取实时行情
print(f"[TqApiService] 正在调用 self.api.get_quote({symbol})...")
quote = self.api.get_quote(symbol)
print(f"[TqApiService] 获取 quote 对象成功")
# 尝试多次获取行情更新,避免单次超时
max_attempts = 3
print(f"[TqApiService] 开始尝试获取行情更新,最多 {max_attempts}")
for attempt in range(max_attempts):
try:
# 等待行情更新,设置超时
print(f"[TqApiService] 尝试 {attempt + 1}/{max_attempts}: 调用 self.api.wait_update(1.0)...")
updated = self.api.wait_update(1.0) # 1秒超时
print(f"[TqApiService] 尝试 {attempt + 1}/{max_attempts}: wait_update 返回: {updated}")
if updated:
print(f"[TqApiService] 获取行情更新成功")
break
print(f"[TqApiService] 尝试 {attempt + 1}/{max_attempts}: 获取 {symbol} 的行情信息超时")
except Exception as e:
print(f"[TqApiService] 尝试 {attempt + 1}/{max_attempts}: 获取 {symbol} 的行情信息出错: {e}")
await asyncio.sleep(0.5)
# 即使没有更新,也尝试返回当前数据
print(f"[TqApiService] 正在构建返回数据...")
result = {
"last_price": float(quote.get("last_price", 0)),
"pre_close": float(quote.get("pre_close", 0)),
"open": float(quote.get("open", 0)),
"high": float(quote.get("high", 0)),
"low": float(quote.get("low", 0)),
"volume": float(quote.get("volume", 0)),
"open_interest": float(quote.get("open_interest", 0)),
"bid_price1": float(quote.get("bid_price1", 0)),
"bid_volume1": float(quote.get("bid_volume1", 0)),
"ask_price1": float(quote.get("ask_price1", 0)),
"ask_volume1": float(quote.get("ask_volume1", 0)),
"datetime": int(quote.get("datetime", datetime.datetime.now()).timestamp())
}
print(f"[TqApiService] 获取 tick 数据成功,返回: {result}")
return result
except Exception as e:
print(f"[TqApiService] 获取tick数据失败: {e}")
import traceback
traceback.print_exc()
# 返回默认数据,避免整个请求失败
default_result = {
"last_price": 0,
"pre_close": 0,
"open": 0,
"high": 0,
"low": 0,
"volume": 0,
"open_interest": 0,
"bid_price1": 0,
"bid_volume1": 0,
"ask_price1": 0,
"ask_volume1": 0,
"datetime": int(datetime.datetime.now().timestamp())
}
print(f"[TqApiService] 返回默认数据: {default_result}")
return default_result
async def disconnect(self) -> bool:
"""断开连接"""
try:
if self.api:
self.connected = False
self.api.close()
self.api = None
return True
except Exception as e:
print(f"断开连接失败: {e}")
return False
def _convert_period(self, period: str) -> int:
"""转换周期格式为秒"""
period_map = {
"1M": 60,
"5M": 300,
"15M": 900,
"30M": 1800,
"1H": 3600,
"4H": 14400,
"1D": 86400
}
return period_map.get(period, 3600) # 默认1小时

@ -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,84 +0,0 @@
import { spawn } from 'child_process';
import * as path from 'path';
import { config } from '../../config';
import { logger } from '../../utils/logger';
class PythonServiceManager {
private pythonProcess: any = null;
private isRunning: boolean = false;
async start(): Promise<boolean> {
if (this.isRunning) {
logger.log('Python服务已经在运行');
return true;
}
try {
// 构建Python服务的路径
const pythonServicePath = path.join(__dirname, '../../../python_service/main.py');
// 从配置中读取端口默认3007
const port = config.dataSource?.tqsdk?.pythonPort || 3007;
logger.log(`启动Python TQAPI服务端口: ${port}...`);
// 启动Python服务传递端口参数
this.pythonProcess = spawn('python', [pythonServicePath, '--port', port.toString()], {
cwd: path.join(__dirname, '../../../python_service'),
stdio: 'inherit',
shell: true
});
this.pythonProcess.on('error', (error: any) => {
logger.error('启动Python服务失败:', error);
this.isRunning = false;
});
this.pythonProcess.on('exit', (code: number, signal: string) => {
logger.log(`Python服务退出代码: ${code}, 信号: ${signal}`);
this.isRunning = false;
});
// 等待2秒确保Python服务有时间启动
await new Promise(resolve => setTimeout(resolve, 2000));
this.isRunning = true;
logger.log('Python TQAPI服务启动成功');
return true;
} catch (error) {
logger.error('启动Python服务时出错:', error);
this.isRunning = false;
return false;
}
}
stop(): void {
if (this.pythonProcess) {
try {
logger.log('停止Python TQAPI服务...');
// 发送终止信号
this.pythonProcess.kill();
// 等待进程退出
this.pythonProcess.on('exit', (code: number, signal: string) => {
logger.log(`Python服务退出代码: ${code}, 信号: ${signal}`);
});
this.pythonProcess = null;
this.isRunning = false;
logger.log('Python TQAPI服务已停止');
} catch (error) {
logger.error('停止Python服务时出错:', error);
this.pythonProcess = null;
this.isRunning = false;
}
} else {
logger.log('Python服务未运行无需停止');
}
}
getStatus(): boolean {
return this.isRunning;
}
}
// 导出单例实例
export const pythonServiceManager = new PythonServiceManager();

@ -1,439 +0,0 @@
// TQAPI数据源实现 - 使用Python服务
import { DataSource } from './DataSource';
import { logger } from '../../utils/logger';
// 简化的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}`;
}
logger.log('发送GET请求:', url);
try {
// 设置20秒超时
const controller = new AbortController();
const timeoutId = setTimeout(() => {
logger.error('GET请求超时正在中止请求...');
controller.abort();
}, 20000);
try {
logger.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();
logger.log(`GET请求完成耗时: ${end - start}ms`);
logger.log('GET请求响应状态:', response.status);
if (!response.ok) {
const errorText = await response.text();
logger.error('GET请求失败:', errorText);
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}
logger.log('正在解析GET响应数据...');
const data = await response.json();
logger.log('GET请求响应数据:', data);
return data as T;
} finally {
clearTimeout(timeoutId);
}
} catch (error: any) {
logger.error('GET请求网络错误:', error.message || error);
logger.error('错误详情:', error);
logger.error('错误堆栈:', error.stack);
throw new Error(`网络请求失败: ${error.message || error}`);
}
}
async post<T>(endpoint: string, data?: any): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
logger.log(`发送POST请求: ${url} 数据: ${JSON.stringify(data)}`);
try {
// 设置20秒超时
const controller = new AbortController();
const timeoutId = setTimeout(() => {
logger.error('POST请求超时正在中止请求...');
controller.abort();
}, 20000);
try {
logger.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();
logger.log(`请求完成,耗时: ${end - start}ms`);
logger.log('POST请求响应状态:', response.status);
if (!response.ok) {
const errorText = await response.text();
logger.error('POST请求失败:', errorText);
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}
logger.log('正在解析响应数据...');
const responseData = await response.json();
logger.log('POST请求响应数据:', responseData);
return responseData as T;
} finally {
clearTimeout(timeoutId);
}
} catch (error: any) {
logger.error('POST请求网络错误:', error.message || error);
logger.error('错误详情:', error);
logger.error('错误堆栈:', error.stack);
throw new Error(`网络请求失败: ${error.message || error}`);
}
}
}
export class TQDataSource implements DataSource {
private httpClient: HttpClient;
private initialized: boolean = false;
private config: {
username?: string;
password?: string;
timeout?: number;
retries?: number;
maxConnections?: number;
pythonServiceUrl?: string;
};
constructor(config: any = {}) {
logger.log('使用TQAPI数据源初始化...');
// 从配置中读取端口默认8001
const port = config.pythonPort || 8001;
this.config = {
username: config.username || '',
password: config.password || '',
timeout: config.timeout || 30000,
retries: config.retries || 3,
maxConnections: config.maxConnections || 5,
pythonServiceUrl: config.pythonServiceUrl || `http://127.0.0.1:${port}/api`
};
logger.log('TQAPI数据源配置:', this.config);
// 测试Python服务URL是否正确
logger.log('测试Python服务URL:', this.config.pythonServiceUrl);
this.httpClient = new HttpClient(this.config.pythonServiceUrl!);
}
async initialize(): Promise<boolean> {
try {
logger.log('开始初始化TQAPI数据源...');
logger.log('Python服务URL:', this.config.pythonServiceUrl);
logger.log('连接参数:', {
username: this.config.username ? '***' : '',
password: this.config.password ? '***' : ''
});
// 先测试Python服务是否可达
logger.log('测试Python服务是否可达...');
try {
const testUrl = `${this.config.pythonServiceUrl}/../health`;
logger.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
});
logger.log('Python服务健康检查响应:', testResponse.status);
if (testResponse.ok) {
logger.log('Python服务可达');
} else {
logger.error('Python服务不可达状态码:', testResponse.status);
}
} finally {
clearTimeout(timeoutId);
}
} catch (error: any) {
logger.error('Python服务健康检查失败:', error.message || error);
}
// 连接到天勤服务器
logger.log('连接到天勤服务器...');
try {
const response = await this.httpClient.post<any>('/connect', {
username: this.config.username,
password: this.config.password
});
logger.log('连接响应:', response);
if (response.success) {
logger.log('TQAPI连接成功');
this.initialized = true;
return true;
} else {
logger.error('TQAPI连接失败:', response.message);
this.initialized = false;
return false;
}
} catch (error: any) {
logger.error('连接请求失败:', error.message || error);
logger.error('错误详情:', error);
throw error;
}
} catch (error) {
logger.error('TQAPI数据源初始化失败:', error);
this.initialized = false;
return false;
}
}
async getContractList(): Promise<any[]> {
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
logger.log('获取合约列表...');
const response = await this.httpClient.get<any>('/contracts');
if (response.success && Array.isArray(response.data)) {
logger.log('获取合约列表成功,数量:', response.data.length);
return response.data;
} else {
logger.error('获取合约列表失败:', response.message);
return [];
}
} catch (error) {
logger.error('获取合约列表失败:', error);
throw error;
}
}
async getContractDetail(symbol: string): Promise<any> {
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
logger.log(`获取合约${symbol}详情...`);
const response = await this.httpClient.get<any>(`/contract/${symbol}`);
if (response.success) {
logger.log(`获取合约${symbol}详情成功`);
return response.data;
} else {
logger.error(`获取合约${symbol}详情失败:`, response.message);
throw new Error(`获取合约${symbol}详情失败`);
}
} catch (error) {
logger.error(`获取合约${symbol}详情失败:`, error);
throw error;
}
}
async getKlineData(symbol: string, period: string, count: number): Promise<any[]> {
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
logger.log('开始获取K线数据:', {
symbol: symbol,
period: period,
count: count
});
// 确保合约代码格式正确
let contractSymbol = symbol;
if (!contractSymbol.includes('.')) {
// 如果没有交易所前缀,尝试添加默认交易所
logger.warn('合约代码缺少交易所前缀,尝试添加默认交易所');
contractSymbol = `SHFE.${contractSymbol}`;
}
logger.log('使用的合约代码:', contractSymbol);
const response = await this.httpClient.get<any>('/klines/' + contractSymbol, {
period: period,
count: count.toString()
});
if (response.success && Array.isArray(response.data)) {
logger.log('获取K线数据成功长度:', response.data.length);
return response.data;
} else {
logger.error('获取K线数据失败:', response.message);
return [];
}
} catch (error) {
logger.error(`获取合约${symbol}K线数据失败:`, error);
throw error;
}
}
async getTickData(symbol: string): Promise<any> {
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
logger.log(`获取合约${symbol}实时行情数据...`);
// 确保合约代码格式正确
let contractSymbol = symbol;
if (!contractSymbol.includes('.')) {
// 如果没有交易所前缀,尝试添加默认交易所
logger.warn('合约代码缺少交易所前缀,尝试添加默认交易所');
contractSymbol = `SHFE.${contractSymbol}`;
}
logger.log('使用的合约代码:', contractSymbol);
logger.log('正在发送GET请求到:', `/tick/${contractSymbol}`);
const start = Date.now();
const response = await this.httpClient.get<any>(`/tick/${contractSymbol}`);
const end = Date.now();
logger.log(`GET请求完成耗时: ${end - start}ms`);
logger.log('响应数据:', response);
if (response.success) {
logger.log(`获取合约${symbol}实时行情数据成功`);
// 计算价格变化
const tickData = response.data;
tickData.price_change = tickData.last_price - (tickData.pre_close || 0);
logger.log('计算价格变化后的数据:', tickData);
return tickData;
} else {
logger.error(`获取合约${symbol}实时行情数据失败:`, response.message);
// 返回默认数据,避免整个请求失败
return {
last_price: 0,
pre_close: 0,
open: 0,
high: 0,
low: 0,
volume: 0,
open_interest: 0,
bid_price1: 0,
bid_volume1: 0,
ask_price1: 0,
ask_volume1: 0,
price_change: 0,
datetime: Date.now()
};
}
} catch (error) {
logger.error(`获取合约${symbol}实时行情数据失败:`, error);
// 返回默认数据,避免整个请求失败
return {
last_price: 0,
pre_close: 0,
open: 0,
high: 0,
low: 0,
volume: 0,
open_interest: 0,
bid_price1: 0,
bid_volume1: 0,
ask_price1: 0,
ask_volume1: 0,
price_change: 0,
datetime: Date.now()
};
}
}
async getMarketOverview(): Promise<any[]> {
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
// 获取所有合约
const contracts = await this.getContractList();
// 限制获取的合约数量,避免请求过多
const limitedContracts = contracts.slice(0, 20);
// 获取每个合约的实时行情
const overview = [];
for (const contract of limitedContracts) {
try {
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({
symbol: contract.symbol,
name: contract.name,
price: price,
change: change,
change_percent: change / pre_close * 100,
volume: volume,
open_interest: open_interest
});
} catch (error) {
logger.error(`获取合约${contract.symbol}行情失败:`, error);
}
}
return overview;
} catch (error) {
logger.error('获取市场概览失败:', error);
throw error;
}
}
async getHistoricalTrades(symbol: string, start: number, end: number): Promise<any[]> {
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
// 目前Python服务未实现此功能
logger.warn('TQAPI服务暂未实现历史成交数据获取功能');
return [];
} catch (error) {
logger.error(`获取合约${symbol}历史成交数据失败:`, error);
throw error;
}
}
async close(): Promise<void> {
try {
logger.log('关闭TQAPI连接...');
const response = await this.httpClient.post<any>('/disconnect');
if (response.success) {
logger.log('TQAPI连接已关闭');
} else {
logger.error('关闭TQAPI连接失败:', response.message);
}
} catch (error) {
logger.error('关闭TQAPI连接失败:', error);
} finally {
this.initialized = false;
logger.log('TQAPI数据源已关闭');
}
}
}

@ -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. 需要确保安全性,特别是在处理敏感信息时

@ -1,5 +1,4 @@
// 市场数据服务 // 市场数据服务
import { DataSourceFactory, DataSourceType } from './datasource/DataSourceFactory';
import { futuresList, generateFuturesOverview, generateFutureData, generateKlineData, riskAlerts } from '../utils/mockData'; import { futuresList, generateFuturesOverview, generateFutureData, generateKlineData, riskAlerts } from '../utils/mockData';
import { config } from '../config'; import { config } from '../config';
import { serviceImplementationClient } from './ServiceImplementationClient'; import { serviceImplementationClient } from './ServiceImplementationClient';
@ -137,104 +136,6 @@ export const fetchMarketOverview = async () => {
// service_implementation API 失败,尝试使用其他数据源 // service_implementation API 失败,尝试使用其他数据源
} }
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
logger.log('获取数据源配置:', dataSourceConfig);
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 使用用户指定的合约列表
const overview = [];
for (const future of futuresList) {
try {
// 构建合约符号使用小写代码因为TQAPI期望小写
const symbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取合约详情和实时行情
const tick = await dataSource.getTickData(symbol);
overview.push({
code: future.code,
name: future.name,
currentPrice: tick.last_price,
changePercent: tick.price_change / tick.pre_close * 100,
winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率
atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR
adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX
adxStatus: (adx: number) => {
if (adx < 20) return '无趋势/震荡';
if (adx < 40) return '弱趋势';
return '强趋势';
},
trends: {
'5MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'30MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1HOUR': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1DAY': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
}
},
tradingAdvice: {
entry: tick.last_price,
stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)),
target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)),
resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)),
support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5))
},
overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)],
aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近`
});
} catch (error) {
logger.error(`获取合约${future.code}行情失败:`, error);
// 跳过获取失败的合约
continue;
}
}
return overview;
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 300));
return generateFuturesOverview();
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取市场概览失败,所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 300));
return generateFuturesOverview();
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) { } catch (error) {
logger.error('获取市场概览失败:', error); logger.error('获取市场概览失败:', error);
// 直接返回友好的错误提示 // 直接返回友好的错误提示
@ -354,103 +255,6 @@ export const fetchMarketDetail = async (symbol: string) => {
// service_implementation API 失败,尝试使用其他数据源 // service_implementation API 失败,尝试使用其他数据源
} }
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 构建合约符号使用小写代码因为TQAPI期望小写
const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取合约详情和实时行情
const tick = await dataSource.getTickData(contractSymbol);
// 转换为前端需要的格式
return {
code: future.code,
name: future.name,
fullName: `${future.name}-${future.code}605`,
currentPrice: tick.last_price,
changePercent: tick.price_change / tick.pre_close * 100,
winRate: Math.floor(Math.random() * 50) + 30,
atr: +(Math.random() * 5 + 0.5).toFixed(2),
adx: Math.floor(Math.random() * 60) + 10,
adxStatus: (adx: number) => {
if (adx < 20) return '无趋势/震荡';
if (adx < 40) return '弱趋势';
return '强趋势';
},
trends: {
'5MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'30MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1HOUR': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1DAY': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
}
},
indicators: {
macd: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)],
rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`,
bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)],
kdj: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)]
},
tradingAdvice: {
entry: tick.last_price,
stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)),
target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)),
resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)),
support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5))
},
riskLevel: ['低', '中等', '高'][Math.floor(Math.random() * 3)],
volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)],
overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)],
aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近`
};
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 200));
return generateFutureData(symbol, future.name);
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取品种详情失败,所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 200));
return generateFutureData(symbol, future.name);
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) { } catch (error) {
logger.error(`获取品种${symbol}详情失败:`, error); logger.error(`获取品种${symbol}详情失败:`, error);
// 直接返回友好的错误提示 // 直接返回友好的错误提示
@ -535,58 +339,6 @@ export const fetchKlineData = async (symbol: string, period: string) => {
logger.error('service_implementation API 获取K线数据失败:', error); logger.error('service_implementation API 获取K线数据失败:', error);
// service_implementation API 失败,尝试使用其他数据源 // service_implementation API 失败,尝试使用其他数据源
} }
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 构建合约符号使用小写代码因为TQAPI期望小写
const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取K线数据
const klineData = await dataSource.getKlineData(contractSymbol, period, 30);
// 转换为前端需要的格式
return klineData.map(item => ({
timestamp: item.datetime / 1000000000, // 转换为秒
open: item.open,
high: item.high,
low: item.low,
close: item.close,
volume: item.volume
}));
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 200));
return generateKlineData(30);
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取K线数据失败所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 200));
return generateKlineData(30);
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) { } catch (error) {
logger.error(`获取合约${symbol}K线数据失败:`, error); logger.error(`获取合约${symbol}K线数据失败:`, error);
// 直接返回友好的错误提示 // 直接返回友好的错误提示

Loading…
Cancel
Save