2026-01-04 22:43:13 +08:00
|
|
|
# coding:utf-8
|
|
|
|
|
import os
|
|
|
|
|
import datetime
|
|
|
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI, Query
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
from fastapi.responses import FileResponse
|
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
# 导入新的管理器类
|
|
|
|
|
from qmt_engine import MultiEngineManager, TerminalStatus
|
2026-01-04 22:43:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================= Pydantic模型 =================
|
2026-01-10 04:06:35 +08:00
|
|
|
|
|
|
|
|
class TerminalStatusModel(BaseModel):
|
|
|
|
|
"""单个终端状态模型"""
|
|
|
|
|
qmt_id: str
|
|
|
|
|
alias: str
|
|
|
|
|
account_id: str
|
|
|
|
|
is_connected: bool
|
|
|
|
|
last_heartbeat: str
|
|
|
|
|
|
2026-01-04 22:43:13 +08:00
|
|
|
class StatusResponse(BaseModel):
|
2026-01-10 04:06:35 +08:00
|
|
|
"""全局状态响应模型"""
|
2026-01-04 22:43:13 +08:00
|
|
|
running: bool
|
|
|
|
|
start_time: str
|
2026-01-10 04:06:35 +08:00
|
|
|
terminals: List[TerminalStatusModel]
|
2026-01-04 22:43:13 +08:00
|
|
|
|
|
|
|
|
class PositionsResponse(BaseModel):
|
|
|
|
|
"""持仓响应模型"""
|
2026-01-10 04:06:35 +08:00
|
|
|
# 按 qmt_id 分组的实盘持仓
|
|
|
|
|
real_positions: Dict[str, List[Dict[str, Any]]]
|
|
|
|
|
# 按策略名分组的虚拟持仓
|
2026-01-04 22:43:13 +08:00
|
|
|
virtual_positions: Dict[str, Dict[str, str]]
|
|
|
|
|
|
|
|
|
|
class LogsResponse(BaseModel):
|
|
|
|
|
"""日志响应模型"""
|
|
|
|
|
logs: List[str]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================= FastAPI应用 =================
|
2026-01-10 04:06:35 +08:00
|
|
|
|
2026-01-04 22:43:13 +08:00
|
|
|
class QMTAPIServer:
|
2026-01-10 04:06:35 +08:00
|
|
|
"""多终端 QMT API服务器"""
|
2026-01-04 22:43:13 +08:00
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
def __init__(self, manager: MultiEngineManager):
|
|
|
|
|
self.app = FastAPI(title="QMT Multi-Terminal Monitor")
|
|
|
|
|
self.manager = manager
|
2026-01-04 22:43:13 +08:00
|
|
|
self._setup_middleware()
|
|
|
|
|
self._setup_routes()
|
|
|
|
|
|
|
|
|
|
def _setup_middleware(self):
|
|
|
|
|
"""设置中间件"""
|
|
|
|
|
self.app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=["*"],
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _setup_routes(self):
|
|
|
|
|
"""设置路由"""
|
|
|
|
|
|
|
|
|
|
@self.app.get("/", summary="仪表盘页面")
|
|
|
|
|
async def read_root():
|
|
|
|
|
"""返回仪表盘HTML页面"""
|
|
|
|
|
if os.path.exists("dashboard.html"):
|
|
|
|
|
return FileResponse("dashboard.html")
|
|
|
|
|
return {"error": "Dashboard not found"}
|
|
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
@self.app.get("/api/status", response_model=StatusResponse, summary="获取所有终端状态")
|
2026-01-04 22:43:13 +08:00
|
|
|
def get_status():
|
2026-01-10 04:06:35 +08:00
|
|
|
"""获取所有 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
|
|
|
|
|
]
|
|
|
|
|
|
2026-01-04 22:43:13 +08:00
|
|
|
return StatusResponse(
|
2026-01-10 04:06:35 +08:00
|
|
|
running=self.manager.is_running,
|
|
|
|
|
start_time=self.manager.start_time,
|
|
|
|
|
terminals=terminals
|
2026-01-04 22:43:13 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@self.app.get("/api/positions", response_model=PositionsResponse, summary="获取持仓信息")
|
|
|
|
|
def get_positions():
|
2026-01-10 04:06:35 +08:00
|
|
|
"""汇总所有终端的实盘持仓和所有策略的虚拟持仓"""
|
|
|
|
|
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
|
|
|
|
|
|
2026-01-04 22:43:13 +08:00
|
|
|
return PositionsResponse(
|
2026-01-10 04:06:35 +08:00
|
|
|
real_positions=real_pos_data,
|
|
|
|
|
virtual_positions=virtual_pos_data
|
2026-01-04 22:43:13 +08:00
|
|
|
)
|
|
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
@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)
|
2026-01-04 22:43:13 +08:00
|
|
|
return LogsResponse(logs=logs)
|
|
|
|
|
|
|
|
|
|
@self.app.get("/api/health", summary="健康检查")
|
|
|
|
|
def health_check():
|
2026-01-10 04:06:35 +08:00
|
|
|
"""健康检查:只要有一个终端在线即视为正常"""
|
|
|
|
|
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:
|
2026-01-04 22:43:13 +08:00
|
|
|
return {"status": "healthy", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
|
|
|
else:
|
2026-01-10 04:06:35 +08:00
|
|
|
return {"status": "unhealthy", "reason": "No terminals connected", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
2026-01-04 22:43:13 +08:00
|
|
|
|
|
|
|
|
def get_app(self) -> FastAPI:
|
|
|
|
|
"""获取FastAPI应用实例"""
|
|
|
|
|
return self.app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================= 辅助函数 =================
|
2026-01-10 04:06:35 +08:00
|
|
|
|
|
|
|
|
def create_api_server(manager: MultiEngineManager) -> FastAPI:
|
|
|
|
|
"""创建API服务器入口"""
|
|
|
|
|
server = QMTAPIServer(manager)
|
|
|
|
|
return server.get_app()
|