From 4bafbcdd14189eee0dd4cfc55c562851ff2cb02b Mon Sep 17 00:00:00 2001 From: Lxy Date: Tue, 24 Feb 2026 21:19:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=E4=B8=8D=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/python_service/README.md | 108 ----- .../__pycache__/main.cpython-311.pyc | Bin 1230 -> 0 bytes .../__pycache__/schemas.cpython-311.pyc | Bin 2184 -> 0 bytes .../__pycache__/tqapi_service.cpython-311.pyc | Bin 15258 -> 0 bytes .../api/__pycache__/router.cpython-311.pyc | Bin 6627 -> 0 bytes backend/python_service/api/router.py | 96 ---- backend/python_service/main.py | 61 --- backend/python_service/requirements.txt | 5 - backend/python_service/schemas.py | 26 -- backend/python_service/test_connect.py | 20 - backend/python_service/tqapi_service.py | 329 ------------- .../service/data/futures_analysis.db | Bin 12320768 -> 12320768 bytes backend/src/services/datasource/DataSource.ts | 26 -- .../services/datasource/DataSourceFactory.ts | 70 --- .../src/services/datasource/MockDataSource.ts | 200 -------- .../datasource/PythonServiceManager.ts | 84 ---- .../src/services/datasource/TQDataSource.ts | 439 ------------------ .../services/datasource/TQDataSourcePython.md | 62 --- backend/src/services/marketService.ts | 248 ---------- 19 files changed, 1774 deletions(-) delete mode 100644 backend/python_service/README.md delete mode 100644 backend/python_service/__pycache__/main.cpython-311.pyc delete mode 100644 backend/python_service/__pycache__/schemas.cpython-311.pyc delete mode 100644 backend/python_service/__pycache__/tqapi_service.cpython-311.pyc delete mode 100644 backend/python_service/api/__pycache__/router.cpython-311.pyc delete mode 100644 backend/python_service/api/router.py delete mode 100644 backend/python_service/main.py delete mode 100644 backend/python_service/requirements.txt delete mode 100644 backend/python_service/schemas.py delete mode 100644 backend/python_service/test_connect.py delete mode 100644 backend/python_service/tqapi_service.py delete mode 100644 backend/src/services/datasource/DataSource.ts delete mode 100644 backend/src/services/datasource/DataSourceFactory.ts delete mode 100644 backend/src/services/datasource/MockDataSource.ts delete mode 100644 backend/src/services/datasource/PythonServiceManager.ts delete mode 100644 backend/src/services/datasource/TQDataSource.ts delete mode 100644 backend/src/services/datasource/TQDataSourcePython.md diff --git a/backend/python_service/README.md b/backend/python_service/README.md deleted file mode 100644 index f0def1a..0000000 --- a/backend/python_service/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# Python TQAPI 服务 - -## 功能说明 - -Python TQAPI 服务是 AI 期货分析系统的一个组件,负责与天勤量化(TQSDK)进行交互,为系统提供期货市场数据。 - -## 集成说明 - -### 自动启动 - -Python TQAPI 服务已集成到 Node.js 后端服务中,当满足以下条件时会自动启动: - -1. 配置文件中 `defaultDataSource` 设置为 `tqsdk` -2. 或配置文件中 `tqsdk.enabled` 设置为 `true` - -### 手动启动 - -如果需要手动启动 Python 服务,可以执行以下命令: - -```bash -cd backend/python_service -python main.py -``` - -## 依赖安装 - -### Python 依赖 - -在 `backend/python_service` 目录下执行: - -```bash -pip install -r requirements.txt -``` - -### 依赖包说明 - -- **tqsdk**:天勤量化 SDK,用于获取期货市场数据 -- **fastapi**:高性能的 Python Web 框架,用于提供 API 接口 -- **uvicorn**:ASGI 服务器,用于运行 FastAPI 应用 -- **pandas**:数据分析库,用于处理和分析数据 - -## API 端点 - -| 端点 | 方法 | 功能 | -|------|------|------| -| `/api/connect` | POST | 连接到天勤量化服务 | -| `/api/contracts` | GET | 获取合约列表 | -| `/api/contract/{symbol}` | GET | 获取单个合约详情 | -| `/api/klines/{symbol}` | GET | 获取K线数据 | -| `/api/tick/{symbol}` | GET | 获取Tick数据 | -| `/health` | GET | 健康检查 | - -## 配置说明 - -### 天勤量化账号配置 - -在 `config.json` 文件中配置天勤量化账号: - -```json -{ - "dataSource": { - "tqsdk": { - "enabled": true, - "username": "你的天勤账号", - "password": "你的天勤密码" - } - } -} -``` - -### 服务端口配置 - -服务默认运行在端口 8000,可以在 `main.py` 文件中修改: - -```python -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) # 修改端口号 -``` - -## 故障排查 - -### 常见问题 - -1. **Python 服务启动失败** - - 检查 Python 是否安装 - - 检查依赖包是否安装完整 - - 检查天勤量化账号是否正确 - -2. **连接天勤服务器失败** - - 检查网络连接 - - 检查天勤量化账号密码是否正确 - - 检查天勤量化服务是否正常 - -3. **数据获取失败** - - 检查合约代码是否正确 - - 检查网络连接 - - 检查天勤量化服务状态 - -### 日志查看 - -Python 服务的日志会输出到 Node.js 后端服务的控制台中,可以通过查看控制台日志来排查问题。 - -## 技术实现 - -- **FastAPI**:提供 RESTful API 接口 -- **TqApi**:与天勤量化服务交互 -- **异步处理**:提高并发性能 -- **错误处理**:确保服务稳定性 diff --git a/backend/python_service/__pycache__/main.cpython-311.pyc b/backend/python_service/__pycache__/main.cpython-311.pyc deleted file mode 100644 index ee6831e9b7a8e00a1ae75ba51c5219266a457d57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1230 zcmY)sO>f*p^o{Mc)9kL>>?V!22oXMoIqVh=8~`CIP!Xyqh4i#q$ugd4*2HUDW4j@H zT0x~&e2AR*kPs?TwNNEL#;GD<&8boqL~oADsVCmpb!gtq`<(Zl-pBmWZZ87D$_u}E zzbOC@q*8&XQOcUH4|N3?p$)22V{c)u z0_-AIFf^EPd!g2z=1irhHV9oTDN1|Ic>^O6DOr67B4DPj-c!YwIOi%MYci zMTBwDiIWRgK0?ua|C7(wKlUi~*_Oi@T67+!3FDWi?W>=n&)PcU*FBe|s~CHD=kDRR zcOKsS;qbd3*W}~hzkYxC&F|C3>;3cn^Oh4v;TcfRzZ^n`1 zve5-U8ao%$B;_o=!o$($j=RYMI*PWjy15L|h7xbfT(W)}q?wGlodq^Zx|)ti07N49!rE0Lj0S%+$gJqWuM(d(g?XPToDc^KyRnT*akQ zG=X%FKqnZ0q$o+}IA-U~+plEM*$3+YtPHF|2c>ay^{drey?xR>Al*VCzBV|}1}IdwP(eF~ZsolG1L&GVx&QzG diff --git a/backend/python_service/__pycache__/schemas.cpython-311.pyc b/backend/python_service/__pycache__/schemas.cpython-311.pyc deleted file mode 100644 index 7e8dd2946207b99af1131b37a3a0d0fc663740af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2184 zcmb7EO>Y}T7@qy`dgFMVq_uzq@mX5cmzEO}riv6Og{X;BL6N|UwA${BnpO70omqp- zX)PgC1QNcr+&EN$s7PEY@i!dAA=)d7fLoAz>Z$KLYlFRZpdIf#o|)&F+4q@uc7Cv| z5`psIh{?12wb=F`8-RohS^M8-KuV zwV8-7w9CL9-P=F<`re-p_Kv>&rKY6wlnIx6GS<{=3&ukyU}MUopTGR$`)+Es93ei7 zIBmefrfs9jw$qYr2N7-i*e=`lt+wN53`diE9w603%+m$icET`<9o!UQ+ZkqyDX&JtxmiXg*JrvsRuIQ?y$fS7g`-Y z2a8C+mx^lu$=L6(y4-vB?wKD~`_=kEwfRKZ+8R&=V$tZ0xGJze-GE20Hk!P25=yDk$VEe3$jP1e{qxP|YlAS?i+ z`i&^^`4aXQ5l$hpI?kMJ=?!2` zv}7y;n-yk!UId$yCKthE%pE*?nM?jGs8gLfvD4Tu=IrqmATxJ(b8uL1%Hj(+hJXi0 zTm+bHu~fM&zNz)xuP*lWWq1xGLL>8wE6n!$6e%oU_uSnHXZ%H2w^7UIZOqs#Pp`lv zj(|B8C#S7sCF{w07izMe*}zG&ZM+W56Qyl3Vb)dO3)u`${3Td0f8NgE7y_O?aSq_W zl96mBTV1HhRu()?oE`jAq(HfX$~^Qs vs9ZtikOE}|m3ipBiOLEphZHEQsLVre9hFs74ylvkyt|x_6Xs+Q%j`V{(>m08 diff --git a/backend/python_service/__pycache__/tqapi_service.cpython-311.pyc b/backend/python_service/__pycache__/tqapi_service.cpython-311.pyc deleted file mode 100644 index 25697732c0054649350bbab67322b98181c001ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15258 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 diff --git a/backend/python_service/api/router.py b/backend/python_service/api/router.py deleted file mode 100644 index 0a1f019..0000000 --- a/backend/python_service/api/router.py +++ /dev/null @@ -1,96 +0,0 @@ -from fastapi import APIRouter, HTTPException, Query, Request -from schemas import ConnectRequest, ConnectResponse, ContractResponse, TickResponse, KlineResponse, DisconnectResponse -from tqapi_service import TqApiService - -router = APIRouter() - -# 存储当前连接的用户凭证 -current_credentials = {} - -# 获取当前TqApiService实例 -async def get_current_service(): - if not current_credentials: - raise HTTPException(status_code=401, detail="未连接到天勤服务器") - # 使用当前凭证获取或创建实例 - return await TqApiService.get_instance( - current_credentials['username'], - current_credentials['password'] - ) - -@router.post("/connect", response_model=ConnectResponse) -async def connect(request: ConnectRequest): - """连接到天勤服务器""" - try: - # 存储凭证 - current_credentials['username'] = request.username - current_credentials['password'] = request.password - # 获取或创建TqApiService实例 - service = await TqApiService.get_instance(request.username, request.password) - return ConnectResponse(success=True, message="连接成功") - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/contracts", response_model=ContractResponse) -async def get_contracts(): - """获取合约列表""" - try: - service = await get_current_service() - contracts = await service.get_contracts() - return ContractResponse(success=True, data=contracts) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/contract/{symbol}", response_model=ContractResponse) -async def get_contract(symbol: str): - """获取合约详情""" - try: - service = await get_current_service() - contract = await service.get_contract(symbol) - return ContractResponse(success=True, data=contract) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/klines/{symbol}", response_model=KlineResponse) -async def get_klines( - symbol: str, - period: str = Query(..., description="周期,如 1M, 5M, 1H, 1D"), - count: int = Query(30, description="数据数量") -): - """获取 K 线数据""" - try: - service = await get_current_service() - klines = await service.get_klines(symbol, period, count) - return KlineResponse(success=True, data=klines) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/tick/{symbol}", response_model=TickResponse) -async def get_tick(symbol: str): - """获取 tick 数据""" - try: - print(f"[Router] 接收到获取 tick 数据请求,symbol: {symbol}") - print(f"[Router] 正在获取当前服务实例...") - service = await get_current_service() - print(f"[Router] 获取服务实例成功") - print(f"[Router] 正在调用 service.get_tick({symbol})...") - tick = await service.get_tick(symbol) - print(f"[Router] 获取 tick 数据成功: {tick}") - return TickResponse(success=True, data=tick) - except Exception as e: - print(f"[Router] 获取 tick 数据失败: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.post("/disconnect", response_model=DisconnectResponse) -async def disconnect(): - """断开连接""" - try: - service = await get_current_service() - success = await service.disconnect() - if success: - # 清除凭证 - current_credentials.clear() - return DisconnectResponse(success=True, message="断开成功") - else: - raise HTTPException(status_code=400, detail="断开失败") - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/python_service/main.py b/backend/python_service/main.py deleted file mode 100644 index b79d77c..0000000 --- a/backend/python_service/main.py +++ /dev/null @@ -1,61 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -from api.router import router -import uvicorn -import argparse - -app = FastAPI( - title="TQAPI Service", - description="天勤量化 API 服务", - version="1.0.0" -) - -# 配置 CORS -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # 在生产环境中应该设置具体的域名 - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 注册路由 -app.include_router(router, prefix="/api") - -# 健康检查 -@app.get("/health") -async def health_check(): - return {"status": "healthy"} - -# 关闭事件处理 -@app.on_event("shutdown") -async def shutdown_event(): - """服务关闭时的清理工作""" - print("正在关闭服务...") - # 断开TQApi连接 - from api.router import current_credentials - from tqapi_service import TqApiService - - if current_credentials: - try: - # 获取当前服务实例并断开连接 - service = await TqApiService.get_instance(current_credentials['username'], current_credentials['password']) - success = await service.disconnect() - if success: - print("TQApi连接已成功关闭") - else: - print("TQApi连接关闭失败") - except Exception as e: - print(f"关闭TQApi连接时出错: {e}") - print("服务已关闭") - -if __name__ == "__main__": - # 解析命令行参数 - parser = argparse.ArgumentParser(description="天勤量化 API 服务") - parser.add_argument("--port", type=int, default=8000, help="服务端口,默认8000") - parser.add_argument("--host", type=str, default="0.0.0.0", help="服务主机,默认0.0.0.0") - args = parser.parse_args() - - # 启动服务 - print(f"启动天勤量化 API 服务,监听 {args.host}:{args.port}") - uvicorn.run(app, host=args.host, port=args.port) diff --git a/backend/python_service/requirements.txt b/backend/python_service/requirements.txt deleted file mode 100644 index 676c041..0000000 --- a/backend/python_service/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -fastapi -uvicorn[standard] -tqsdk -python-dotenv -pydantic-settings diff --git a/backend/python_service/schemas.py b/backend/python_service/schemas.py deleted file mode 100644 index c25c239..0000000 --- a/backend/python_service/schemas.py +++ /dev/null @@ -1,26 +0,0 @@ -from pydantic import BaseModel, Field -from typing import List, Optional, Any - -class ConnectRequest(BaseModel): - username: str = Field(..., description="天勤账号") - password: str = Field(..., description="天勤密码") - -class ConnectResponse(BaseModel): - success: bool - message: str - -class ContractResponse(BaseModel): - success: bool - data: List[Any] | Any - -class TickResponse(BaseModel): - success: bool - data: Any - -class KlineResponse(BaseModel): - success: bool - data: List[Any] - -class DisconnectResponse(BaseModel): - success: bool - message: str diff --git a/backend/python_service/test_connect.py b/backend/python_service/test_connect.py deleted file mode 100644 index 3a53930..0000000 --- a/backend/python_service/test_connect.py +++ /dev/null @@ -1,20 +0,0 @@ -import requests -import json - -# 测试连接到天勤服务器 -def test_connect(): - url = "http://127.0.0.1:8001/api/connect" - data = { - "username": "windsdreamer", - "password": "1qazse42W3" - } - - try: - response = requests.post(url, json=data, timeout=10) - print(f"Status code: {response.status_code}") - print(f"Response: {response.json()}") - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - test_connect() diff --git a/backend/python_service/tqapi_service.py b/backend/python_service/tqapi_service.py deleted file mode 100644 index 3b780e8..0000000 --- a/backend/python_service/tqapi_service.py +++ /dev/null @@ -1,329 +0,0 @@ -import asyncio -import datetime -from tqsdk import TqApi, TqAuth -from typing import List, Dict, Any - -class TqApiService: - # 类级缓存,存储用户名密码对应的TqApiService实例 - _instance_cache = {} - - def __init__(self): - self.api = None - self.connected = False - self.username = None - self.password = None - - @classmethod - async def get_instance(cls, username: str, password: str): - """获取或创建TqApiService实例""" - # 生成缓存键 - cache_key = f"{username}:{password}" - - # 检查缓存中是否存在实例 - if cache_key in cls._instance_cache: - instance = cls._instance_cache[cache_key] - # 检查实例是否仍然连接 - if instance.connected: - print(f"使用缓存的TqApiService实例 for {username}") - return instance - else: - # 实例已断开连接,从缓存中移除 - print(f"TqApiService实例 for {username} 已断开连接,从缓存中移除") - del cls._instance_cache[cache_key] - - # 创建新实例 - print(f"创建新的TqApiService实例 for {username}") - instance = cls() - instance.username = username - instance.password = password - # 连接到天勤服务器 - await instance.connect(username, password) - # 存入缓存 - cls._instance_cache[cache_key] = instance - return instance - - async def connect(self, username: str, password: str) -> bool: - """连接到天勤服务器""" - try: - # 立即返回,不等待TqApi实例完全连接到天勤服务器 - # 连接过程将在后台进行 - self.username = username - self.password = password - self.connected = True - # 创建后台任务来初始化API实例和运行事件循环 - asyncio.create_task(self._initialize_api()) - return True - except Exception as e: - print(f"连接失败: {e}") - self.connected = False - return False - - async def _initialize_api(self): - """初始化TqApi实例并运行事件循环""" - try: - print("正在初始化TqApi实例...") - # 创建TqApi实例 - self.api = TqApi(auth=TqAuth(self.username, self.password), debug=False) - print("TqApi实例初始化完成") - # 运行事件循环 - await self._run_api() - except Exception as e: - print(f"初始化TqApi实例失败: {e}") - self.connected = False - - async def _run_api(self): - """运行TQAPI事件循环""" - try: - while self.connected and self.api: - try: - # 非阻塞方式检查更新 - self.api.wait_update(0.1) # 超时0.1秒,避免完全阻塞 - except Exception as e: - print(f"API更新出错: {e}") - await asyncio.sleep(0.1) - # 让出CPU时间,避免阻塞其他任务 - await asyncio.sleep(0.01) - except Exception as e: - print(f"API运行出错: {e}") - self.connected = False - - async def get_contracts(self) -> List[Dict[str, Any]]: - """获取合约列表""" - if not self.connected: - raise Exception("未连接到天勤服务器") - - if not self.api: - # API实例尚未初始化,返回空列表 - print("API实例尚未初始化,返回空合约列表") - return [] - - try: - # 获取所有合约 - contracts = self.api.get_contracts() - # 过滤期货合约,排除期权等其他类型 - futures_contracts = [] - for symbol, contract in contracts.items(): - # 只保留期货合约 - if contract["product_class"] == "FUTURE": - futures_contracts.append({ - "symbol": symbol, - "name": contract["name"], - "exchange": contract["exchange"], - "product_id": contract["product_id"], - "price_tick": contract["price_tick"], - "volume_multiple": contract["volume_multiple"], - "margin_rate": contract["margin_rate"], - "expire_datetime": contract["expire_datetime"] - }) - return futures_contracts - except Exception as e: - print(f"获取合约列表失败: {e}") - # 返回空列表,避免整个请求失败 - return [] - - async def get_contract(self, symbol: str) -> Dict[str, Any]: - """获取合约详情""" - if not self.connected: - raise Exception("未连接到天勤服务器") - - if not self.api: - # API实例尚未初始化,返回默认值 - print("API实例尚未初始化,返回默认合约详情") - return { - "symbol": symbol, - "name": "", - "exchange": "", - "product_id": "", - "price_tick": 0, - "volume_multiple": 0, - "margin_rate": 0, - "expire_datetime": 0 - } - - try: - # 获取合约详情 - contract = self.api.get_contract(symbol) - return { - "symbol": symbol, - "name": contract["name"], - "exchange": contract["exchange"], - "product_id": contract["product_id"], - "price_tick": contract["price_tick"], - "volume_multiple": contract["volume_multiple"], - "margin_rate": contract["margin_rate"], - "expire_datetime": contract["expire_datetime"] - } - except Exception as e: - print(f"获取合约详情失败: {e}") - # 返回默认值,避免整个请求失败 - return { - "symbol": symbol, - "name": "", - "exchange": "", - "product_id": "", - "price_tick": 0, - "volume_multiple": 0, - "margin_rate": 0, - "expire_datetime": 0 - } - - async def get_klines(self, symbol: str, period: str, count: int) -> List[Dict[str, Any]]: - """获取K线数据""" - if not self.connected: - raise Exception("未连接到天勤服务器") - - if not self.api: - # API实例尚未初始化,返回空列表 - print("API实例尚未初始化,返回空K线数据列表") - return [] - - try: - # 转换周期格式 - duration_seconds = self._convert_period(period) - # 获取K线数据 - klines = self.api.get_kline_serial(symbol, duration_seconds, data_length=count) - # 转换为前端需要的格式 - result = [] - for i in range(len(klines)): - result.append({ - "time": int(klines.iloc[i]["datetime"].timestamp()), - "open": float(klines.iloc[i]["open"]), - "high": float(klines.iloc[i]["high"]), - "low": float(klines.iloc[i]["low"]), - "close": float(klines.iloc[i]["close"]), - "volume": float(klines.iloc[i]["volume"]) - }) - return result - except Exception as e: - print(f"获取K线数据失败: {e}") - # 返回空列表,避免整个请求失败 - return [] - - async def get_tick(self, symbol: str) -> Dict[str, Any]: - """获取tick数据""" - print(f"[TqApiService] 开始获取 tick 数据,symbol: {symbol}") - try: - print(f"[TqApiService] 检查连接状态: {self.connected}") - if not self.connected: - # 未连接到天勤服务器,返回默认数据 - print("[TqApiService] 未连接到天勤服务器,返回默认tick数据") - return { - "last_price": 0, - "pre_close": 0, - "open": 0, - "high": 0, - "low": 0, - "volume": 0, - "open_interest": 0, - "bid_price1": 0, - "bid_volume1": 0, - "ask_price1": 0, - "ask_volume1": 0, - "datetime": int(datetime.datetime.now().timestamp()) - } - - print(f"[TqApiService] 检查API实例状态: {self.api is not None}") - if not self.api: - # API实例尚未初始化,返回默认数据 - print("[TqApiService] API实例尚未初始化,返回默认tick数据") - return { - "last_price": 0, - "pre_close": 0, - "open": 0, - "high": 0, - "low": 0, - "volume": 0, - "open_interest": 0, - "bid_price1": 0, - "bid_volume1": 0, - "ask_price1": 0, - "ask_volume1": 0, - "datetime": int(datetime.datetime.now().timestamp()) - } - - # 获取实时行情 - print(f"[TqApiService] 正在调用 self.api.get_quote({symbol})...") - quote = self.api.get_quote(symbol) - print(f"[TqApiService] 获取 quote 对象成功") - - # 尝试多次获取行情更新,避免单次超时 - max_attempts = 3 - print(f"[TqApiService] 开始尝试获取行情更新,最多 {max_attempts} 次") - for attempt in range(max_attempts): - try: - # 等待行情更新,设置超时 - print(f"[TqApiService] 尝试 {attempt + 1}/{max_attempts}: 调用 self.api.wait_update(1.0)...") - updated = self.api.wait_update(1.0) # 1秒超时 - print(f"[TqApiService] 尝试 {attempt + 1}/{max_attempts}: wait_update 返回: {updated}") - if updated: - print(f"[TqApiService] 获取行情更新成功") - break - print(f"[TqApiService] 尝试 {attempt + 1}/{max_attempts}: 获取 {symbol} 的行情信息超时") - except Exception as e: - print(f"[TqApiService] 尝试 {attempt + 1}/{max_attempts}: 获取 {symbol} 的行情信息出错: {e}") - await asyncio.sleep(0.5) - - # 即使没有更新,也尝试返回当前数据 - print(f"[TqApiService] 正在构建返回数据...") - result = { - "last_price": float(quote.get("last_price", 0)), - "pre_close": float(quote.get("pre_close", 0)), - "open": float(quote.get("open", 0)), - "high": float(quote.get("high", 0)), - "low": float(quote.get("low", 0)), - "volume": float(quote.get("volume", 0)), - "open_interest": float(quote.get("open_interest", 0)), - "bid_price1": float(quote.get("bid_price1", 0)), - "bid_volume1": float(quote.get("bid_volume1", 0)), - "ask_price1": float(quote.get("ask_price1", 0)), - "ask_volume1": float(quote.get("ask_volume1", 0)), - "datetime": int(quote.get("datetime", datetime.datetime.now()).timestamp()) - } - print(f"[TqApiService] 获取 tick 数据成功,返回: {result}") - return result - except Exception as e: - print(f"[TqApiService] 获取tick数据失败: {e}") - import traceback - traceback.print_exc() - # 返回默认数据,避免整个请求失败 - default_result = { - "last_price": 0, - "pre_close": 0, - "open": 0, - "high": 0, - "low": 0, - "volume": 0, - "open_interest": 0, - "bid_price1": 0, - "bid_volume1": 0, - "ask_price1": 0, - "ask_volume1": 0, - "datetime": int(datetime.datetime.now().timestamp()) - } - print(f"[TqApiService] 返回默认数据: {default_result}") - return default_result - - async def disconnect(self) -> bool: - """断开连接""" - try: - if self.api: - self.connected = False - self.api.close() - self.api = None - return True - except Exception as e: - print(f"断开连接失败: {e}") - return False - - def _convert_period(self, period: str) -> int: - """转换周期格式为秒""" - period_map = { - "1M": 60, - "5M": 300, - "15M": 900, - "30M": 1800, - "1H": 3600, - "4H": 14400, - "1D": 86400 - } - return period_map.get(period, 3600) # 默认1小时 diff --git a/backend/service_implementation/service/data/futures_analysis.db b/backend/service_implementation/service/data/futures_analysis.db index 6fc43b7d1a821e3bd0773532d0197ddd654b7859..4f43464f15f00494dd3e1373ab0ce6a971df99bb 100644 GIT binary patch delta 2102 zcmbW1Yiv_x7{~jbb#2Eg?YNH(x9NZhWBd2A(GG=jQ?3dqh}^`1<1Rzx4!VN=xk$?< zgBNk)1e7U?j7wXsAHWbkm}pQighdT162XOF$N)b;e9zfrB&&%r>F*@x-{<$f&v|p6 zQ&Dj(xk6EyAskhiLxrP$=JG=A+SzUA3ww#%>p%!XBa#R+kxWk=jqBGHj=t^`W(uwXw29ZhhAnqr!h-@N6a9z+qL5JixCl4lA-qHpQB3qFe1xAUAqEf+6ORxBi9tkw7)%Tyh7!Yw;l!iF zW5ftzBr%E@O^hMN5|0y45aWpP#026=;wfSxF^PDZm`prFOd+Nc(}?NB4B}a0Ch;6m zO3Wf=6VDT6#2jKSF^`x}EFcyVi-;G9#Y8!=gm{s7iFlb|9NkBzdQkBh_NslwvYjWUxW~S37Bt zdBg378pS(`-HLY=wTkx?dlY*W`xNghK2Ype)G0nxe55#__*n6Y;-KPF#UVw#;;`aK z^mfBhYm>Q5)S0C7(4Lq)o_(mZ{QmT@GxDr0k|_u6jVH@Rj!194-CP-SL`FoXeRr&` zpLv$)j7g5GjZWDjyuc*u3zBAHGOHP-6 zX2;WId11^V%X5!ftOw_azf6|Fa{O8KxZ~_Gv(=>^P~)%qZt2YljN}(tFCsl;sfb9k zBhQp6zfE;^(NNM_C}EE!YmHa#CMjCu<-0d#Yq7D;@2!cY$VlYi9ckU<@%3qz=$Bfg z%x*TBOu4d3M7l}msKXXH$`Q#(b$Lo$&5bm+E@1T=3;c0U;x&>>_aq)8`R$&>Z6p`z zjL;dSGe+lUbbcOj#?7_%aayG*DNVbi#kI<)?NGczla-|dx*F}tZyIXa%LxNFw%pDr z@u$ihEwW5r?chj_&OQ>a%}jFGJ4$Qzfvo5Wr&Sa3w9|V1|6Nuk1%hkklAq$aSw;SW z;$oi{emC63{owBB_GUL9NRdAfT+>qIDS*fCaeLbqc>=-JEk*7EPk)!!lc2~Q2(D@_ z^12F8nZzECZvTAjZaw%49j@u^Ys@?guRBAt@~F+lP|G0zg5#Gz?>(-b`c&Z zLbuccrpuzKICv;JS4(tAYapP!WO^!bq&o+*uuRc{GG}#c1>q&9r=YO&47sPwWvG$lXU!Ob? z+HL%X>3R=wGb6DzyS2r9eLO{|T7*uE&@x?~ByOHctj)Sd(?47yR4PJsMyJ(UnYa~6 jtd+KEdHP31ggS`OX0hdh@>5QTJG&BS(W)!)#hw2K6{qqN delta 1142 zcmbu*>r>Qq6vy%3Z+F=Rf!!abs9Xvau_EP@mk6@7%x;o)wbD{j(=1cyB1E@E@@-jO zK<(RwiZqfg5)14u^qXd^hs{)qHcU(xW}0T29#k{sI6ZKtFZmzTnR%TD=ggUN=EUQF zWW=p~g>JaDXt5hk)HYNEelFVoZAF=TX1kD!0w{<=C9F&WqD26mme=g|%X z+KFC3FQQ%OCG;}djrO3uXdh}tZD>C_fDWQpkU_7a*U;1dI;?lBL)KyIU8~c2 z&pKjtS?^mPSRYy+Sx2pI>tn0ON?5&CU;6Awf8=y%U0{M^yq-e?=2%HjEMtee(ycJ3 zjwerrVs%cbX?0VRb2PVF3U<%4mHvM#rg11a&FuOyIn#^|4~Ek7hmv2<3axQYI;M7X zu-0=9Y>M91prmnO< z9PZv2JK!ucC$hbKQ(RRQHGBK~GLvvq#b&LWGN$ct^(1qwB`<7NS65}5pL+dm=BttR zsA+3yJl9Bn9!PD-8GCT4sfnegnAzq1S>}rq$uj$SXY;xHdkO|JOx{p(DP9UMjW>Wd zi1$g#^Ftk>n&1K_?e+NafI03(`pv#xzu5dXK9-$c-{*G}=Wj!pdef76`cqUHuu|ARjx{pI(5WgzH{Yc-Kkf9ikS@m;$0SAXlo Fe*lum)ouU) diff --git a/backend/src/services/datasource/DataSource.ts b/backend/src/services/datasource/DataSource.ts deleted file mode 100644 index 2dbf54f..0000000 --- a/backend/src/services/datasource/DataSource.ts +++ /dev/null @@ -1,26 +0,0 @@ -// 数据源抽象接口 -export interface DataSource { - // 获取所有合约列表 - getContractList(): Promise; - - // 获取单个合约详情 - getContractDetail(symbol: string): Promise; - - // 获取K线数据 - getKlineData(symbol: string, period: string, count: number): Promise; - - // 获取实时行情数据 - getTickData(symbol: string): Promise; - - // 获取市场概览 - getMarketOverview(): Promise; - - // 获取历史成交数据 - getHistoricalTrades(symbol: string, start: number, end: number): Promise; - - // 初始化数据源 - initialize(): Promise; - - // 关闭数据源连接 - close(): Promise; -} \ No newline at end of file diff --git a/backend/src/services/datasource/DataSourceFactory.ts b/backend/src/services/datasource/DataSourceFactory.ts deleted file mode 100644 index 218953e..0000000 --- a/backend/src/services/datasource/DataSourceFactory.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 数据源工厂类 -import { DataSource } from './DataSource'; -import { TQDataSource } from './TQDataSource'; -import { logger } from '../../utils/logger'; - -// 数据源类型 -export enum DataSourceType { - TQSDK = 'tqsdk', - MOCK = 'mock', - WIND = 'wind', - SINA = 'sina', - TEST = 'test' -} - -export class DataSourceFactory { - private static dataSources: Map = new Map(); - - // 获取数据源实例 - static async getDataSource(type: DataSourceType = DataSourceType.TQSDK, config: any = {}): Promise { - logger.log('获取数据源实例:', type); - if (!this.dataSources.has(type)) { - let dataSource: DataSource; - - switch (type) { - case DataSourceType.TQSDK: - dataSource = new TQDataSource(config.tqsdk || {}); - break; - case DataSourceType.MOCK: - case DataSourceType.TEST: - // 导入模拟数据源 - const { MockDataSource } = await import('./MockDataSource'); - dataSource = new MockDataSource(); - break; - default: - throw new Error(`不支持的数据源类型: ${type}`); - } - - // 初始化数据源 - const initialized = await dataSource.initialize(); - if (!initialized) { - throw new Error(`数据源${type}初始化失败`); - } - - this.dataSources.set(type, dataSource); - } - - return this.dataSources.get(type)!; - } - - // 关闭所有数据源 - static async closeAllDataSources(): Promise { - for (const [type, dataSource] of this.dataSources) { - try { - await dataSource.close(); - logger.log(`数据源${type}已关闭`); - } catch (error) { - logger.error(`关闭数据源${type}失败:`, error); - } - } - this.dataSources.clear(); - } - - // 切换数据源 - static async switchDataSource(type: DataSourceType): Promise { - // 关闭当前数据源 - await this.closeAllDataSources(); - // 获取新数据源 - return this.getDataSource(type); - } -} \ No newline at end of file diff --git a/backend/src/services/datasource/MockDataSource.ts b/backend/src/services/datasource/MockDataSource.ts deleted file mode 100644 index 2d6b5eb..0000000 --- a/backend/src/services/datasource/MockDataSource.ts +++ /dev/null @@ -1,200 +0,0 @@ -// 模拟数据源实现 -import { DataSource } from './DataSource'; -import { futuresList, generateFuturesOverview, generateFutureData, generateKlineData, riskAlerts } from '../../utils/mockData'; -import { logger } from '../../utils/logger'; - -export class MockDataSource implements DataSource { - private initialized: boolean = false; - - async initialize(): Promise { - // 模拟初始化 - this.initialized = true; - logger.log('模拟数据源初始化成功'); - return true; - } - - async getContractList(): Promise { - if (!this.initialized) { - throw new Error('模拟数据源未初始化'); - } - - // 返回模拟合约列表 - return futuresList.map(item => ({ - symbol: item.code, - name: item.name, - exchange: this.getExchangeBySymbol(item.code), - product_class: 'futures', - price_tick: 0.01, - size: 1, - margin_rate: 0.05 - })); - } - - async getContractDetail(symbol: string): Promise { - if (!this.initialized) { - throw new Error('模拟数据源未初始化'); - } - - // 返回模拟合约详情 - const future = futuresList.find(item => item.code === symbol); - if (!future) { - throw new Error(`合约${symbol}不存在`); - } - - return { - symbol: future.code, - name: future.name, - exchange: this.getExchangeBySymbol(future.code), - product_class: 'futures', - price_tick: 0.01, - size: 1, - margin_rate: 0.05, - delivery_month: '202412', - trading_hours: '09:00-11:30, 13:30-15:00, 21:00-02:30', - pre_close: 2000, - open: 2005, - high: 2010, - low: 1995, - last_price: 2003, - volume: 10000, - open_interest: 50000 - }; - } - - async getKlineData(symbol: string, period: string, count: number): Promise { - if (!this.initialized) { - throw new Error('模拟数据源未初始化'); - } - - // 返回模拟K线数据 - const klineData = generateKlineData(count); - return klineData.map(item => ({ - datetime: item.time * 1000000000, // 转换为纳秒 - open: item.open, - high: item.high, - low: item.low, - close: item.close, - volume: item.volume, - open_interest: Math.floor(Math.random() * 100000) + 50000 - })); - } - - async getTickData(symbol: string): Promise { - if (!this.initialized) { - throw new Error('模拟数据源未初始化'); - } - - // 返回模拟实时行情数据 - const future = futuresList.find(item => item.code === symbol); - if (!future) { - throw new Error(`合约${symbol}不存在`); - } - - const pre_close = 2000; - const last_price = pre_close + (Math.random() * 20 - 10); - const price_change = last_price - pre_close; - - return { - datetime: Date.now() * 1000000, // 转换为纳秒 - last_price: +last_price.toFixed(2), - pre_close, - open: pre_close + (Math.random() * 10 - 5), - high: Math.max(last_price, pre_close + (Math.random() * 15 - 5)), - low: Math.min(last_price, pre_close + (Math.random() * 15 - 10)), - volume: Math.floor(Math.random() * 10000) + 5000, - open_interest: Math.floor(Math.random() * 100000) + 50000, - price_change: +price_change.toFixed(2), - bid_price1: last_price - 0.01, - bid_volume1: Math.floor(Math.random() * 100) + 50, - ask_price1: last_price + 0.01, - ask_volume1: Math.floor(Math.random() * 100) + 50 - }; - } - - async getMarketOverview(): Promise { - if (!this.initialized) { - throw new Error('模拟数据源未初始化'); - } - - // 返回模拟市场概览 - const overview = generateFuturesOverview(); - return overview.map(item => ({ - symbol: item.code, - name: item.name, - price: item.currentPrice, - change: item.changePercent, - change_percent: item.changePercent, - volume: Math.floor(Math.random() * 100000) + 50000, - open_interest: Math.floor(Math.random() * 1000000) + 500000 - })); - } - - async getHistoricalTrades(symbol: string, start: number, end: number): Promise { - if (!this.initialized) { - throw new Error('模拟数据源未初始化'); - } - - // 返回模拟历史成交数据 - const trades = []; - const count = 100; // 生成100条模拟数据 - const basePrice = 2000; - - for (let i = 0; i < count; i++) { - const price = basePrice + (Math.random() * 10 - 5); - const volume = Math.floor(Math.random() * 100) + 10; - const timestamp = start + Math.floor((end - start) * (i / count)); - - trades.push({ - datetime: timestamp * 1000000, // 转换为纳秒 - price: +price.toFixed(2), - volume, - direction: Math.random() > 0.5 ? 'BUY' : 'SELL' - }); - } - - return trades; - } - - async close(): Promise { - // 模拟关闭 - this.initialized = false; - logger.log('模拟数据源已关闭'); - } - - // 根据合约代码获取交易所 - private getExchangeBySymbol(symbol: string): string { - // 简单的交易所映射 - const exchangeMap: Record = { - 'AU': 'SHFE', - 'AG': 'SHFE', - 'CU': 'SHFE', - 'NI': 'SHFE', - 'SN': 'SHFE', - 'AL': 'SHFE', - 'ZN': 'SHFE', - 'FG': 'CZCE', - 'SJS': 'CZCE', - 'SCA': 'CZCE', - 'JM': 'DCE', - 'RB': 'SHFE', - 'ALO': 'SHFE', - 'MA': 'DCE', - 'PVC': 'DCE', - 'FU': 'SHFE', - 'SC': 'INE', - 'L': 'DCE', - 'NR': 'SHFE', - 'BU': 'SHFE', - 'LU': 'INE', - 'P': 'DCE', - 'LC': 'SHFE', - 'SI': 'SHFE', - 'PGS': 'SHFE', - 'IC': 'CFFEX', - 'IM': 'CFFEX', - 'IH': 'CFFEX' - }; - - return exchangeMap[symbol] || 'SHFE'; - } -} \ No newline at end of file diff --git a/backend/src/services/datasource/PythonServiceManager.ts b/backend/src/services/datasource/PythonServiceManager.ts deleted file mode 100644 index 751f186..0000000 --- a/backend/src/services/datasource/PythonServiceManager.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { spawn } from 'child_process'; -import * as path from 'path'; -import { config } from '../../config'; -import { logger } from '../../utils/logger'; - -class PythonServiceManager { - private pythonProcess: any = null; - private isRunning: boolean = false; - - async start(): Promise { - if (this.isRunning) { - logger.log('Python服务已经在运行'); - return true; - } - - try { - // 构建Python服务的路径 - const pythonServicePath = path.join(__dirname, '../../../python_service/main.py'); - - // 从配置中读取端口,默认3007 - const port = config.dataSource?.tqsdk?.pythonPort || 3007; - - logger.log(`启动Python TQAPI服务,端口: ${port}...`); - - // 启动Python服务,传递端口参数 - this.pythonProcess = spawn('python', [pythonServicePath, '--port', port.toString()], { - cwd: path.join(__dirname, '../../../python_service'), - stdio: 'inherit', - shell: true - }); - - this.pythonProcess.on('error', (error: any) => { - logger.error('启动Python服务失败:', error); - this.isRunning = false; - }); - - this.pythonProcess.on('exit', (code: number, signal: string) => { - logger.log(`Python服务退出,代码: ${code}, 信号: ${signal}`); - this.isRunning = false; - }); - - // 等待2秒,确保Python服务有时间启动 - await new Promise(resolve => setTimeout(resolve, 2000)); - - this.isRunning = true; - logger.log('Python TQAPI服务启动成功'); - return true; - } catch (error) { - logger.error('启动Python服务时出错:', error); - this.isRunning = false; - return false; - } - } - - stop(): void { - if (this.pythonProcess) { - try { - logger.log('停止Python TQAPI服务...'); - // 发送终止信号 - this.pythonProcess.kill(); - // 等待进程退出 - this.pythonProcess.on('exit', (code: number, signal: string) => { - logger.log(`Python服务退出,代码: ${code}, 信号: ${signal}`); - }); - this.pythonProcess = null; - this.isRunning = false; - logger.log('Python TQAPI服务已停止'); - } catch (error) { - logger.error('停止Python服务时出错:', error); - this.pythonProcess = null; - this.isRunning = false; - } - } else { - logger.log('Python服务未运行,无需停止'); - } - } - - getStatus(): boolean { - return this.isRunning; - } -} - -// 导出单例实例 -export const pythonServiceManager = new PythonServiceManager(); diff --git a/backend/src/services/datasource/TQDataSource.ts b/backend/src/services/datasource/TQDataSource.ts deleted file mode 100644 index ddc2859..0000000 --- a/backend/src/services/datasource/TQDataSource.ts +++ /dev/null @@ -1,439 +0,0 @@ -// TQAPI数据源实现 - 使用Python服务 -import { DataSource } from './DataSource'; -import { logger } from '../../utils/logger'; - -// 简化的HTTP客户端 -class HttpClient { - private baseUrl: string; - - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - - async get(endpoint: string, params?: Record): Promise { - let url = `${this.baseUrl}${endpoint}`; - if (params) { - const queryString = new URLSearchParams(params).toString(); - url += `?${queryString}`; - } - - logger.log('发送GET请求:', url); - - try { - // 设置20秒超时 - const controller = new AbortController(); - const timeoutId = setTimeout(() => { - logger.error('GET请求超时,正在中止请求...'); - controller.abort(); - }, 20000); - - try { - logger.log('正在发送GET请求...'); - const start = Date.now(); - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - signal: controller.signal - }); - const end = Date.now(); - logger.log(`GET请求完成,耗时: ${end - start}ms`); - logger.log('GET请求响应状态:', response.status); - - if (!response.ok) { - const errorText = await response.text(); - logger.error('GET请求失败:', errorText); - throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); - } - - logger.log('正在解析GET响应数据...'); - const data = await response.json(); - logger.log('GET请求响应数据:', data); - return data as T; - } finally { - clearTimeout(timeoutId); - } - } catch (error: any) { - logger.error('GET请求网络错误:', error.message || error); - logger.error('错误详情:', error); - logger.error('错误堆栈:', error.stack); - throw new Error(`网络请求失败: ${error.message || error}`); - } - } - - async post(endpoint: string, data?: any): Promise { - const url = `${this.baseUrl}${endpoint}`; - logger.log(`发送POST请求: ${url} 数据: ${JSON.stringify(data)}`); - - try { - // 设置20秒超时 - const controller = new AbortController(); - const timeoutId = setTimeout(() => { - logger.error('POST请求超时,正在中止请求...'); - controller.abort(); - }, 20000); - - try { - logger.log('正在发送请求...'); - const start = Date.now(); - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: data ? JSON.stringify(data) : undefined, - signal: controller.signal - }); - const end = Date.now(); - logger.log(`请求完成,耗时: ${end - start}ms`); - logger.log('POST请求响应状态:', response.status); - - if (!response.ok) { - const errorText = await response.text(); - logger.error('POST请求失败:', errorText); - throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); - } - - logger.log('正在解析响应数据...'); - const responseData = await response.json(); - logger.log('POST请求响应数据:', responseData); - return responseData as T; - } finally { - clearTimeout(timeoutId); - } - } catch (error: any) { - logger.error('POST请求网络错误:', error.message || error); - logger.error('错误详情:', error); - logger.error('错误堆栈:', error.stack); - throw new Error(`网络请求失败: ${error.message || error}`); - } - } -} - -export class TQDataSource implements DataSource { - private httpClient: HttpClient; - private initialized: boolean = false; - private config: { - username?: string; - password?: string; - timeout?: number; - retries?: number; - maxConnections?: number; - pythonServiceUrl?: string; - }; - - constructor(config: any = {}) { - logger.log('使用TQAPI数据源初始化...'); - // 从配置中读取端口,默认8001 - const port = config.pythonPort || 8001; - this.config = { - username: config.username || '', - password: config.password || '', - timeout: config.timeout || 30000, - retries: config.retries || 3, - maxConnections: config.maxConnections || 5, - pythonServiceUrl: config.pythonServiceUrl || `http://127.0.0.1:${port}/api` - }; - logger.log('TQAPI数据源配置:', this.config); - // 测试Python服务URL是否正确 - logger.log('测试Python服务URL:', this.config.pythonServiceUrl); - this.httpClient = new HttpClient(this.config.pythonServiceUrl!); - } - - async initialize(): Promise { - try { - logger.log('开始初始化TQAPI数据源...'); - logger.log('Python服务URL:', this.config.pythonServiceUrl); - logger.log('连接参数:', { - username: this.config.username ? '***' : '', - password: this.config.password ? '***' : '' - }); - - // 先测试Python服务是否可达 - logger.log('测试Python服务是否可达...'); - try { - const testUrl = `${this.config.pythonServiceUrl}/../health`; - logger.log('测试URL:', testUrl); - // 使用AbortController设置超时 - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - try { - const testResponse = await fetch(testUrl, { - method: 'GET', - signal: controller.signal - }); - logger.log('Python服务健康检查响应:', testResponse.status); - if (testResponse.ok) { - logger.log('Python服务可达'); - } else { - logger.error('Python服务不可达,状态码:', testResponse.status); - } - } finally { - clearTimeout(timeoutId); - } - } catch (error: any) { - logger.error('Python服务健康检查失败:', error.message || error); - } - - // 连接到天勤服务器 - logger.log('连接到天勤服务器...'); - - try { - const response = await this.httpClient.post('/connect', { - username: this.config.username, - password: this.config.password - }); - - logger.log('连接响应:', response); - - if (response.success) { - logger.log('TQAPI连接成功'); - this.initialized = true; - return true; - } else { - logger.error('TQAPI连接失败:', response.message); - this.initialized = false; - return false; - } - } catch (error: any) { - logger.error('连接请求失败:', error.message || error); - logger.error('错误详情:', error); - throw error; - } - } catch (error) { - logger.error('TQAPI数据源初始化失败:', error); - this.initialized = false; - return false; - } - } - - async getContractList(): Promise { - if (!this.initialized) { - throw new Error('TQAPI数据源未初始化'); - } - - try { - logger.log('获取合约列表...'); - const response = await this.httpClient.get('/contracts'); - - if (response.success && Array.isArray(response.data)) { - logger.log('获取合约列表成功,数量:', response.data.length); - return response.data; - } else { - logger.error('获取合约列表失败:', response.message); - return []; - } - } catch (error) { - logger.error('获取合约列表失败:', error); - throw error; - } - } - - async getContractDetail(symbol: string): Promise { - if (!this.initialized) { - throw new Error('TQAPI数据源未初始化'); - } - - try { - logger.log(`获取合约${symbol}详情...`); - const response = await this.httpClient.get(`/contract/${symbol}`); - - if (response.success) { - logger.log(`获取合约${symbol}详情成功`); - return response.data; - } else { - logger.error(`获取合约${symbol}详情失败:`, response.message); - throw new Error(`获取合约${symbol}详情失败`); - } - } catch (error) { - logger.error(`获取合约${symbol}详情失败:`, error); - throw error; - } - } - - async getKlineData(symbol: string, period: string, count: number): Promise { - if (!this.initialized) { - throw new Error('TQAPI数据源未初始化'); - } - - try { - logger.log('开始获取K线数据:', { - symbol: symbol, - period: period, - count: count - }); - - // 确保合约代码格式正确 - let contractSymbol = symbol; - if (!contractSymbol.includes('.')) { - // 如果没有交易所前缀,尝试添加默认交易所 - logger.warn('合约代码缺少交易所前缀,尝试添加默认交易所'); - contractSymbol = `SHFE.${contractSymbol}`; - } - logger.log('使用的合约代码:', contractSymbol); - - const response = await this.httpClient.get('/klines/' + contractSymbol, { - period: period, - count: count.toString() - }); - - if (response.success && Array.isArray(response.data)) { - logger.log('获取K线数据成功,长度:', response.data.length); - return response.data; - } else { - logger.error('获取K线数据失败:', response.message); - return []; - } - } catch (error) { - logger.error(`获取合约${symbol}K线数据失败:`, error); - throw error; - } - } - - async getTickData(symbol: string): Promise { - if (!this.initialized) { - throw new Error('TQAPI数据源未初始化'); - } - - try { - logger.log(`获取合约${symbol}实时行情数据...`); - - // 确保合约代码格式正确 - let contractSymbol = symbol; - if (!contractSymbol.includes('.')) { - // 如果没有交易所前缀,尝试添加默认交易所 - logger.warn('合约代码缺少交易所前缀,尝试添加默认交易所'); - contractSymbol = `SHFE.${contractSymbol}`; - } - logger.log('使用的合约代码:', contractSymbol); - - logger.log('正在发送GET请求到:', `/tick/${contractSymbol}`); - const start = Date.now(); - const response = await this.httpClient.get(`/tick/${contractSymbol}`); - const end = Date.now(); - logger.log(`GET请求完成,耗时: ${end - start}ms`); - logger.log('响应数据:', response); - - if (response.success) { - logger.log(`获取合约${symbol}实时行情数据成功`); - // 计算价格变化 - const tickData = response.data; - tickData.price_change = tickData.last_price - (tickData.pre_close || 0); - logger.log('计算价格变化后的数据:', tickData); - return tickData; - } else { - logger.error(`获取合约${symbol}实时行情数据失败:`, response.message); - // 返回默认数据,避免整个请求失败 - return { - last_price: 0, - pre_close: 0, - open: 0, - high: 0, - low: 0, - volume: 0, - open_interest: 0, - bid_price1: 0, - bid_volume1: 0, - ask_price1: 0, - ask_volume1: 0, - price_change: 0, - datetime: Date.now() - }; - } - } catch (error) { - logger.error(`获取合约${symbol}实时行情数据失败:`, error); - // 返回默认数据,避免整个请求失败 - return { - last_price: 0, - pre_close: 0, - open: 0, - high: 0, - low: 0, - volume: 0, - open_interest: 0, - bid_price1: 0, - bid_volume1: 0, - ask_price1: 0, - ask_volume1: 0, - price_change: 0, - datetime: Date.now() - }; - } - } - - async getMarketOverview(): Promise { - if (!this.initialized) { - throw new Error('TQAPI数据源未初始化'); - } - - try { - // 获取所有合约 - const contracts = await this.getContractList(); - // 限制获取的合约数量,避免请求过多 - const limitedContracts = contracts.slice(0, 20); - // 获取每个合约的实时行情 - const overview = []; - for (const contract of limitedContracts) { - try { - const tick = await this.getTickData(contract.symbol); - // 确保所有值都是有效的数字 - const price = typeof tick.last_price === 'number' ? tick.last_price : 0; - const change = typeof tick.price_change === 'number' ? tick.price_change : 0; - const pre_close = typeof tick.pre_close === 'number' && tick.pre_close !== 0 ? tick.pre_close : 1; - const volume = typeof tick.volume === 'number' ? tick.volume : 0; - const open_interest = typeof tick.open_interest === 'number' ? tick.open_interest : 0; - - overview.push({ - symbol: contract.symbol, - name: contract.name, - price: price, - change: change, - change_percent: change / pre_close * 100, - volume: volume, - open_interest: open_interest - }); - } catch (error) { - logger.error(`获取合约${contract.symbol}行情失败:`, error); - } - } - return overview; - } catch (error) { - logger.error('获取市场概览失败:', error); - throw error; - } - } - - async getHistoricalTrades(symbol: string, start: number, end: number): Promise { - if (!this.initialized) { - throw new Error('TQAPI数据源未初始化'); - } - - try { - // 目前Python服务未实现此功能 - logger.warn('TQAPI服务暂未实现历史成交数据获取功能'); - return []; - } catch (error) { - logger.error(`获取合约${symbol}历史成交数据失败:`, error); - throw error; - } - } - - async close(): Promise { - try { - logger.log('关闭TQAPI连接...'); - const response = await this.httpClient.post('/disconnect'); - - if (response.success) { - logger.log('TQAPI连接已关闭'); - } else { - logger.error('关闭TQAPI连接失败:', response.message); - } - } catch (error) { - logger.error('关闭TQAPI连接失败:', error); - } finally { - this.initialized = false; - logger.log('TQAPI数据源已关闭'); - } - } -} \ No newline at end of file diff --git a/backend/src/services/datasource/TQDataSourcePython.md b/backend/src/services/datasource/TQDataSourcePython.md deleted file mode 100644 index 51202f0..0000000 --- a/backend/src/services/datasource/TQDataSourcePython.md +++ /dev/null @@ -1,62 +0,0 @@ -# TQDataSource Python 实现方案 - -## 概述 - -将当前的 TQSDK Node.js 接口改为使用天勤接口的 Python 封装 tqapi。通过创建一个 Python 服务来处理 tqapi 的调用,并修改现有的 Node.js 代码与之通信。 - -## 实现方案 - -### 1. Python 服务 - -使用 FastAPI 创建一个 Python 服务,提供以下 HTTP 接口: - -| 方法 | 路径 | 功能 | 请求体 | 响应体 | -|------|------|------|--------|--------| -| POST | /api/connect | 连接到天勤服务器 | `{"username": "...", "password": "..."}` | `{"success": true, "message": "连接成功"}` | -| GET | /api/contracts | 获取合约列表 | N/A | `{"success": true, "data": [{...}, {...}]}` | -| GET | /api/contract/{symbol} | 获取合约详情 | N/A | `{"success": true, "data": {...}}` | -| GET | /api/klines/{symbol} | 获取 K 线数据 | 查询参数: period, count | `{"success": true, "data": [{...}, {...}]}` | -| GET | /api/tick/{symbol} | 获取 tick 数据 | N/A | `{"success": true, "data": {...}}` | -| POST | /api/disconnect | 断开连接 | N/A | `{"success": true, "message": "断开成功"}` | - -### 2. Node.js 代码修改 - -修改现有的 TQDataSource 实现,改为调用 Python 服务的 HTTP 接口: - -- 初始化时,调用 /api/connect 接口 -- 获取合约列表时,调用 /api/contracts 接口 -- 获取合约详情时,调用 /api/contract/{symbol} 接口 -- 获取 K 线数据时,调用 /api/klines/{symbol} 接口 -- 获取 tick 数据时,调用 /api/tick/{symbol} 接口 -- 关闭时,调用 /api/disconnect 接口 - -## 技术栈 - -- Python 3.8+ -- FastAPI -- TqApi (天勤量化) -- Node.js 16+ -- axios (HTTP 客户端) - -## 优势 - -1. 使用官方推荐的 Python 封装,接口更稳定 -2. 充分利用 Python 生态系统的优势 -3. 与 Node.js 代码解耦,便于维护 -4. 可以独立部署和扩展 Python 服务 - -## 实现步骤 - -1. 创建 Python 服务项目 -2. 实现 Python 服务的核心功能 -3. 测试 Python 服务的接口 -4. 修改 Node.js 代码,与 Python 服务通信 -5. 测试整体功能 -6. 优化性能和错误处理 - -## 注意事项 - -1. 需要确保 Python 服务和 Node.js 服务在同一网络环境中 -2. 需要处理好错误和异常情况 -3. 需要考虑性能优化,特别是在获取大量数据时 -4. 需要确保安全性,特别是在处理敏感信息时 \ No newline at end of file diff --git a/backend/src/services/marketService.ts b/backend/src/services/marketService.ts index 5f8c388..19f0f62 100644 --- a/backend/src/services/marketService.ts +++ b/backend/src/services/marketService.ts @@ -1,5 +1,4 @@ // 市场数据服务 -import { DataSourceFactory, DataSourceType } from './datasource/DataSourceFactory'; import { futuresList, generateFuturesOverview, generateFutureData, generateKlineData, riskAlerts } from '../utils/mockData'; import { config } from '../config'; import { serviceImplementationClient } from './ServiceImplementationClient'; @@ -137,104 +136,6 @@ export const fetchMarketOverview = async () => { // service_implementation API 失败,尝试使用其他数据源 } - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - logger.log('获取数据源配置:', dataSourceConfig); - // 检查是否有可用的数据源 - const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; - if (!hasAvailableDataSource) { - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); - } - - // 尝试使用TQSDK数据源 - if (dataSourceConfig.tqsdk?.enabled) { - try { - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - - // 使用用户指定的合约列表 - const overview = []; - for (const future of futuresList) { - try { - // 构建合约符号(使用小写代码,因为TQAPI期望小写) - const symbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`; - // 获取合约详情和实时行情 - const tick = await dataSource.getTickData(symbol); - - overview.push({ - code: future.code, - name: future.name, - currentPrice: tick.last_price, - changePercent: tick.price_change / tick.pre_close * 100, - winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率 - atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR - adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX - adxStatus: (adx: number) => { - if (adx < 20) return '无趋势/震荡'; - if (adx < 40) return '弱趋势'; - return '强趋势'; - }, - trends: { - '5MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '30MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1HOUR': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1DAY': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - } - }, - tradingAdvice: { - entry: tick.last_price, - stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)), - target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)), - resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)), - support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5)) - }, - overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], - aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` - }); - } catch (error) { - logger.error(`获取合约${future.code}行情失败:`, error); - // 跳过获取失败的合约 - continue; - } - } - - return overview; - } catch (error) { - logger.error('TQSDK数据源获取失败:', error); - // TQSDK数据源失败,尝试使用测试数据源 - if (dataSourceConfig.test?.enabled) { - logger.log('切换到测试数据源'); - // 启用了测试数据源,使用测试数据 - await new Promise(resolve => setTimeout(resolve, 300)); - return generateFuturesOverview(); - } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取市场概览失败,所有数据源均不可用'); - } - } - } else if (dataSourceConfig.test?.enabled) { - // 直接使用测试数据源 - logger.log('使用测试数据源'); - await new Promise(resolve => setTimeout(resolve, 300)); - return generateFuturesOverview(); - } else { - // 无可用数据源 - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); - } } catch (error) { logger.error('获取市场概览失败:', error); // 直接返回友好的错误提示 @@ -354,103 +255,6 @@ export const fetchMarketDetail = async (symbol: string) => { // service_implementation API 失败,尝试使用其他数据源 } - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - - // 检查是否有可用的数据源 - const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; - if (!hasAvailableDataSource) { - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); - } - - // 尝试使用TQSDK数据源 - if (dataSourceConfig.tqsdk?.enabled) { - try { - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - - // 构建合约符号(使用小写代码,因为TQAPI期望小写) - const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`; - - // 获取合约详情和实时行情 - const tick = await dataSource.getTickData(contractSymbol); - - // 转换为前端需要的格式 - return { - code: future.code, - name: future.name, - fullName: `${future.name}-${future.code}605`, - currentPrice: tick.last_price, - changePercent: tick.price_change / tick.pre_close * 100, - winRate: Math.floor(Math.random() * 50) + 30, - atr: +(Math.random() * 5 + 0.5).toFixed(2), - adx: Math.floor(Math.random() * 60) + 10, - adxStatus: (adx: number) => { - if (adx < 20) return '无趋势/震荡'; - if (adx < 40) return '弱趋势'; - return '强趋势'; - }, - trends: { - '5MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '30MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1HOUR': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1DAY': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - } - }, - indicators: { - macd: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)], - rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`, - bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)], - kdj: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)] - }, - tradingAdvice: { - entry: tick.last_price, - stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)), - target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)), - resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)), - support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5)) - }, - riskLevel: ['低', '中等', '高'][Math.floor(Math.random() * 3)], - volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)], - overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], - aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` - }; - } catch (error) { - logger.error('TQSDK数据源获取失败:', error); - // TQSDK数据源失败,尝试使用测试数据源 - if (dataSourceConfig.test?.enabled) { - logger.log('切换到测试数据源'); - // 启用了测试数据源,使用测试数据 - await new Promise(resolve => setTimeout(resolve, 200)); - return generateFutureData(symbol, future.name); - } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取品种详情失败,所有数据源均不可用'); - } - } - } else if (dataSourceConfig.test?.enabled) { - // 直接使用测试数据源 - logger.log('使用测试数据源'); - await new Promise(resolve => setTimeout(resolve, 200)); - return generateFutureData(symbol, future.name); - } else { - // 无可用数据源 - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); - } } catch (error) { logger.error(`获取品种${symbol}详情失败:`, error); // 直接返回友好的错误提示 @@ -535,58 +339,6 @@ export const fetchKlineData = async (symbol: string, period: string) => { logger.error('service_implementation API 获取K线数据失败:', error); // service_implementation API 失败,尝试使用其他数据源 } - - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - - // 检查是否有可用的数据源 - const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; - if (!hasAvailableDataSource) { - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); - } - - // 尝试使用TQSDK数据源 - if (dataSourceConfig.tqsdk?.enabled) { - try { - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - - // 构建合约符号(使用小写代码,因为TQAPI期望小写) - const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`; - - // 获取K线数据 - const klineData = await dataSource.getKlineData(contractSymbol, period, 30); - - // 转换为前端需要的格式 - return klineData.map(item => ({ - timestamp: item.datetime / 1000000000, // 转换为秒 - open: item.open, - high: item.high, - low: item.low, - close: item.close, - volume: item.volume - })); - } catch (error) { - logger.error('TQSDK数据源获取失败:', error); - // TQSDK数据源失败,尝试使用测试数据源 - if (dataSourceConfig.test?.enabled) { - logger.log('切换到测试数据源'); - // 启用了测试数据源,使用测试数据 - await new Promise(resolve => setTimeout(resolve, 200)); - return generateKlineData(30); - } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取K线数据失败,所有数据源均不可用'); - } - } - } else if (dataSourceConfig.test?.enabled) { - // 直接使用测试数据源 - logger.log('使用测试数据源'); - await new Promise(resolve => setTimeout(resolve, 200)); - return generateKlineData(30); - } else { - // 无可用数据源 - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); - } } catch (error) { logger.error(`获取合约${symbol}K线数据失败:`, error); // 直接返回友好的错误提示