From 50ee1a5a0ab5b8247841be3664f119d957ce5cf7 Mon Sep 17 00:00:00 2001 From: liaozhaorun <1300336796@qq.com> Date: Sat, 10 Jan 2026 04:06:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0qmt=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=9A=E7=AB=AFqmt=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qmt/api_server.py | 123 ++++++---- qmt/dashboard.html | 227 +++++++++---------- qmt/qmt_engine.py | 544 +++++++++++++++++++++------------------------ qmt/qmt_test.py | 8 +- qmt/run.py | 68 +++--- qmt/start.bat | 12 +- 6 files changed, 487 insertions(+), 495 deletions(-) diff --git a/qmt/api_server.py b/qmt/api_server.py index 39ff4d1..4c33c61 100644 --- a/qmt/api_server.py +++ b/qmt/api_server.py @@ -8,37 +8,46 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from pydantic import BaseModel -from qmt_engine import QMTEngine, QMTStatus +# 导入新的管理器类 +from qmt_engine import MultiEngineManager, TerminalStatus # ================= Pydantic模型 ================= -class StatusResponse(BaseModel): - """状态响应模型""" - running: bool - qmt_connected: bool - start_time: str - last_loop_update: str - account_id: str +class TerminalStatusModel(BaseModel): + """单个终端状态模型""" + qmt_id: str + alias: str + account_id: str + is_connected: bool + last_heartbeat: str + +class StatusResponse(BaseModel): + """全局状态响应模型""" + running: bool + start_time: str + terminals: List[TerminalStatusModel] class PositionsResponse(BaseModel): """持仓响应模型""" - real_positions: List[Dict[str, Any]] + # 按 qmt_id 分组的实盘持仓 + real_positions: Dict[str, List[Dict[str, Any]]] + # 按策略名分组的虚拟持仓 virtual_positions: Dict[str, Dict[str, str]] - class LogsResponse(BaseModel): """日志响应模型""" logs: List[str] # ================= FastAPI应用 ================= + class QMTAPIServer: - """QMT API服务器""" + """多终端 QMT API服务器""" - def __init__(self, qmt_engine: QMTEngine): - self.app = FastAPI(title="QMT Monitor") - self.qmt_engine = qmt_engine + def __init__(self, manager: MultiEngineManager): + self.app = FastAPI(title="QMT Multi-Terminal Monitor") + self.manager = manager self._setup_middleware() self._setup_routes() @@ -61,41 +70,78 @@ class QMTAPIServer: return FileResponse("dashboard.html") return {"error": "Dashboard not found"} - @self.app.get("/api/status", response_model=StatusResponse, summary="获取系统状态") + @self.app.get("/api/status", response_model=StatusResponse, summary="获取所有终端状态") def get_status(): - """获取QMT连接状态和系统信息""" - status = self.qmt_engine.get_status() + """获取所有 QMT 终端的连接状态""" + terminal_data = self.manager.get_all_status() + + terminals = [ + TerminalStatusModel( + qmt_id=t.qmt_id, + alias=t.alias, + account_id=t.account_id, + is_connected=t.is_connected, + last_heartbeat=t.last_heartbeat + ) for t in terminal_data + ] + return StatusResponse( - running=status.is_running, - qmt_connected=status.is_connected, - start_time=status.start_time, - last_loop_update=status.last_heartbeat, - account_id=status.account_id + running=self.manager.is_running, + start_time=self.manager.start_time, + terminals=terminals ) @self.app.get("/api/positions", response_model=PositionsResponse, summary="获取持仓信息") def get_positions(): - """获取实盘和虚拟持仓信息""" - positions = self.qmt_engine.get_positions() + """汇总所有终端的实盘持仓和所有策略的虚拟持仓""" + real_pos_data = {} + virtual_pos_data = {} + + # 1. 遍历所有终端单元获取实盘持仓 + for qmt_id, unit in self.manager.units.items(): + positions = [] + if unit.callback and unit.callback.is_connected: + try: + xt_pos = unit.xt_trader.query_stock_positions(unit.acc_obj) + if xt_pos: + positions = [ + { + "code": p.stock_code, + "volume": p.volume, + "can_use": p.can_use_volume, + "market_value": round(p.market_value, 2) + } for p in xt_pos if p.volume > 0 + ] + except: + pass + 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 + return PositionsResponse( - real_positions=positions["real_positions"], - virtual_positions=positions["virtual_positions"] + real_positions=real_pos_data, + virtual_positions=virtual_pos_data ) - @self.app.get("/api/logs", response_model=LogsResponse, summary="获取日志") - def get_logs(lines: int = Query(50, ge=1, le=1000, description="返回日志行数")): - """获取最近的交易日志""" - logs = self.qmt_engine.get_logs(lines) + @self.app.get("/api/logs", response_model=LogsResponse, summary="获取系统日志") + def get_logs(lines: int = Query(50, ge=1, le=1000)): + """获取最近的系统运行日志""" + logs = self.manager.get_logs(lines) return LogsResponse(logs=logs) @self.app.get("/api/health", summary="健康检查") def health_check(): - """健康检查接口""" - status = self.qmt_engine.get_status() - if status.is_running and status.is_connected: + """健康检查:只要有一个终端在线即视为正常""" + 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", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + return {"status": "unhealthy", "reason": "No terminals connected", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} def get_app(self) -> FastAPI: """获取FastAPI应用实例""" @@ -103,7 +149,8 @@ class QMTAPIServer: # ================= 辅助函数 ================= -def create_api_server(qmt_engine: QMTEngine) -> FastAPI: - """创建API服务器""" - server = QMTAPIServer(qmt_engine) - return server.get_app() + +def create_api_server(manager: MultiEngineManager) -> FastAPI: + """创建API服务器入口""" + server = QMTAPIServer(manager) + return server.get_app() \ No newline at end of file diff --git a/qmt/dashboard.html b/qmt/dashboard.html index 49b14b0..bc82f29 100644 --- a/qmt/dashboard.html +++ b/qmt/dashboard.html @@ -3,7 +3,7 @@ - QMT 实盘监控看板 + QMT 多终端监控看板 @@ -13,23 +13,18 @@ .card-header { display: flex; justify-content: space-between; align-items: center; } .box-card { margin-bottom: 20px; } .log-box { - background: #1e1e1e; - color: #d4d4d4; - padding: 10px; - border-radius: 4px; - height: 400px; - overflow-y: scroll; - font-family: 'Consolas', 'Monaco', monospace; - font-size: 13px; - line-height: 1.5; + background: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 4px; + height: 350px; overflow-y: scroll; font-family: 'Consolas', monospace; + font-size: 12px; line-height: 1.5; } .log-line { margin: 0; border-bottom: 1px solid #333; white-space: pre-wrap; word-break: break-all; } - .log-line:hover { background-color: #2a2a2a; } - .virtual-item { margin-bottom: 20px; border-left: 4px solid #409EFF; padding-left: 10px; } - .virtual-title { font-weight: bold; font-size: 14px; margin-bottom: 8px; color: #606266; } + .terminal-group { margin-bottom: 15px; border: 1px solid #ebeef5; border-radius: 8px; padding: 10px; background: #fff; } + .terminal-title { font-weight: bold; font-size: 14px; margin-bottom: 8px; color: #409EFF; display: flex; align-items: center; } .status-badge { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; } .bg-green { background-color: #67C23A; } + .bg-red { background-color: #F56C6C; } .bg-gray { background-color: #909399; } + .virtual-item { margin-bottom: 15px; border-left: 4px solid #E6A23C; padding-left: 10px; } @@ -41,86 +36,100 @@
- QMT 实盘守护系统 + QMT 多账号实盘守护系统
- API: {{ status.running ? 'Running' : 'Offline' }} - - - QMT: {{ status.qmt_connected ? 'Connected' : 'Disconnected' }} + 系统: {{ status.running ? '运行中' : '离线' }} + 手动刷新
- - {{ status.account_id || '---' }} - {{ status.start_time || '---' }} - - - {{ status.last_loop_update || '---' }} - - - - 手动刷新 - -
- - - {{ tradingStatusText }} - + + + + + +
{{ t.alias }}
+
ID: {{ t.account_id }}
+
+ + {{ t.is_connected ? '已连接' : '已断开' }} + + {{ t.last_heartbeat }} +
+
+
+ +
+
启动时间: {{ status.start_time }}
+
+ + {{ tradingStatusText }} +
- - +
+
- + + - - - - - - + +
暂无持仓数据
+ +
+
+ + {{ getTerminalAlias(qmtId) }} +
+ + + + + + +
- + + -
- 暂无策略数据 / Redis未连接 -
-
{{ strategyName }}
- - - - - +
+ {{ strategyName }} + 虚拟占位: {{ Object.keys(posObj).length }} +
+ + +
+ - +
{{ line }}
@@ -134,122 +143,92 @@