feat: 完善 QMT 交易模块文档和配置展示功能

- 优化前端仪表盘界面
- 添加配置文件可视化展示
- 编写 QMT 模块配置文档
- 完善项目规则体系(KiloCode)
This commit is contained in:
2026-01-27 00:52:35 +08:00
parent 50ee1a5a0a
commit 4607555eaf
31 changed files with 5248 additions and 8621 deletions

View File

@@ -9,6 +9,7 @@ import threading
import logging
from typing import Optional, Dict, Any, List
from dataclasses import dataclass
from dateutil.parser import parse as parse_time
import redis
from xtquant import xtdata
@@ -31,6 +32,8 @@ class TerminalStatus:
alias: str
account_id: str
is_connected: bool
callback_connected: bool
physical_connected: bool
last_heartbeat: str
# ================= 1. 业务逻辑辅助类 =================
@@ -101,6 +104,201 @@ class DailySettlement:
def reset_flag(self):
self.has_settled = False
# ================= 1.5 定时重连调度器 =================
class AutoReconnectScheduler:
"""每日定时自动重连调度器"""
def __init__(self, manager, reconnect_time="22:00", config_file="config.json"):
"""
初始化定时重连调度器。
参数:
- manager: MultiEngineManager 实例
- reconnect_time: 重连时间(格式 "HH:MM"),默认 22:00
- config_file: 配置文件路径
"""
self.manager = manager
self.logger = logging.getLogger("QMT_Engine")
self.reconnect_time = reconnect_time
self.config_file = config_file
self.scheduler_thread = None
self.stop_event = threading.Event()
self.enabled = True
self._load_config()
def _load_config(self):
"""从配置文件加载设置"""
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
if 'auto_reconnect' in config:
self.reconnect_time = config['auto_reconnect'].get('reconnect_time', '22:00')
self.enabled = config['auto_reconnect'].get('enabled', True)
self.logger.info(f"加载自动重连配置: 时间={self.reconnect_time}, 启用={self.enabled}")
except Exception as e:
self.logger.warning(f"加载自动重连配置失败,使用默认值: {e}")
def _save_config(self):
"""保存配置到文件"""
config = {}
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
except:
pass
if 'auto_reconnect' not in config:
config['auto_reconnect'] = {}
config['auto_reconnect']['reconnect_time'] = self.reconnect_time
config['auto_reconnect']['enabled'] = self.enabled
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
self.logger.info(f"自动重连配置已保存: 时间={self.reconnect_time}, 启用={self.enabled}")
except Exception as e:
self.logger.error(f"保存自动重连配置失败: {e}")
def _calculate_next_run_time(self):
"""计算下一次执行时间"""
now = datetime.datetime.now()
try:
target_time = datetime.datetime.strptime(self.reconnect_time, "%H:%M").time()
next_run = datetime.datetime.combine(now.date(), target_time)
# 如果今天的时间已过,则安排到明天
if next_run <= now:
next_run += datetime.timedelta(days=1)
return next_run
except ValueError as e:
self.logger.error(f"时间格式错误 {self.reconnect_time}: {e}")
# 默认返回明天的 22:00
next_run = datetime.datetime.combine(now.date() + datetime.timedelta(days=1), datetime.time(22, 0))
return next_run
def _scheduler_loop(self):
"""调度器主循环"""
while not self.stop_event.is_set():
try:
if self.enabled:
next_run = self._calculate_next_run_time()
delay = (next_run - datetime.datetime.now()).total_seconds()
if delay > 0:
# 等待直到下一个执行时间,每分钟检查一次停止事件
wait_interval = 60 # 每分钟检查一次
waited = 0
while waited < delay and not self.stop_event.is_set():
sleep_time = min(wait_interval, delay - waited)
time.sleep(sleep_time)
waited += sleep_time
if not self.stop_event.is_set() and self.enabled:
self._scheduled_reconnect()
else:
# 如果禁用,每分钟检查一次
time.sleep(60)
except Exception as e:
self.logger.error(f"调度器异常: {e}")
time.sleep(60)
def _scheduled_reconnect(self):
"""执行定时重连任务"""
self.logger.info(f"执行定时重连任务,时间: {self.reconnect_time}")
# 1. 检测当前连接状态
statuses = self.manager.get_all_status()
connected_count = sum(1 for s in statuses if s.is_connected)
self.logger.info(f"当前连接状态: {connected_count}/{len(statuses)} 个终端在线")
# 2. 如果有连接,先断开
if connected_count > 0:
self.logger.info("正在断开所有终端连接...")
for unit in self.manager.units.values():
try:
if unit.xt_trader:
unit.cleanup()
except Exception as e:
self.logger.warning(f"断开终端 {unit.alias} 失败: {e}")
# 3. 等待几秒后重新连接
self.logger.info("等待 3 秒后重新连接...")
time.sleep(3)
# 4. 重新连接所有终端
self.logger.info("正在重新连接所有终端...")
success_count = 0
for unit in self.manager.units.values():
if unit.connect():
success_count += 1
self.logger.info(f"终端 {unit.alias} 重连成功")
else:
self.logger.warning(f"终端 {unit.alias} 重连失败")
self.logger.info(f"定时重连完成: {success_count}/{len(self.manager.units)} 个终端重连成功")
def start(self):
"""启动定时任务"""
if self.scheduler_thread and self.scheduler_thread.is_alive():
self.logger.warning("调度器已在运行中")
return
self.stop_event.clear()
self.scheduler_thread = threading.Thread(target=self._scheduler_loop, name="AutoReconnectScheduler", daemon=True)
self.scheduler_thread.start()
self.logger.info(f"自动重连调度器已启动,重连时间: {self.reconnect_time}, 启用状态: {self.enabled}")
def stop(self):
"""停止定时任务"""
self.logger.info("正在停止自动重连调度器...")
self.stop_event.set()
if self.scheduler_thread and self.scheduler_thread.is_alive():
self.scheduler_thread.join(timeout=5)
self.logger.info("自动重连调度器已停止")
def set_reconnect_time(self, time_str):
"""
设置重连时间。
参数:
- time_str: 时间字符串,格式 "HH:MM"
"""
try:
# 验证时间格式
datetime.datetime.strptime(time_str, "%H:%M")
old_time = self.reconnect_time
self.reconnect_time = time_str
self._save_config()
self.logger.info(f"重连时间已从 {old_time} 修改为 {time_str}")
return True
except ValueError as e:
self.logger.error(f"时间格式错误 {time_str}: {e}")
return False
def set_enabled(self, enabled):
"""设置是否启用自动重连"""
self.enabled = enabled
self._save_config()
self.logger.info(f"自动重连已{'启用' if enabled else '禁用'}")
def get_config(self):
"""获取当前配置"""
return {
"reconnect_time": self.reconnect_time,
"enabled": self.enabled
}
def trigger_reconnect(self):
"""手动触发重连(立即执行)"""
self.logger.info("手动触发重连任务")
threading.Thread(target=self._scheduled_reconnect, name="ManualReconnect", daemon=True).start()
# ================= 2. 执行单元 (TradingUnit) =================
class UnitCallback(XtQuantTraderCallback):
@@ -385,8 +583,71 @@ class MultiEngineManager:
except:
self.logger.error(traceback.format_exc())
def verify_connection(self, timeout=5):
"""验证物理连接是否有效"""
try:
# 先检查回调状态
if not self.callback or not self.callback.is_connected:
return False
# 实际调用 API 进行物理探测
asset = self.xt_trader.query_stock_asset(self.acc_obj)
if asset is not None:
return True
return False
except Exception as e:
logging.getLogger("QMT_Engine").warning(f"终端 {self.alias} 物理连接验证失败: {e}")
return False
def get_all_status(self) -> List[TerminalStatus]:
return [TerminalStatus(u.qmt_id, u.alias, u.account_id, (u.callback.is_connected if u.callback else False), u.last_heartbeat) for u in self.units.values()]
"""获取所有状态,包含物理连接验证"""
statuses = []
for u in self.units.values():
callback_conn = u.callback.is_connected if u.callback else False
# 物理探测通过查询资产确认连接有效性xtquant 无 verify_connection 方法)
physical_conn = False
if callback_conn and u.xt_trader and u.acc_obj:
try:
asset = u.xt_trader.query_stock_asset(u.acc_obj)
physical_conn = asset is not None
except:
physical_conn = False
is_connected = callback_conn and physical_conn
statuses.append(TerminalStatus(
qmt_id=u.qmt_id,
alias=u.alias,
account_id=u.account_id,
is_connected=is_connected,
callback_connected=callback_conn,
physical_connected=physical_conn,
last_heartbeat=u.last_heartbeat
))
return statuses
def get_logs(self, lines: int = 50) -> List[str]:
"""获取最近的系统日志
参数:
lines: 返回的行数默认50行
返回:
日志行列表
"""
log_dir = "logs"
log_file = os.path.join(log_dir, f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
if not os.path.exists(log_file):
return []
try:
with open(log_file, 'r', encoding='utf-8') as f:
all_lines = f.readlines()
# 返回最后指定行数
return all_lines[-lines:] if lines < len(all_lines) else all_lines
except Exception as e:
self.logger.error(f"读取日志文件失败: {e}")
return []
def stop(self):
self.is_running = False