diff --git a/backend/.vscode/launch.json b/backend/.vscode/launch.json new file mode 100644 index 0000000..e9397a2 --- /dev/null +++ b/backend/.vscode/launch.json @@ -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" + } + ] +} diff --git a/backend/package-lock.json b/backend/package-lock.json index 86b1f86..2108ca9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index bbaaf59..3faf7a3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/python_service/README.md b/backend/python_service/README.md new file mode 100644 index 0000000..f0def1a --- /dev/null +++ b/backend/python_service/README.md @@ -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**:与天勤量化服务交互 +- **异步处理**:提高并发性能 +- **错误处理**:确保服务稳定性 diff --git a/backend/python_service/__pycache__/main.cpython-311.pyc b/backend/python_service/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..ee6831e Binary files /dev/null and b/backend/python_service/__pycache__/main.cpython-311.pyc differ diff --git a/backend/python_service/__pycache__/schemas.cpython-311.pyc b/backend/python_service/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000..7e8dd29 Binary files /dev/null and b/backend/python_service/__pycache__/schemas.cpython-311.pyc differ diff --git a/backend/python_service/__pycache__/tqapi_service.cpython-311.pyc b/backend/python_service/__pycache__/tqapi_service.cpython-311.pyc new file mode 100644 index 0000000..0a07339 Binary files /dev/null and b/backend/python_service/__pycache__/tqapi_service.cpython-311.pyc differ diff --git a/backend/python_service/api/__pycache__/router.cpython-311.pyc b/backend/python_service/api/__pycache__/router.cpython-311.pyc new file mode 100644 index 0000000..bf15d35 Binary files /dev/null and b/backend/python_service/api/__pycache__/router.cpython-311.pyc differ diff --git a/backend/python_service/api/router.py b/backend/python_service/api/router.py new file mode 100644 index 0000000..a20368e --- /dev/null +++ b/backend/python_service/api/router.py @@ -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)) diff --git a/backend/python_service/main.py b/backend/python_service/main.py new file mode 100644 index 0000000..a26fb44 --- /dev/null +++ b/backend/python_service/main.py @@ -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) diff --git a/backend/python_service/requirements.txt b/backend/python_service/requirements.txt new file mode 100644 index 0000000..676c041 --- /dev/null +++ b/backend/python_service/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn[standard] +tqsdk +python-dotenv +pydantic-settings diff --git a/backend/python_service/schemas.py b/backend/python_service/schemas.py new file mode 100644 index 0000000..c25c239 --- /dev/null +++ b/backend/python_service/schemas.py @@ -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 diff --git a/backend/python_service/test_connect.py b/backend/python_service/test_connect.py new file mode 100644 index 0000000..3a53930 --- /dev/null +++ b/backend/python_service/test_connect.py @@ -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() diff --git a/backend/python_service/tqapi_service.py b/backend/python_service/tqapi_service.py new file mode 100644 index 0000000..1878ab6 --- /dev/null +++ b/backend/python_service/tqapi_service.py @@ -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小时 diff --git a/backend/src/app.ts b/backend/src/app.ts index 6d5e02d..4b25ed6 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -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; \ No newline at end of file diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e323633..2e508eb 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -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: { diff --git a/backend/src/services/datasource/PythonServiceManager.ts b/backend/src/services/datasource/PythonServiceManager.ts new file mode 100644 index 0000000..f50ffc4 --- /dev/null +++ b/backend/src/services/datasource/PythonServiceManager.ts @@ -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 { + 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(); diff --git a/backend/src/services/datasource/TQDataSource.ts b/backend/src/services/datasource/TQDataSource.ts index 12c6fc2..827db53 100644 --- a/backend/src/services/datasource/TQDataSource.ts +++ b/backend/src/services/datasource/TQDataSource.ts @@ -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(endpoint: string, params?: Record): Promise { + 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(endpoint: string, data?: any): Promise { + 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 { try { - console.log('开始初始化TQSDK数据源...'); - - // 创建TQSDK实例,使用配置的参数 - console.log('创建TQSDK实例...'); - - // 初始化TQSDK,传入WebSocket对象 - this.tq = new TQSDK({ autoInit: true }, { WebSocket }); - - console.log('TQSDK实例创建成功,等待就绪...'); - - // 等待初始化完成,设置超时 - const timeout = this.config.timeout || 10000; - console.log('等待TQSDK连接,超时时间:', timeout, 'ms'); - - await new Promise((resolve, reject) => { - // 设置超时 - const timeoutId = setTimeout(() => { - console.error('TQSDK连接超时'); - reject(new Error('TQSDK连接超时')); - }, timeout); - - // 监听就绪事件 - this.tq?.on('ready', () => { - console.log('TQSDK就绪'); - clearTimeout(timeoutId); - resolve(true); - }); - - // 监听错误事件 - this.tq?.on('error', (error: any) => { - console.error('TQSDK错误:', error); - clearTimeout(timeoutId); - reject(error); - }); + console.log('开始初始化TQAPI数据源...'); + console.log('Python服务URL:', this.config.pythonServiceUrl); + console.log('连接参数:', { + username: this.config.username ? '***' : '', + password: this.config.password ? '***' : '' }); - this.initialized = true; - console.log('TQSDK数据源初始化成功'); - return true; - } catch (error) { - console.error('TQSDK数据源初始化失败:', error); - // 清理资源 - if (this.tq) { + // 先测试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 { - if (typeof this.tq.close === 'function') { - this.tq.close(); + const testResponse = await fetch(testUrl, { + method: 'GET', + signal: controller.signal + }); + console.log('Python服务健康检查响应:', testResponse.status); + if (testResponse.ok) { + console.log('Python服务可达'); } else { - console.log('TqApi实例没有close方法'); + console.error('Python服务不可达,状态码:', testResponse.status); } - } catch (closeError) { - console.error('关闭TQSDK连接失败:', closeError); + } finally { + clearTimeout(timeoutId); } - this.tq = null; + } catch (error: any) { + console.error('Python服务健康检查失败:', error.message || error); } + + // 连接到天勤服务器 + console.log('连接到天勤服务器...'); + + try { + const response = await this.httpClient.post('/connect', { + username: this.config.username, + password: this.config.password + }); + + console.log('连接响应:', response); + + if (response.success) { + console.log('TQAPI连接成功'); + this.initialized = true; + return true; + } else { + console.error('TQAPI连接失败:', response.message); + this.initialized = false; + return false; + } + } 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 { - 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 - }); - - // 转换合约格式 - const futuresContracts = contracts.map((symbol: string) => { - try { - const quote = this.tq.getQuote(symbol); - return { - symbol: symbol, - name: quote.instrument_name, - exchange: symbol.split('.')[0] - }; - } catch (error) { - console.error(`获取合约${symbol}信息失败:`, error); - return null; - } - }).filter((contract: any) => contract !== null); + console.log('获取合约列表...'); + const response = await this.httpClient.get('/contracts'); - return futuresContracts; + if (response.success && Array.isArray(response.data)) { + console.log('获取合约列表成功,数量:', response.data.length); + return response.data; + } else { + console.error('获取合约列表失败:', response.message); + return []; + } } catch (error) { console.error('获取合约列表失败:', error); throw error; @@ -126,23 +230,21 @@ export class TQDataSource implements DataSource { } async getContractDetail(symbol: string): Promise { - 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(`/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 { - 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('/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 { - 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(`/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 { - 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 { - 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 { - 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('TQSDK连接已关闭'); - } catch (error) { - console.error('关闭TQSDK连接失败:', error); + try { + console.log('关闭TQAPI连接...'); + const response = await this.httpClient.post('/disconnect'); + + if (response.success) { + console.log('TQAPI连接已关闭'); + } else { + console.error('关闭TQAPI连接失败:', response.message); } - this.tq = null; + } catch (error) { + 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数据源已关闭'); } } } \ No newline at end of file diff --git a/backend/src/services/datasource/TQDataSourcePython.md b/backend/src/services/datasource/TQDataSourcePython.md new file mode 100644 index 0000000..51202f0 --- /dev/null +++ b/backend/src/services/datasource/TQDataSourcePython.md @@ -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. 需要确保安全性,特别是在处理敏感信息时 \ No newline at end of file diff --git a/backend/src/utils/mockData.ts b/backend/src/utils/mockData.ts index 91bc19d..ead2f25 100644 --- a/backend/src/utils/mockData.ts +++ b/backend/src/utils/mockData.ts @@ -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: '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: '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: '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: '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: '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: '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: 'I', name: '铁矿石', type: '其他', exchange: 'DCE' }, + // { code: 'J', name: '焦炭', type: '其他', exchange: 'DCE' }, + // { code: 'ZC', name: '动力煤', type: '其他', exchange: 'CZCE' } ]; // 生成随机数据的工具函数 diff --git a/backend/test_market_api.js b/backend/test_market_api.js new file mode 100644 index 0000000..a302344 --- /dev/null +++ b/backend/test_market_api.js @@ -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(); \ No newline at end of file diff --git a/backend/test_market_detail.js b/backend/test_market_detail.js new file mode 100644 index 0000000..b348a86 --- /dev/null +++ b/backend/test_market_detail.js @@ -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(); \ No newline at end of file diff --git a/backend/test_python_service.js b/backend/test_python_service.js new file mode 100644 index 0000000..c086de7 --- /dev/null +++ b/backend/test_python_service.js @@ -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(); \ No newline at end of file diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..4bbaf30 --- /dev/null +++ b/config.example.json @@ -0,0 +1,17 @@ +{ + "server": { + "port": 3007 + }, + "dataSource": { + "tqsdk": { + "enabled": true, + "username": "你的天勤账号", + "password": "你的天勤密码", + "timeout": 30000, + "retries": 3, + "maxConnections": 5, + "pythonPort": 8000 + }, + "defaultDataSource": "tqsdk" + } +} diff --git a/config.json b/config.json index 57d16d1..8920097 100644 --- a/config.json +++ b/config.json @@ -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 diff --git a/docs/开发文档/前端开发文档.md b/docs/开发文档/前端开发文档.md new file mode 100644 index 0000000..ece73c8 --- /dev/null +++ b/docs/开发文档/前端开发文档.md @@ -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的智能投资顾问功能 diff --git a/docs/开发文档/后端服务开发文档.md b/docs/开发文档/后端服务开发文档.md new file mode 100644 index 0000000..879d436 --- /dev/null +++ b/docs/开发文档/后端服务开发文档.md @@ -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 常见问题 + +#### 问题1:Python服务启动失败 + +**症状**: +- Node.js服务启动时提示Python服务启动失败 +- 日志显示Python相关错误 + +**解决方案**: +- 检查Python是否安装:`python --version` +- 检查Python依赖:`pip install -r requirements.txt` +- 检查端口是否被占用:`netstat -ano | findstr :8000` + +#### 问题2:TQSDK连接失败 + +**症状**: +- 日志显示"连接到天勤服务器失败" +- 数据获取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. **开发建议**:代码规范、测试策略和部署建议 + +通过本文档,开发人员可以快速了解系统架构,掌握开发和部署流程,确保系统的稳定性和可靠性。 diff --git a/src/pages/admin/AdminConfig.jsx b/src/pages/admin/AdminConfig.jsx index ad3aae6..7e38db7 100644 --- a/src/pages/admin/AdminConfig.jsx +++ b/src/pages/admin/AdminConfig.jsx @@ -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 = () => { /> + + + handleDataSourceConfigChange('tqsdk', 'pythonPort', value)} + /> + +