fix: 天勤服务器通信成功,获取k线仍有问题

master
Lxy 3 months ago
parent aa84e15132
commit 41916f3ee0

@ -0,0 +1,33 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Backend (ts-node-dev)",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": [
"ts-node-dev",
"--respawn",
"--transpile-only",
"src/app.ts"
],
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"restart": true,
"protocol": "inspector"
},
{
"name": "Debug Backend (built)",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"args": ["dist/app.js"],
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"protocol": "inspector"
}
]
}

@ -18,9 +18,7 @@
"morgan": "^1.10.0",
"pg": "^8.11.3",
"redis": "^4.6.12",
"socket.io": "^4.7.4",
"tqsdk": "^1.3.1",
"ws": "^8.19.0"
"socket.io": "^4.7.4"
},
"devDependencies": {
"@types/cors": "^2.8.17",
@ -29,7 +27,6 @@
"@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",
@ -602,16 +599,6 @@
"@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",
@ -1132,6 +1119,7 @@
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@ -1401,17 +1389,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/core-js": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
@ -1953,12 +1930,6 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==",
"license": "MIT"
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@ -2573,12 +2544,6 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -2855,24 +2820,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"license": "Apache-2.0",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -3345,6 +3292,7 @@
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
@ -4446,21 +4394,6 @@
"node": ">=0.6"
}
},
"node_modules/tqsdk": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tqsdk/-/tqsdk-1.3.1.tgz",
"integrity": "sha512-3SzqnMg4lES/GWw2ktKJ17619UrFSSmElmix32Ptula/Us5gKyBpukra9XkJbbNBdUOgi4RGnyvW1edseEtFtw==",
"license": "ISC",
"dependencies": {
"core-js": "^3.4.4",
"eventemitter3": "^3.1.0",
"localforage": "^1.7.3"
},
"optionalDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
@ -4763,6 +4696,7 @@
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@ -4875,27 +4809,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/ws": {
"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"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

@ -21,9 +21,7 @@
"morgan": "^1.10.0",
"pg": "^8.11.3",
"redis": "^4.6.12",
"socket.io": "^4.7.4",
"tqsdk": "^1.3.1",
"ws": "^8.19.0"
"socket.io": "^4.7.4"
},
"devDependencies": {
"@types/cors": "^2.8.17",
@ -32,7 +30,6 @@
"@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",

@ -0,0 +1,108 @@
# 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**:与天勤量化服务交互
- **异步处理**:提高并发性能
- **错误处理**:确保服务稳定性

@ -0,0 +1,70 @@
from fastapi import APIRouter, HTTPException, Query
from schemas import ConnectRequest, ConnectResponse, ContractResponse, TickResponse, KlineResponse, DisconnectResponse
from tqapi_service import TqApiService
router = APIRouter()
tq_service = TqApiService()
@router.post("/connect", response_model=ConnectResponse)
async def connect(request: ConnectRequest):
"""连接到天勤服务器"""
try:
success = await tq_service.connect(request.username, request.password)
if success:
return ConnectResponse(success=True, message="连接成功")
else:
raise HTTPException(status_code=400, detail="连接失败")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/contracts", response_model=ContractResponse)
async def get_contracts():
"""获取合约列表"""
try:
contracts = await tq_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:
contract = await tq_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:
klines = await tq_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:
tick = await tq_service.get_tick(symbol)
return TickResponse(success=True, data=tick)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/disconnect", response_model=DisconnectResponse)
async def disconnect():
"""断开连接"""
try:
success = await tq_service.disconnect()
if success:
return DisconnectResponse(success=True, message="断开成功")
else:
raise HTTPException(status_code=400, detail="断开失败")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@ -0,0 +1,55 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.router import router, tq_service
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连接
try:
success = await tq_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)

@ -0,0 +1,5 @@
fastapi
uvicorn[standard]
tqsdk
python-dotenv
pydantic-settings

@ -0,0 +1,26 @@
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

@ -0,0 +1,20 @@
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()

@ -0,0 +1,279 @@
import asyncio
import datetime
from tqsdk import TqApi, TqAuth
from typing import List, Dict, Any
class TqApiService:
def __init__(self):
self.api = None
self.connected = False
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数据"""
try:
if not self.connected:
# 未连接到天勤服务器,返回默认数据
print("未连接到天勤服务器返回默认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())
}
if not self.api:
# API实例尚未初始化返回默认数据
print("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())
}
# 获取实时行情
quote = self.api.get_quote(symbol)
# 尝试多次获取行情更新,避免单次超时
max_attempts = 3
for attempt in range(max_attempts):
try:
# 等待行情更新,设置超时
updated = self.api.wait_update(1.0) # 1秒超时
if updated:
break
print(f"尝试 {attempt + 1}/{max_attempts}: 获取 {symbol} 的行情信息超时")
except Exception as e:
print(f"尝试 {attempt + 1}/{max_attempts}: 获取 {symbol} 的行情信息出错: {e}")
await asyncio.sleep(0.5)
# 即使没有更新,也尝试返回当前数据
return {
"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())
}
except Exception as e:
print(f"获取tick数据失败: {e}")
# 返回默认数据,避免整个请求失败
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())
}
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小时

@ -10,6 +10,7 @@ import riskRoutes from './api/risk';
import configRoutes from './api/config';
import watchlistRoutes from './api/watchlist';
import pushRoutes from './api/push';
import { pythonServiceManager } from './services/datasource/PythonServiceManager';
const app = express();
@ -55,8 +56,51 @@ app.use((err: any, req: express.Request, res: express.Response, next: express.Ne
});
// 启动服务器
app.listen(3007, () => {
console.log('服务器运行在 http://localhost:3007');
});
const port = config.port || 3007;
// 启动前检查是否需要启动Python TQAPI服务
async function startServer() {
try {
// 检查配置是否使用TQSDK数据源
if (config.dataSource?.defaultDataSource === 'tqsdk' ||
(config.dataSource?.tqsdk && config.dataSource.tqsdk.enabled)) {
console.log('检测到使用TQSDK数据源准备启动Python服务...');
const pythonStarted = await pythonServiceManager.start();
if (!pythonStarted) {
console.warn('Python服务启动失败可能会影响TQSDK数据获取');
}
}
// 启动Express服务器
const server = app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});
// 处理服务器关闭事件
function handleShutdown() {
console.log('正在关闭服务器...');
// 关闭Python服务
pythonServiceManager.stop();
console.log('Python服务已关闭');
// 关闭Express服务器
server.close(() => {
console.log('服务器已关闭');
process.exit(0);
});
}
// 监听终止信号
process.on('SIGINT', handleShutdown);
process.on('SIGTERM', handleShutdown);
process.on('exit', handleShutdown);
} catch (error) {
console.error('启动服务器时出错:', error);
// 出错时也要关闭Python服务
pythonServiceManager.stop();
}
}
// 启动服务器
startServer();
export default app;

@ -48,7 +48,7 @@ try {
}
export const config = {
port: process.env.PORT || (fileConfig.server?.port || 3005),
port: process.env.PORT || (fileConfig.server?.port || 3007),
jwtSecret: process.env.JWT_SECRET || (fileConfig.security?.jwtSecret || 'your-secret-key'),
database: {
mongo: {

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

@ -1,10 +1,117 @@
// TQSDK数据源实现
// TQAPI数据源实现 - 使用Python服务
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 {
private tq: any = null;
private httpClient: HttpClient;
private initialized: boolean = false;
private config: {
username?: string;
@ -12,113 +119,110 @@ export class TQDataSource implements DataSource {
timeout?: number;
retries?: number;
maxConnections?: number;
pythonServiceUrl?: string;
};
constructor(config: any = {}) {
console.log('使用TQSDK数据源初始化...');
console.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
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> {
try {
console.log('开始初始化TQSDK数据源...');
// 创建TQSDK实例使用配置的参数
console.log('创建TQSDK实例...');
// 初始化TQSDK传入WebSocket对象
this.tq = new TQSDK({ autoInit: true }, { WebSocket });
console.log('开始初始化TQAPI数据源...');
console.log('Python服务URL:', this.config.pythonServiceUrl);
console.log('连接参数:', {
username: this.config.username ? '***' : '',
password: this.config.password ? '***' : ''
});
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('等待TQSDK连接超时时间:', timeout, 'ms');
// 连接到天勤服务器
console.log('连接到天勤服务器...');
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);
try {
const response = await this.httpClient.post<any>('/connect', {
username: this.config.username,
password: this.config.password
});
// 监听错误事件
this.tq?.on('error', (error: any) => {
console.error('TQSDK错误:', error);
clearTimeout(timeoutId);
reject(error);
});
});
console.log('连接响应:', response);
if (response.success) {
console.log('TQAPI连接成功');
this.initialized = true;
console.log('TQSDK数据源初始化成功');
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);
console.error('TQAPI连接失败:', response.message);
this.initialized = false;
return false;
}
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;
return false;
}
}
async getContractList(): Promise<any[]> {
if (!this.initialized || !this.tq) {
throw new Error('TQSDK数据源未初始化');
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
// 使用TQSDK的getQuotesByInput方法获取合约列表
// 这里使用空字符串搜索,获取所有合约
const contracts = this.tq.getQuotesByInput('', {
future: true,
future_index: true,
future_cont: true,
option: false,
combine: false
});
console.log('获取合约列表...');
const response = await this.httpClient.get<any>('/contracts');
// 转换合约格式
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;
if (response.success && Array.isArray(response.data)) {
console.log('获取合约列表成功,数量:', response.data.length);
return response.data;
} else {
console.error('获取合约列表失败:', response.message);
return [];
}
}).filter((contract: any) => contract !== null);
return futuresContracts;
} catch (error) {
console.error('获取合约列表失败:', error);
throw error;
@ -126,23 +230,21 @@ export class TQDataSource implements DataSource {
}
async getContractDetail(symbol: string): Promise<any> {
if (!this.initialized || !this.tq) {
throw new Error('TQSDK数据源未初始化');
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
// 获取合约详情
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
};
console.log(`获取合约${symbol}详情...`);
const response = await this.httpClient.get<any>(`/contract/${symbol}`);
if (response.success) {
console.log(`获取合约${symbol}详情成功`);
return response.data;
} else {
console.error(`获取合约${symbol}详情失败:`, response.message);
throw new Error(`获取合约${symbol}详情失败`);
}
} catch (error) {
console.error(`获取合约${symbol}详情失败:`, error);
throw error;
@ -150,16 +252,38 @@ export class TQDataSource implements DataSource {
}
async getKlineData(symbol: string, period: string, count: number): Promise<any[]> {
if (!this.initialized || !this.tq) {
throw new Error('TQSDK数据源未初始化');
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
// 转换周期格式
const duration = this.convertPeriodToDuration(period);
// 获取K线数据
const klines = this.tq.getKlines(symbol, duration);
return klines.data || [];
console.log('开始获取K线数据:', {
symbol: symbol,
period: period,
count: count
});
// 确保合约代码格式正确
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) {
console.error(`获取合约${symbol}K线数据失败:`, error);
throw error;
@ -167,28 +291,40 @@ export class TQDataSource implements DataSource {
}
async getTickData(symbol: string): Promise<any> {
if (!this.initialized || !this.tq) {
throw new Error('TQSDK数据源未初始化');
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
// 获取实时行情数据
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
};
console.log(`获取合约${symbol}实时行情数据...`);
// 确保合约代码格式正确
let contractSymbol = symbol;
if (!contractSymbol.includes('.')) {
// 如果没有交易所前缀,尝试添加默认交易所
console.warn('合约代码缺少交易所前缀,尝试添加默认交易所');
contractSymbol = `SHFE.${contractSymbol}`;
}
console.log('使用的合约代码:', contractSymbol);
console.log('正在发送GET请求到:', `/tick/${contractSymbol}`);
const start = Date.now();
const response = await this.httpClient.get<any>(`/tick/${contractSymbol}`);
const end = Date.now();
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) {
console.error(`获取合约${symbol}实时行情数据失败:`, error);
throw error;
@ -196,8 +332,8 @@ export class TQDataSource implements DataSource {
}
async getMarketOverview(): Promise<any[]> {
if (!this.initialized || !this.tq) {
throw new Error('TQSDK数据源未初始化');
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
@ -210,14 +346,21 @@ export class TQDataSource implements DataSource {
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: tick.last_price,
change: tick.price_change,
change_percent: tick.price_change / tick.pre_close * 100,
volume: tick.volume,
open_interest: tick.open_interest
price: price,
change: change,
change_percent: change / pre_close * 100,
volume: volume,
open_interest: open_interest
});
} catch (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[]> {
if (!this.initialized || !this.tq) {
throw new Error('TQSDK数据源未初始化');
if (!this.initialized) {
throw new Error('TQAPI数据源未初始化');
}
try {
// TQSDK Node.js版本可能不支持直接获取历史成交数据
// 这里返回空数组实际使用时需要根据TQSDK文档调整
console.warn('TQSDK Node.js版本可能不支持直接获取历史成交数据');
// 目前Python服务未实现此功能
console.warn('TQAPI服务暂未实现历史成交数据获取功能');
return [];
} catch (error) {
console.error(`获取合约${symbol}历史成交数据失败:`, error);
@ -247,45 +389,20 @@ export class TQDataSource implements DataSource {
}
async close(): Promise<void> {
if (this.tq) {
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('关闭TQAPI连接...');
const response = await this.httpClient.post<any>('/disconnect');
if (response.success) {
console.log('TQAPI连接已关闭');
} else {
console.error('关闭TQAPI连接失败:', response.message);
}
console.log('TQSDK连接已关闭');
} catch (error) {
console.error('关闭TQSDK连接失败:', error);
}
this.tq = null;
console.error('关闭TQAPI连接失败:', error);
} finally {
this.initialized = false;
console.log('TQSDK数据源已关闭');
}
}
// 转换周期格式为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分钟
console.log('TQAPI数据源已关闭');
}
}
}

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

@ -4,64 +4,64 @@
export const futuresList = [
// 金属类
{ code: 'AU', name: '黄金', type: '金属', exchange: 'SHFE' },
{ code: 'AG', name: '白银', type: '金属', exchange: 'SHFE' },
{ code: 'CU', name: '铜', type: '金属', exchange: 'SHFE' },
{ code: 'NI', name: '镍', type: '金属', exchange: 'SHFE' },
{ code: 'SN', name: '锡', type: '金属', exchange: 'SHFE' },
{ code: 'AL', name: '铝', type: '金属', exchange: 'SHFE' },
{ code: 'ZN', name: '锌', type: '金属', exchange: 'SHFE' },
{ code: 'PB', name: '铅', type: '金属', exchange: 'SHFE' },
// 建材类
{ code: 'FG', name: '玻璃', type: '建材', exchange: 'CZCE' },
{ code: 'LY', name: '烧碱', type: '建材', exchange: 'CZCE' },
{ code: 'SA', name: '纯碱', type: '建材', exchange: 'CZCE' },
{ code: 'JM', name: '焦煤', type: '建材', exchange: 'DCE' },
{ code: 'RB', name: '螺纹钢', type: '建材', exchange: 'SHFE' },
{ code: 'ALO', name: '氧化铝', type: '建材', exchange: 'SHFE' },
{ code: 'HC', name: '热轧卷板', type: '建材', exchange: 'SHFE' },
// 能源化工类
{ code: 'MA', name: '甲醇', type: '能源化工', exchange: 'CZCE' },
{ code: 'V', name: 'PVC', type: '能源化工', exchange: 'DCE' },
{ code: 'FU', name: '燃油', type: '能源化工', exchange: 'SHFE' },
{ code: 'SC', name: '原油', type: '能源化工', exchange: 'INE' },
{ code: 'RU', name: '橡胶', type: '能源化工', exchange: 'SHFE' },
{ code: 'BR', name: '合成橡胶', type: '能源化工', exchange: 'DCE' },
{ code: 'NR', name: '20号胶', type: '能源化工', exchange: 'SHFE' },
{ code: 'BU', name: '沥青', type: '能源化工', exchange: 'SHFE' },
{ code: 'LU', name: '低硫燃油', type: '能源化工', exchange: 'INE' },
{ code: 'L', name: '聚乙烯', type: '能源化工', exchange: 'DCE' },
{ code: 'PP', name: '聚丙烯', type: '能源化工', exchange: 'DCE' },
{ code: 'TA', name: 'PTA', type: '能源化工', exchange: 'CZCE' },
// 农产品类
{ code: 'P', name: '棕榈油', type: '农产品', exchange: 'DCE' },
{ code: 'A', name: '大豆', type: '农产品', exchange: 'DCE' },
{ code: 'B', name: '豆粕', type: '农产品', exchange: 'DCE' },
{ code: 'M', name: '豆粕', type: '农产品', exchange: 'DCE' },
{ code: 'Y', name: '豆油', type: '农产品', exchange: 'DCE' },
{ code: 'C', name: '玉米', type: '农产品', exchange: 'DCE' },
{ code: 'CS', name: '玉米淀粉', type: '农产品', exchange: 'DCE' },
{ code: 'CF', name: '棉花', type: '农产品', exchange: 'CZCE' },
{ code: 'SR', name: '白糖', type: '农产品', exchange: 'CZCE' },
{ code: 'RM', name: '菜籽粕', type: '农产品', exchange: 'CZCE' },
{ code: 'OI', name: '菜籽油', type: '农产品', exchange: 'CZCE' },
// 新能源类
{ code: 'LI', name: '碳酸锂', type: '新能源', exchange: 'SHFE' },
{ code: 'SI', name: '工业硅', type: '新能源', exchange: 'GEM' },
{ code: 'SP', name: '多晶硅', type: '新能源', exchange: 'GEM' },
// 金融类
{ code: 'IM', name: '中证1000', type: '金融', exchange: 'CFFEX' },
{ code: 'IC', name: '中证500', type: '金融', exchange: 'CFFEX' },
{ code: 'IH', name: '上证50', type: '金融', exchange: 'CFFEX' },
// 其他
{ code: 'I', name: '铁矿石', type: '其他', exchange: 'DCE' },
{ code: 'J', name: '焦炭', type: '其他', exchange: 'DCE' },
{ code: 'ZC', name: '动力煤', type: '其他', exchange: 'CZCE' }
{ code: 'AG', name: '白银', type: '金属', exchange: 'SHFE' }
// { code: 'CU', name: '铜', type: '金属', exchange: 'SHFE' },
// { code: 'NI', name: '镍', type: '金属', exchange: 'SHFE' },
// { code: 'SN', name: '锡', type: '金属', exchange: 'SHFE' },
// { code: 'AL', name: '铝', type: '金属', exchange: 'SHFE' },
// { code: 'ZN', name: '锌', type: '金属', exchange: 'SHFE' },
// { code: 'PB', name: '铅', type: '金属', exchange: 'SHFE' },
// // 建材类
// { code: 'FG', name: '玻璃', type: '建材', exchange: 'CZCE' },
// { code: 'LY', name: '烧碱', type: '建材', exchange: 'CZCE' },
// { code: 'SA', name: '纯碱', type: '建材', exchange: 'CZCE' },
// { code: 'JM', name: '焦煤', type: '建材', exchange: 'DCE' },
// { code: 'RB', name: '螺纹钢', type: '建材', exchange: 'SHFE' },
// { code: 'ALO', name: '氧化铝', type: '建材', exchange: 'SHFE' },
// { code: 'HC', name: '热轧卷板', type: '建材', exchange: 'SHFE' },
// // 能源化工类
// { code: 'MA', name: '甲醇', type: '能源化工', exchange: 'CZCE' },
// { code: 'V', name: 'PVC', type: '能源化工', exchange: 'DCE' },
// { code: 'FU', name: '燃油', type: '能源化工', exchange: 'SHFE' },
// { code: 'SC', name: '原油', type: '能源化工', exchange: 'INE' },
// { code: 'RU', name: '橡胶', type: '能源化工', exchange: 'SHFE' },
// { code: 'BR', name: '合成橡胶', type: '能源化工', exchange: 'DCE' },
// { code: 'NR', name: '20号胶', type: '能源化工', exchange: 'SHFE' },
// { code: 'BU', name: '沥青', type: '能源化工', exchange: 'SHFE' },
// { code: 'LU', name: '低硫燃油', type: '能源化工', exchange: 'INE' },
// { code: 'L', name: '聚乙烯', type: '能源化工', exchange: 'DCE' },
// { code: 'PP', name: '聚丙烯', type: '能源化工', exchange: 'DCE' },
// { code: 'TA', name: 'PTA', type: '能源化工', exchange: 'CZCE' },
// // 农产品类
// { code: 'P', name: '棕榈油', type: '农产品', exchange: 'DCE' },
// { code: 'A', name: '大豆', type: '农产品', exchange: 'DCE' },
// { code: 'B', name: '豆粕', type: '农产品', exchange: 'DCE' },
// { code: 'M', name: '豆粕', type: '农产品', exchange: 'DCE' },
// { code: 'Y', name: '豆油', type: '农产品', exchange: 'DCE' },
// { code: 'C', name: '玉米', type: '农产品', exchange: 'DCE' },
// { code: 'CS', name: '玉米淀粉', type: '农产品', exchange: 'DCE' },
// { code: 'CF', name: '棉花', type: '农产品', exchange: 'CZCE' },
// { code: 'SR', name: '白糖', type: '农产品', exchange: 'CZCE' },
// { code: 'RM', name: '菜籽粕', type: '农产品', exchange: 'CZCE' },
// { code: 'OI', name: '菜籽油', type: '农产品', exchange: 'CZCE' },
// // 新能源类
// { code: 'LI', name: '碳酸锂', type: '新能源', exchange: 'SHFE' },
// { code: 'SI', name: '工业硅', type: '新能源', exchange: 'GEM' },
// { code: 'SP', name: '多晶硅', type: '新能源', exchange: 'GEM' },
// // 金融类
// { code: 'IM', name: '中证1000', type: '金融', exchange: 'CFFEX' },
// { code: 'IC', name: '中证500', type: '金融', exchange: 'CFFEX' },
// { code: 'IH', name: '上证50', type: '金融', exchange: 'CFFEX' },
// // 其他
// { code: 'I', name: '铁矿石', type: '其他', exchange: 'DCE' },
// { code: 'J', name: '焦炭', type: '其他', exchange: 'DCE' },
// { code: 'ZC', name: '动力煤', type: '其他', exchange: 'CZCE' }
];
// 生成随机数据的工具函数

@ -0,0 +1,32 @@
const http = require('http');
function testMarketOverview() {
console.log('测试获取市场概览...');
const options = {
hostname: 'localhost',
port: 3007,
path: '/api/market/overview',
method: 'GET'
};
const req = http.request(options, (res) => {
console.log(`状态码: ${res.statusCode}`);
res.setEncoding('utf8');
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('响应数据:', data);
});
});
req.on('error', (e) => {
console.error(`请求失败: ${e.message}`);
});
req.end();
}
testMarketOverview();

@ -0,0 +1,32 @@
const http = require('http');
function testMarketDetail() {
console.log('测试获取品种详情...');
const options = {
hostname: 'localhost',
port: 3007,
path: '/api/market/detail/AU',
method: 'GET'
};
const req = http.request(options, (res) => {
console.log(`状态码: ${res.statusCode}`);
res.setEncoding('utf8');
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('响应数据:', data);
});
});
req.on('error', (e) => {
console.error(`请求失败: ${e.message}`);
});
req.end();
}
testMarketDetail();

@ -0,0 +1,67 @@
const http = require('http');
function testConnectEndpoint() {
console.log('测试/connect端点...');
const options = {
hostname: 'localhost',
port: 8001,
path: '/api/connect',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(JSON.stringify({ username: '', password: '' }))
}
};
const req = http.request(options, (res) => {
console.log(`状态码: ${res.statusCode}`);
res.setEncoding('utf8');
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('响应数据:', data);
testContractsEndpoint();
});
});
req.on('error', (e) => {
console.error(`请求失败: ${e.message}`);
});
req.write(JSON.stringify({ username: '', password: '' }));
req.end();
}
function testContractsEndpoint() {
console.log('测试/contracts端点...');
const options = {
hostname: 'localhost',
port: 8001,
path: '/api/contracts',
method: 'GET'
};
const req = http.request(options, (res) => {
console.log(`状态码: ${res.statusCode}`);
res.setEncoding('utf8');
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('响应数据:', data);
});
});
req.on('error', (e) => {
console.error(`请求失败: ${e.message}`);
});
req.end();
}
testConnectEndpoint();

@ -0,0 +1,17 @@
{
"server": {
"port": 3007
},
"dataSource": {
"tqsdk": {
"enabled": true,
"username": "你的天勤账号",
"password": "你的天勤密码",
"timeout": 30000,
"retries": 3,
"maxConnections": 5,
"pythonPort": 8000
},
"defaultDataSource": "tqsdk"
}
}

@ -37,7 +37,7 @@
}
},
"server": {
"port": 3006,
"port": 3007,
"host": "0.0.0.0",
"environment": "development",
"debug": true,
@ -53,22 +53,13 @@
},
"cors": {
"origin": "*",
"methods": [
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
],
"allowedHeaders": [
"Content-Type",
"Authorization"
]
"methods": "GET, POST, PUT, DELETE, OPTIONS",
"allowedHeaders": "Content-Type, Authorization"
}
},
"dataSource": {
"test": {
"enabled": true,
"enabled": false,
"timeout": 10000,
"retries": 3,
"refreshInterval": 60000
@ -77,6 +68,7 @@
"enabled": true,
"username": "windsdreamer",
"password": "1qazse42W3",
"pythonPort": 8001 ,
"timeout": 10000,
"retries": 10,
"maxConnections": 20

@ -0,0 +1,354 @@
# 前端开发文档
## 1. 项目架构
### 1.1 技术栈
| 类别 | 技术/库 | 版本 | 用途 |
|------|---------|------|------|
| 框架 | React | 18.x | 前端UI框架 |
| 路由 | React Router | 6.x | 页面路由管理 |
| 状态管理 | Redux Toolkit | 2.x | 全局状态管理 |
| UI组件库 | Ant Design | 5.x | 界面组件 |
| 图表库 | Lightweight Charts | 4.x | K线图表展示 |
| HTTP客户端 | Fetch API | 浏览器内置 | API调用 |
| 构建工具 | Vite | 5.x | 项目构建和开发服务器 |
### 1.2 目录结构
```
src/
├── assets/ # 静态资源
├── components/ # 通用组件
│ └── layout/ # 布局组件
├── pages/ # 页面组件
│ ├── admin/ # 管理配置页面
│ ├── config/ # 配置管理页面
│ ├── dashboard/ # 市场概览页面
│ ├── detail/ # 合约详情页面
│ ├── risk-control/ # 风控管理页面
│ └── watchlist/ # 自选合约页面
├── store/ # Redux状态管理
│ ├── futuresSlice.js # 期货数据状态管理
│ └── index.js # Store配置
├── utils/ # 工具函数
│ └── mockData.js # 模拟数据
├── App.jsx # 应用入口组件
├── main.jsx # 应用启动文件
└── index.css # 全局样式
```
## 2. 页面结构与路由
### 2.1 路由配置
| 路径 | 组件 | 功能 |
|------|------|------|
| `/` | Dashboard | 市场概览 |
| `/watchlist` | Watchlist | 自选合约 |
| `/detail/:code` | Detail | 合约详情分析 |
| `/risk-control` | RiskControl | 风控管理 |
| `/config` | Config | 配置管理 |
| `/admin` | AdminConfig | 管理配置 |
### 2.2 布局设计
- **顶部导航栏**:包含系统名称、深色模式切换、消息通知和用户头像
- **左侧菜单栏**:包含主要功能模块的导航链接
- **右侧内容区**:根据路由显示对应页面内容
- **响应式设计**:支持桌面端、平板和移动端
## 3. 组件设计
### 3.1 布局组件
#### MainLayout 组件
- **功能**:提供统一的页面布局,包括顶部导航栏和左侧菜单栏
- **交互**
- 支持菜单栏折叠/展开
- 深色模式切换
- 响应式布局适配
### 3.2 页面组件
#### Dashboard 页面
- **功能**:市场概览,展示所有合约的基本信息和热点合约
- **交互**
- 数据刷新
- 合约筛选和排序
- 添加/移除自选合约
- 查看合约详情
- 消息推送配置
#### Detail 页面
- **功能**合约详情分析包括K线图、技术指标和AI研判
- **交互**
- 周期切换5分钟、30分钟、1小时、1天、1周
- 技术指标切换MA、MACD、KDJ、RSI、布林带
- 图表缩放和拖拽
#### Watchlist 页面
- **功能**:展示用户添加的自选合约
- **交互**
- 查看合约详情
- 从自选中移除合约
- 消息推送配置
#### RiskControl 页面
- **功能**:风险管理和仓位计算
- **交互**
- 止损策略设置
- 仓位大小计算
- 风险偏好设置
- 换月预警查看
#### Config 页面
- **功能**:系统配置管理
- **交互**
- 系统参数调整
- 消息推送配置
#### AdminConfig 页面
- **功能**:管理配置,包括数据库、服务器、安全和数据源配置
- **交互**
- 数据库连接测试
- 数据源连接测试
- 配置保存和恢复默认
## 4. 状态管理
### 4.1 Redux Store 设计
```javascript
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import futuresReducer from './futuresSlice';
const store = configureStore({
reducer: {
futures: futuresReducer
}
});
export default store;
```
### 4.2 状态切片
#### futuresSlice.js
- **状态结构**
- `overview`:市场概览数据
- `selectedFuture`:当前选中的合约详情
- `riskAlerts`:风险预警数据
- `aiAnalysis`AI市场分析数据
- `loading`:加载状态
- `error`:错误信息
- `watchlist`:自选合约列表
- **异步操作**
- `fetchFuturesOverview`:获取市场概览数据
- `fetchFutureDetail`:获取单个合约详情
- `fetchRiskAlerts`:获取风险预警数据
- `fetchAIMarketAnalysis`获取AI市场分析数据
- **同步操作**
- `selectFuture`:选择合约
- `clearSelectedFuture`:清除选中合约
- `toggleWatchlist`:切换自选合约状态
## 5. API 调用
### 5.1 后端 API 接口
| API路径 | 方法 | 功能 |
|---------|------|------|
| `/api/market/overview` | GET | 获取市场概览数据 |
| `/api/market/detail/:code` | GET | 获取合约详情数据 |
| `/api/market/klines/:code` | GET | 获取合约K线数据 |
| `/api/market/alerts` | GET | 获取风险预警数据 |
| `/api/config/get` | GET | 获取系统配置 |
| `/api/config/save` | POST | 保存系统配置 |
| `/api/config/test-database` | POST | 测试数据库连接 |
| `/api/config/test-datasource` | POST | 测试数据源连接 |
### 5.2 前端 API 调用封装
```javascript
// 后端API基础URL
const API_BASE_URL = 'http://localhost:3007/api';
// 异步获取期货概览数据
export const fetchFuturesOverview = createAsyncThunk(
'futures/fetchOverview',
async () => {
const response = await fetch(`${API_BASE_URL}/market/overview`);
if (!response.ok) {
throw new Error('获取市场概览失败');
}
const data = await response.json();
return data.data;
}
);
```
## 6. 用户交互流程
### 6.1 市场概览流程
1. 用户进入系统,看到市场概览页面
2. 系统自动加载市场概览数据、风险预警和AI市场分析
3. 用户可以:
- 刷新数据
- 按类型筛选合约
- 按胜率、涨跌幅或波动率排序
- 点击合约查看详情
- 添加合约到自选
- 配置消息推送
### 6.2 合约详情流程
1. 用户从市场概览或自选列表点击合约进入详情页
2. 系统加载合约详情数据和K线数据
3. 用户可以:
- 切换不同时间周期的K线图
- 切换不同的技术指标
- 查看技术指标数据
- 查看多周期趋势
- 查看AI研判结果
- 查看交易建议和风险评估
### 6.3 自选合约流程
1. 用户进入自选合约页面
2. 系统显示用户添加的自选合约列表
3. 用户可以:
- 刷新数据
- 查看合约详情
- 从自选中移除合约
- 配置消息推送
### 6.4 风控管理流程
1. 用户进入风控管理页面
2. 用户可以:
- 设置止损策略
- 计算仓位大小
- 设置风险偏好
- 查看换月预警
- 查看风险监控指标
### 6.5 配置管理流程
1. 用户进入配置管理页面
2. 用户可以:
- 调整系统参数
- 配置消息推送方式和内容
### 6.6 管理配置流程
1. 管理员进入管理配置页面
2. 管理员可以:
- 配置数据库连接
- 配置服务器参数
- 配置安全设置
- 配置数据源
- 测试连接
- 保存配置
## 7. 响应式设计
- **桌面端**:完整布局,左侧菜单展开
- **平板端**:左侧菜单可折叠,内容区域自适应
- **移动端**:底部导航栏,顶部状态栏,内容区域全屏
## 8. 深色模式
- **实现方式**:使用 Ant Design 的 ConfigProvider 和 CSS 变量
- **切换方式**:顶部导航栏的深色模式切换开关
- **效果**:全局界面元素的颜色和背景色随模式切换
## 9. 性能优化
### 9.1 代码分割
- 使用 React.lazy 和 Suspense 实现组件懒加载
- 按路由分割代码,减少初始加载时间
### 9.2 数据缓存
- 使用 Redux 缓存已获取的数据
- 避免重复请求相同数据
### 9.3 图表优化
- 使用 Lightweight Charts 高性能图表库
- 按需加载图表数据
- 优化图表渲染性能
## 10. 错误处理
- **API 错误处理**:捕获并显示 API 调用错误
- **图表错误处理**:当图表渲染失败时显示错误信息
- **数据加载状态**:使用 Spin 组件显示加载状态
- **边界情况**:处理空数据、无效参数等边界情况
## 11. 开发和部署
### 11.1 开发环境
- **启动命令**`npm run dev`
- **开发服务器**http://localhost:5173
- **API 代理**:配置代理到后端服务 http://localhost:3007
### 11.2 构建和部署
- **构建命令**`npm run build`
- **构建产物**`dist` 目录
- **部署方式**:可部署到任何静态文件服务器
## 12. 功能特性总结
| 功能 | 描述 | 实现方式 |
|------|------|----------|
| 市场概览 | 展示所有合约的基本信息和热点合约 | Redux + API调用 + 响应式布局 |
| 合约详情 | 展示合约的K线图、技术指标和AI研判 | Lightweight Charts + 技术指标计算 |
| 自选合约 | 管理用户关注的合约 | Redux状态管理 |
| 风控管理 | 风险管理和仓位计算 | 表单计算 + 数据展示 |
| 配置管理 | 系统配置和消息推送配置 | 表单 + 本地存储 |
| 管理配置 | 数据库、服务器和数据源配置 | 表单 + API调用 |
| 深色模式 | 支持深色和浅色主题切换 | Ant Design ConfigProvider |
| 响应式设计 | 适配不同屏幕尺寸 | CSS媒体查询 + Flexbox |
| 性能优化 | 代码分割和数据缓存 | React.lazy + Redux缓存 |
| 错误处理 | 完善的错误处理和边界情况 | 错误捕获 + 友好提示 |
## 13. 技术亮点
1. **模块化设计**:清晰的代码结构和组件划分
2. **高性能图表**:使用 Lightweight Charts 实现流畅的K线图表
3. **AI 集成**集成AI市场分析和预测功能
4. **实时数据**通过WebSocket获取实时市场数据
5. **响应式布局**:适配各种设备屏幕
6. **深色模式**:支持深色和浅色主题
7. **完善的风控**:提供全面的风险管理工具
8. **消息推送**:多渠道消息推送配置
9. **性能优化**:代码分割和数据缓存
10. **错误处理**:友好的错误提示和边界情况处理
## 14. 未来扩展
1. **用户系统**:添加用户登录、注册和权限管理
2. **交易系统**:集成交易功能,支持下单和撤单
3. **策略回测**:添加策略回测功能,评估交易策略
4. **更多数据源**支持更多数据源如Wind、新浪财经等
5. **更多技术指标**:添加更多技术指标和分析工具
6. **移动应用**开发iOS和Android移动应用
7. **数据可视化**:增强数据可视化效果,添加更多图表类型
8. **智能投顾**基于AI的智能投资顾问功能

@ -0,0 +1,337 @@
# 后端服务开发文档
## 1. 项目结构
```
backend/
├── src/
│ ├── api/ # API路由
│ ├── config/ # 配置管理
│ ├── services/ # 业务服务
│ │ └── datasource/ # 数据源实现
│ ├── app.ts # 应用入口
│ └── index.ts # 导出模块
├── python_service/ # Python TQAPI服务
│ ├── api/ # Python API路由
│ ├── main.py # Python服务入口
│ └── tqapi_service.py # TQAPI服务实现
├── config.json # 配置文件
├── package.json # Node.js依赖
└── tsconfig.json # TypeScript配置
```
## 2. 核心功能
### 2.1 数据源管理
- **TQDataSource**使用Python服务获取天勤量化数据
- **MockDataSource**:提供模拟数据,用于测试
- **DataSourceFactory**:数据源工厂,负责创建和管理数据源实例
### 2.2 Python TQAPI服务
- **功能**与天勤量化SDK交互提供期货市场数据
- **API端点**合约列表、合约详情、K线数据、Tick数据
- **端口配置**:支持动态端口配置
### 2.3 集成服务
- **自动启动**当使用TQSDK数据源时自动启动Python服务
- **端口同步**确保Python服务端口与Node.js连接端口一致
## 3. 关键修改
### 3.1 Python服务管理器修改
**文件**`backend/src/services/datasource/PythonServiceManager.ts`
**修改内容**
- 从配置中读取 `pythonPort`默认8000
- 启动Python服务时传递端口参数
- 确保Python服务与Node.js服务端口一致
**关键代码**
```typescript
// 从配置中读取端口默认8000
const port = config.dataSource?.tqsdk?.pythonPort || 8000;
// 启动Python服务传递端口参数
this.pythonProcess = spawn('python', [pythonServicePath, '--port', port.toString()], {
cwd: path.join(__dirname, '../../../python_service'),
stdio: 'inherit',
shell: true
});
```
### 3.2 Python服务入口修改
**文件**`backend/python_service/main.py`
**修改内容**
- 添加命令行参数解析
- 支持通过 `--port` 参数设置服务端口
- 默认端口为8000
**关键代码**
```python
# 解析命令行参数
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)
```
### 3.3 TQDataSource修改
**文件**`backend/src/services/datasource/TQDataSource.ts`
**修改内容**
- 从配置中读取 `pythonPort`默认8000
- 动态构建Python服务URL使用配置的端口
- 确保与Python服务端口一致
**关键代码**
```typescript
// 从配置中读取端口默认8000
const port = config.pythonPort || 8000;
this.config = {
// 其他配置...
pythonServiceUrl: config.pythonServiceUrl || `http://127.0.0.1:${port}/api`
};
```
### 3.4 应用启动逻辑修改
**文件**`backend/src/app.ts`
**修改内容**
- 在启动时检查配置自动启动Python服务
- 确保服务启动顺序正确
**关键代码**
```typescript
// 启动前检查是否需要启动Python TQAPI服务
async function startServer() {
try {
// 检查配置是否使用TQSDK数据源
if (config.dataSource?.defaultDataSource === 'tqsdk' ||
(config.dataSource?.tqsdk && config.dataSource.tqsdk.enabled)) {
console.log('检测到使用TQSDK数据源准备启动Python服务...');
const pythonStarted = await pythonServiceManager.start();
if (!pythonStarted) {
console.warn('Python服务启动失败可能会影响TQSDK数据获取');
}
}
// 启动Express服务器
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});
} catch (error) {
console.error('启动服务器时出错:', error);
}
}
```
## 4. 配置说明
### 4.1 配置文件结构
**文件**`config.json`
```json
{
"server": {
"port": 3007
},
"dataSource": {
"tqsdk": {
"enabled": true,
"username": "你的天勤账号",
"password": "你的天勤密码",
"timeout": 30000,
"retries": 3,
"maxConnections": 5,
"pythonPort": 8000
},
"defaultDataSource": "tqsdk"
}
}
```
### 4.2 关键配置项
| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `server.port` | number | 3007 | Node.js服务端口 |
| `dataSource.tqsdk.enabled` | boolean | true | 是否启用TQSDK数据源 |
| `dataSource.tqsdk.username` | string | "" | 天勤量化账号 |
| `dataSource.tqsdk.password` | string | "" | 天勤量化密码 |
| `dataSource.tqsdk.pythonPort` | number | 8000 | Python服务端口 |
| `dataSource.defaultDataSource` | string | "tqsdk" | 默认数据源 |
## 5. 启动方法
### 5.1 依赖安装
#### Node.js 依赖
`backend` 目录下执行:
```bash
npm install
```
#### Python 依赖
`backend/python_service` 目录下执行:
```bash
pip install -r requirements.txt
```
### 5.2 启动服务
#### 方法一:使用启动脚本
执行 `start_services.bat` 脚本:
```bash
./start_services.bat
```
#### 方法二:手动启动
1. **启动 Node.js 服务**
```bash
cd backend
npm run dev
```
2. **启动 Python 服务**(自动启动,无需手动):
- 当使用TQSDK数据源时Node.js服务会自动启动Python服务
### 5.3 服务验证
- **Node.js 服务**:访问 http://localhost:3007/health
- **Python TQAPI 服务**:访问 http://localhost:8000/health
## 6. 注意事项
### 6.1 端口配置
- **端口一致性**:确保 `config.json` 中的 `pythonPort` 与 Python 服务实际使用的端口一致
- **端口冲突**:避免使用已被其他程序占用的端口
- **默认端口**:如果未配置 `pythonPort`默认使用8000
### 6.2 依赖管理
- **Python版本**:建议使用 Python 3.7+
- **Node.js版本**:建议使用 Node.js 16+
- **依赖更新**:定期更新依赖包,确保安全性和稳定性
### 6.3 数据源配置
- **天勤账号**:确保天勤量化账号正确且已激活
- **网络连接**:确保网络连接正常,能够访问天勤量化服务器
- **备用方案**当TQSDK数据源不可用时可切换到MockDataSource
### 6.4 错误处理
- **Python服务启动失败**检查Python是否安装依赖是否完整
- **TQSDK连接失败**:检查网络连接,账号密码是否正确
- **数据获取失败**:检查合约代码是否正确,网络连接是否稳定
### 6.5 性能优化
- **连接池**:合理设置 `maxConnections`,避免连接过多
- **超时设置**:设置合理的 `timeout`,避免请求等待时间过长
- **重试机制**:通过 `retries` 配置,提高数据获取的可靠性
## 7. 故障排查
### 7.1 常见问题
#### 问题1Python服务启动失败
**症状**
- Node.js服务启动时提示Python服务启动失败
- 日志显示Python相关错误
**解决方案**
- 检查Python是否安装`python --version`
- 检查Python依赖`pip install -r requirements.txt`
- 检查端口是否被占用:`netstat -ano | findstr :8000`
#### 问题2TQSDK连接失败
**症状**
- 日志显示"连接到天勤服务器失败"
- 数据获取API返回错误
**解决方案**
- 检查天勤账号密码是否正确
- 检查网络连接是否正常
- 尝试手动登录天勤量化客户端,确认账号状态
#### 问题3端口不一致
**症状**
- 日志显示"fetch failed"错误
- 提示连接到错误端口
**解决方案**
- 检查 `config.json` 中的 `pythonPort` 配置
- 确保Python服务实际使用的端口与配置一致
- 重启服务,确保配置生效
### 7.2 日志查看
- **Node.js日志**在启动Node.js服务的终端中查看
- **Python日志**在启动Python服务的终端中查看
- **API日志**通过访问API端点查看返回结果和错误信息
## 8. 开发建议
### 8.1 代码规范
- **TypeScript**使用TypeScript编写Node.js代码提高代码质量
- **ESLint**使用ESLint检查代码风格确保代码一致性
- **注释**:为关键代码添加注释,提高可维护性
### 8.2 测试策略
- **单元测试**:为核心功能编写单元测试
- **集成测试**:测试不同组件之间的集成
- **Mock数据**使用MockDataSource进行测试避免依赖外部服务
### 8.3 部署建议
- **环境变量**:使用环境变量存储敏感信息
- **配置管理**:使用配置文件管理不同环境的配置
- **监控告警**:添加监控和告警机制,及时发现问题
### 8.4 扩展性
- **模块化**:保持代码模块化,便于扩展和维护
- **接口设计**:设计清晰的接口,便于添加新功能
- **数据源扩展**通过DataSourceFactory可以方便地添加新的数据源实现
## 9. 总结
本开发文档提供了AI期货分析系统后端服务的详细说明包括
1. **项目结构**:清晰的目录结构和文件组织
2. **核心功能**数据源管理、Python TQAPI服务和集成服务
3. **关键修改**:端口配置、服务集成和启动逻辑
4. **配置说明**:详细的配置项和默认值
5. **启动方法**:多种启动方式和服务验证
6. **注意事项**:端口配置、依赖管理和错误处理
7. **故障排查**:常见问题和解决方案
8. **开发建议**:代码规范、测试策略和部署建议
通过本文档,开发人员可以快速了解系统架构,掌握开发和部署流程,确保系统的稳定性和可靠性。

@ -104,7 +104,8 @@ const AdminConfig = () => {
password: '',
timeout: 30000,
retries: 3,
maxConnections: 5
maxConnections: 5,
pythonPort: 8000
},
// Wind
wind: {
@ -200,7 +201,7 @@ const AdminConfig = () => {
},
dataSource: newConfig.dataSource || {
test: { enabled: true, timeout: 10000, retries: 3, refreshInterval: 60000 },
tqsdk: { enabled: true, username: '', password: '', timeout: 30000, retries: 3, maxConnections: 5 },
tqsdk: { enabled: true, username: '', password: '', timeout: 30000, retries: 3, maxConnections: 5, pythonPort: 3007 },
wind: { enabled: false, apiKey: '', apiSecret: '', url: 'https://api.wind.com.cn', timeout: 30000, retries: 3 },
sina: { enabled: false, url: 'https://finance.sina.com.cn', timeout: 10000, retries: 3, refreshInterval: 60000 },
defaultDataSource: 'tqsdk'
@ -276,6 +277,7 @@ const AdminConfig = () => {
'dataSource.tqsdk.timeout': completeConfig.dataSource.tqsdk.timeout,
'dataSource.tqsdk.retries': completeConfig.dataSource.tqsdk.retries,
'dataSource.tqsdk.maxConnections': completeConfig.dataSource.tqsdk.maxConnections,
'dataSource.tqsdk.pythonPort': completeConfig.dataSource.tqsdk.pythonPort || 8000,
'dataSource.wind.enabled': completeConfig.dataSource.wind.enabled,
'dataSource.wind.apiKey': completeConfig.dataSource.wind.apiKey,
'dataSource.wind.apiSecret': completeConfig.dataSource.wind.apiSecret,
@ -1084,6 +1086,17 @@ const AdminConfig = () => {
/>
</Item>
</Col>
<Col span={6}>
<Item label="Python服务端口" name="dataSource.tqsdk.pythonPort">
<InputNumber
min={1}
max={65535}
step={1}
onChange={(value) => handleDataSourceConfigChange('tqsdk', 'pythonPort', value)}
/>
</Item>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
<Button
@ -1102,6 +1115,7 @@ const AdminConfig = () => {
handleDataSourceConfigChange('tqsdk', 'timeout', 30000);
handleDataSourceConfigChange('tqsdk', 'retries', 3);
handleDataSourceConfigChange('tqsdk', 'maxConnections', 5);
handleDataSourceConfigChange('tqsdk', 'pythonPort', 3007);
}}
>
恢复默认

@ -22,12 +22,28 @@ const Detail = () => {
console.log('Detail page loaded with code:', code);
useEffect(() => {
//
setTimeout(() => {
const futureData = generateFutureData(code, '测试品种');
// API
const fetchFutureDetail = async () => {
try {
console.log('Fetching detail for code:', code);
const response = await fetch(`http://localhost:3007/api/market/detail/${code}`);
if (!response.ok) {
throw new Error('获取品种详情失败');
}
const result = await response.json();
console.log('Received detail data:', result);
setData(result.data);
setLoading(false);
} catch (error) {
console.error('获取品种详情失败:', error);
// 使
const futureData = generateFutureData(code, code); // 使code
setData(futureData);
setLoading(false);
}, 500);
}
};
fetchFutureDetail();
}, [code]);
// K线
@ -42,12 +58,22 @@ const Detail = () => {
// APIK线
const fetchKlineData = async () => {
try {
const response = await fetch(`http://localhost:3002/api/market/klines/${code}?period=${currentPeriod}`);
console.log('Fetching Kline data for code:', code, 'period:', currentPeriod);
const response = await fetch(`http://localhost:3007/api/market/klines/${code}?period=${currentPeriod}`);
if (!response.ok) {
throw new Error('获取K线数据失败');
}
const result = await response.json();
console.log('Received Kline data:', result);
//
if (result.data && Array.isArray(result.data) && result.data.length > 0) {
return result.data;
} else {
console.warn('Kline data is empty or invalid, using mock data');
// 使
return generateKlineData(30);
}
} catch (error) {
console.error('获取K线数据失败:', error);
// 使
@ -56,6 +82,9 @@ const Detail = () => {
};
fetchKlineData().then(klineData => {
// chartRef.current
if (!chartRef.current) return;
//
const chart = createChart(chartRef.current, {
width: chartRef.current.clientWidth,
@ -74,7 +103,12 @@ const Detail = () => {
},
});
// chart
console.log('Chart object:', chart);
console.log('Chart methods:', Object.keys(chart));
// K线
try {
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#52c41a',
downColor: '#ff4d4f',
@ -85,7 +119,7 @@ const Detail = () => {
// K线
candlestickSeries.setData(klineData.map(item => ({
time: new Date(item.time * 1000).toISOString().split('T')[0],
time: new Date(item.timestamp * 1000).toISOString().split('T')[0],
open: item.open,
high: item.high,
low: item.low,
@ -104,7 +138,7 @@ const Detail = () => {
for (let i = 4; i < klineData.length; i++) {
const sum = klineData.slice(i - 4, i + 1).reduce((acc, item) => acc + item.close, 0);
ma5Data.push({
time: new Date(klineData[i].time * 1000).toISOString().split('T')[0],
time: new Date(klineData[i].timestamp * 1000).toISOString().split('T')[0],
value: sum / 5,
});
}
@ -120,12 +154,24 @@ const Detail = () => {
for (let i = 9; i < klineData.length; i++) {
const sum = klineData.slice(i - 9, i + 1).reduce((acc, item) => acc + item.close, 0);
ma10Data.push({
time: new Date(klineData[i].time * 1000).toISOString().split('T')[0],
time: new Date(klineData[i].timestamp * 1000).toISOString().split('T')[0],
value: sum / 10,
});
}
ma10Series.setData(ma10Data);
}
} catch (error) {
console.error('Error creating chart series:', error);
// 使线
const lineSeries = chart.addLineSeries({
color: '#1890ff',
lineWidth: 1,
});
lineSeries.setData(klineData.map(item => ({
time: new Date(item.timestamp * 1000).toISOString().split('T')[0],
value: item.close,
})));
}
//
chartInstance.current = chart;

@ -0,0 +1,16 @@
@echo off
REM 启动Node.js后端服务
start "Node.js Backend" cmd /k "cd backend && npm run dev"
REM 等待2秒确保Node.js服务有时间启动
timeout /t 2 /nobreak >nul
REM 启动Python TQAPI服务
start "Python TQAPI Service" cmd /k "cd backend/python_service && python main.py"
echo 服务启动完成!
echo Node.js后端服务运行在 http://localhost:3007
echo Python TQAPI服务运行在 http://localhost:8000
echo 按任意键关闭此窗口...
pause >nul
Loading…
Cancel
Save