feat: 完善 QMT 交易模块文档和配置展示功能
- 优化前端仪表盘界面 - 添加配置文件可视化展示 - 编写 QMT 模块配置文档 - 完善项目规则体系(KiloCode)
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# coding:utf-8
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
@@ -9,7 +12,7 @@ from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
# 导入新的管理器类
|
||||
from qmt_engine import MultiEngineManager, TerminalStatus
|
||||
from qmt_engine import MultiEngineManager, TerminalStatus, AutoReconnectScheduler
|
||||
|
||||
|
||||
# ================= Pydantic模型 =================
|
||||
@@ -20,6 +23,8 @@ class TerminalStatusModel(BaseModel):
|
||||
alias: str
|
||||
account_id: str
|
||||
is_connected: bool
|
||||
callback_connected: bool
|
||||
physical_connected: bool
|
||||
last_heartbeat: str
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
@@ -39,17 +44,46 @@ class LogsResponse(BaseModel):
|
||||
"""日志响应模型"""
|
||||
logs: List[str]
|
||||
|
||||
class ConfigResponse(BaseModel):
|
||||
"""配置响应模型"""
|
||||
reconnect_time: str
|
||||
auto_reconnect_enabled: bool
|
||||
|
||||
|
||||
class FileConfigResponse(BaseModel):
|
||||
"""配置文件响应模型"""
|
||||
redis: Dict[str, Any]
|
||||
qmt_terminals: List[Dict[str, Any]]
|
||||
strategies: Dict[str, Any]
|
||||
raw_config: str
|
||||
config_path: str
|
||||
|
||||
class ConfigUpdateRequest(BaseModel):
|
||||
"""配置更新请求模型"""
|
||||
reconnect_time: Optional[str] = None
|
||||
auto_reconnect_enabled: Optional[bool] = None
|
||||
|
||||
class ReconnectResponse(BaseModel):
|
||||
"""重连响应模型"""
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
# ================= FastAPI应用 =================
|
||||
|
||||
class QMTAPIServer:
|
||||
"""多终端 QMT API服务器"""
|
||||
|
||||
def __init__(self, manager: MultiEngineManager):
|
||||
|
||||
def __init__(self, manager: MultiEngineManager, config_file: str = "config.json"):
|
||||
self.app = FastAPI(title="QMT Multi-Terminal Monitor")
|
||||
self.manager = manager
|
||||
self.config_file = config_file
|
||||
# 初始化自动重连调度器
|
||||
self.scheduler = AutoReconnectScheduler(manager, config_file=config_file)
|
||||
self._setup_middleware()
|
||||
self._setup_routes()
|
||||
# 启动调度器
|
||||
self.scheduler.start()
|
||||
|
||||
def _setup_middleware(self):
|
||||
"""设置中间件"""
|
||||
@@ -60,9 +94,56 @@ class QMTAPIServer:
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
def _mask_sensitive_value(self, value: str) -> str:
|
||||
"""对敏感值进行脱敏处理"""
|
||||
if not value:
|
||||
return value
|
||||
if len(value) <= 4:
|
||||
return "*" * len(value)
|
||||
return value[0] + "*" * (len(value) - 2) + value[-1]
|
||||
|
||||
def _mask_config(self, config: dict) -> dict:
|
||||
"""对配置文件中的敏感信息进行脱敏处理"""
|
||||
masked = {}
|
||||
for key, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
masked[key] = self._mask_config(value)
|
||||
elif isinstance(value, list):
|
||||
masked[key] = [
|
||||
self._mask_config(item) if isinstance(item, dict) else item
|
||||
for item in value
|
||||
]
|
||||
elif key in ("password", "pwd"):
|
||||
masked[key] = "******"
|
||||
elif key == "account_id" and isinstance(value, str):
|
||||
masked[key] = self._mask_sensitive_value(value)
|
||||
elif key == "path" and isinstance(value, str):
|
||||
# 路径脱敏,保留目录结构但隐藏用户名
|
||||
parts = value.split(os.sep)
|
||||
if len(parts) >= 3:
|
||||
masked[key] = os.sep.join(parts[:-2] + ["****"] + parts[-2:])
|
||||
else:
|
||||
masked[key] = value
|
||||
else:
|
||||
masked[key] = value
|
||||
return masked
|
||||
|
||||
def _find_config_file(self) -> Optional[str]:
|
||||
"""查找配置文件路径"""
|
||||
# 按优先级查找配置文件
|
||||
config_paths = [
|
||||
"config.json",
|
||||
"qmt/config.json",
|
||||
os.environ.get("QMT_CONFIG_PATH", "")
|
||||
]
|
||||
for path in config_paths:
|
||||
if path and os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
def _setup_routes(self):
|
||||
"""设置路由"""
|
||||
|
||||
|
||||
@self.app.get("/", summary="仪表盘页面")
|
||||
async def read_root():
|
||||
"""返回仪表盘HTML页面"""
|
||||
@@ -72,7 +153,7 @@ class QMTAPIServer:
|
||||
|
||||
@self.app.get("/api/status", response_model=StatusResponse, summary="获取所有终端状态")
|
||||
def get_status():
|
||||
"""获取所有 QMT 终端的连接状态"""
|
||||
"""获取所有 QMT 终端的连接状态,包含物理连接验证"""
|
||||
terminal_data = self.manager.get_all_status()
|
||||
|
||||
terminals = [
|
||||
@@ -81,6 +162,8 @@ class QMTAPIServer:
|
||||
alias=t.alias,
|
||||
account_id=t.account_id,
|
||||
is_connected=t.is_connected,
|
||||
callback_connected=t.callback_connected,
|
||||
physical_connected=t.physical_connected,
|
||||
last_heartbeat=t.last_heartbeat
|
||||
) for t in terminal_data
|
||||
]
|
||||
@@ -96,13 +179,22 @@ class QMTAPIServer:
|
||||
"""汇总所有终端的实盘持仓和所有策略的虚拟持仓"""
|
||||
real_pos_data = {}
|
||||
virtual_pos_data = {}
|
||||
|
||||
# 调试日志:记录管理器状态
|
||||
import logging
|
||||
logger = logging.getLogger("QMT_API")
|
||||
logger.info(f"[POS DEBUG] manager.units 数量: {len(self.manager.units) if hasattr(self.manager, 'units') else 'N/A'}")
|
||||
logger.info(f"[POS DEBUG] manager.config strategies: {list(self.manager.config.get('strategies', {}).keys()) if hasattr(self.manager, 'config') else 'N/A'}")
|
||||
logger.info(f"[POS DEBUG] pos_manager 是否存在: {hasattr(self.manager, 'pos_manager') and self.manager.pos_manager is not None}")
|
||||
|
||||
# 1. 遍历所有终端单元获取实盘持仓
|
||||
for qmt_id, unit in self.manager.units.items():
|
||||
positions = []
|
||||
logger.info(f"[POS DEBUG] 处理终端: {qmt_id}, callback: {unit.callback}, callback.is_connected: {unit.callback.is_connected if unit.callback else 'N/A'}")
|
||||
if unit.callback and unit.callback.is_connected:
|
||||
try:
|
||||
xt_pos = unit.xt_trader.query_stock_positions(unit.acc_obj)
|
||||
logger.info(f"[POS DEBUG] 终端 {qmt_id} 查询到持仓数量: {len(xt_pos) if xt_pos else 0}")
|
||||
if xt_pos:
|
||||
positions = [
|
||||
{
|
||||
@@ -112,14 +204,27 @@ class QMTAPIServer:
|
||||
"market_value": round(p.market_value, 2)
|
||||
} for p in xt_pos if p.volume > 0
|
||||
]
|
||||
except:
|
||||
pass
|
||||
logger.info(f"[POS DEBUG] 终端 {qmt_id} 有效持仓(volume>0): {len(positions)}")
|
||||
except Exception as e:
|
||||
logger.error(f"[POS DEBUG] 终端 {qmt_id} 查询持仓失败: {e}")
|
||||
real_pos_data[qmt_id] = positions
|
||||
|
||||
# 2. 遍历所有策略获取虚拟持仓
|
||||
for s_name in self.manager.config.get('strategies', {}).keys():
|
||||
v_data = self.manager.pos_manager.get_all_virtual_positions(s_name)
|
||||
virtual_pos_data[s_name] = v_data
|
||||
try:
|
||||
if hasattr(self.manager, 'pos_manager') and self.manager.pos_manager is not None:
|
||||
v_data = self.manager.pos_manager.get_all_virtual_positions(s_name)
|
||||
logger.info(f"[POS DEBUG] 策略 {s_name} 虚拟持仓: {v_data}")
|
||||
virtual_pos_data[s_name] = v_data
|
||||
else:
|
||||
logger.warning(f"[POS DEBUG] pos_manager 未初始化,策略 {s_name} 无法获取虚拟持仓")
|
||||
virtual_pos_data[s_name] = {}
|
||||
except Exception as e:
|
||||
logger.error(f"[POS DEBUG] 获取策略 {s_name} 虚拟持仓失败: {e}")
|
||||
virtual_pos_data[s_name] = {}
|
||||
|
||||
logger.info(f"[POS DEBUG] 最终 real_positions keys: {list(real_pos_data.keys())}, 总持仓数: {sum(len(v) for v in real_pos_data.values())}")
|
||||
logger.info(f"[POS DEBUG] 最终 virtual_positions keys: {list(virtual_pos_data.keys())}")
|
||||
|
||||
return PositionsResponse(
|
||||
real_positions=real_pos_data,
|
||||
@@ -137,11 +242,89 @@ class QMTAPIServer:
|
||||
"""健康检查:只要有一个终端在线即视为正常"""
|
||||
terminal_data = self.manager.get_all_status()
|
||||
any_connected = any(t.is_connected for t in terminal_data)
|
||||
|
||||
|
||||
if self.manager.is_running and any_connected:
|
||||
return {"status": "healthy", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
else:
|
||||
return {"status": "unhealthy", "reason": "No terminals connected", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
@self.app.get("/api/config", response_model=ConfigResponse, summary="获取自动重连配置")
|
||||
def get_config():
|
||||
"""获取当前自动重连配置"""
|
||||
config = self.scheduler.get_config()
|
||||
return ConfigResponse(
|
||||
reconnect_time=config["reconnect_time"],
|
||||
auto_reconnect_enabled=config["enabled"]
|
||||
)
|
||||
|
||||
@self.app.post("/api/config", summary="更新自动重连配置")
|
||||
def update_config(request: ConfigUpdateRequest):
|
||||
"""更新自动重连配置"""
|
||||
success = True
|
||||
message = "配置更新成功"
|
||||
|
||||
if request.reconnect_time is not None:
|
||||
if not self.scheduler.set_reconnect_time(request.reconnect_time):
|
||||
success = False
|
||||
message = "时间格式错误,请使用 HH:MM 格式"
|
||||
|
||||
if request.auto_reconnect_enabled is not None:
|
||||
self.scheduler.set_enabled(request.auto_reconnect_enabled)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": message, "config": self.scheduler.get_config()}
|
||||
else:
|
||||
return {"success": False, "message": message}
|
||||
|
||||
@self.app.post("/api/reconnect", response_model=ReconnectResponse, summary="手动触发重连")
|
||||
def trigger_reconnect():
|
||||
"""手动触发立即重连所有终端"""
|
||||
self.scheduler.trigger_reconnect()
|
||||
return ReconnectResponse(
|
||||
success=True,
|
||||
message="重连任务已在后台启动"
|
||||
)
|
||||
|
||||
@self.app.get("/api/file_config", response_model=FileConfigResponse, summary="获取配置文件内容")
|
||||
def get_file_config():
|
||||
"""获取配置文件内容,敏感信息已脱敏"""
|
||||
config_path = self._find_config_file()
|
||||
|
||||
if not config_path or not os.path.exists(config_path):
|
||||
return FileConfigResponse(
|
||||
redis={},
|
||||
qmt_terminals=[],
|
||||
strategies={},
|
||||
raw_config="",
|
||||
config_path=""
|
||||
)
|
||||
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# 脱敏处理
|
||||
masked_config = self._mask_config(config)
|
||||
|
||||
# 获取原始 JSON 字符串
|
||||
raw_json = json.dumps(config, ensure_ascii=False, indent=4)
|
||||
|
||||
return FileConfigResponse(
|
||||
redis=masked_config.get("redis", {}),
|
||||
qmt_terminals=masked_config.get("qmt_terminals", []),
|
||||
strategies=masked_config.get("strategies", {}),
|
||||
raw_config=raw_json,
|
||||
config_path=os.path.abspath(config_path)
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"读取配置文件失败: {e}")
|
||||
return FileConfigResponse(
|
||||
redis={},
|
||||
qmt_terminals=[],
|
||||
strategies={},
|
||||
raw_config=f"读取配置文件失败: {str(e)}",
|
||||
config_path=config_path
|
||||
)
|
||||
|
||||
def get_app(self) -> FastAPI:
|
||||
"""获取FastAPI应用实例"""
|
||||
@@ -150,7 +333,12 @@ class QMTAPIServer:
|
||||
|
||||
# ================= 辅助函数 =================
|
||||
|
||||
def create_api_server(manager: MultiEngineManager) -> FastAPI:
|
||||
"""创建API服务器入口"""
|
||||
server = QMTAPIServer(manager)
|
||||
def create_api_server(manager: MultiEngineManager, config_file: str = "config.json") -> FastAPI:
|
||||
"""创建API服务器入口
|
||||
|
||||
参数:
|
||||
- manager: MultiEngineManager 实例
|
||||
- config_file: 配置文件路径
|
||||
"""
|
||||
server = QMTAPIServer(manager, config_file=config_file)
|
||||
return server.get_app()
|
||||
Reference in New Issue
Block a user