feat: 完善 QMT 交易模块文档和配置展示功能
- 优化前端仪表盘界面 - 添加配置文件可视化展示 - 编写 QMT 模块配置文档 - 完善项目规则体系(KiloCode)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user