fix(qmt): 修复交易模块核心缺陷

- 修复重复的重连逻辑代码块,避免重复连接
- 修复卖出逻辑:增加实盘持仓校验,一切以实盘为准
- 修复幽灵持仓自动清理机制
- 修复消息处理的静默异常,添加完整日志记录
- 统一 qmt 模块所有静默处理问题
- 添加 qmt_signal_sender.py 信号发送器
- 生成 TODO_FIX.md 缺陷修复任务清单
This commit is contained in:
2026-02-17 23:10:28 +08:00
parent e407225d29
commit 29706da299
5 changed files with 1440 additions and 297 deletions

View File

@@ -20,14 +20,17 @@ from xtquant import xtconstant
# ================= 0. Windows 补丁 =================
try:
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 128)
except:
pass
@dataclass
class TerminalStatus:
"""终端实例状态封装"""
qmt_id: str
alias: str
account_id: str
@@ -36,10 +39,13 @@ class TerminalStatus:
physical_connected: bool
last_heartbeat: str
# ================= 1. 业务逻辑辅助类 =================
class PositionManager:
"""Redis 虚拟持仓管理(全局单例)"""
def __init__(self, r_client):
self.r = r_client
@@ -76,8 +82,10 @@ class PositionManager:
def force_delete(self, strategy_name, code):
self.r.hdel(self._get_key(strategy_name), code)
class DailySettlement:
"""终端级别的日终对账"""
def __init__(self, unit):
self.unit = unit
self.has_settled = False
@@ -85,11 +93,16 @@ class DailySettlement:
def run_settlement(self):
trader = self.unit.xt_trader
acc = self.unit.acc_obj
if not trader: return
if not trader:
return
real_positions = trader.query_stock_positions(acc)
real_pos_map = {p.stock_code: p.volume for p in real_positions if p.volume > 0} if real_positions else {}
real_pos_map = (
{p.stock_code: p.volume for p in real_positions if p.volume > 0}
if real_positions
else {}
)
manager = MultiEngineManager()
strategies = manager.get_strategies_by_terminal(self.unit.qmt_id)
for s_name in strategies:
@@ -98,15 +111,18 @@ class DailySettlement:
if code not in real_pos_map:
manager.pos_manager.force_delete(s_name, code)
elif int(v_str) == 0 and code in real_pos_map:
manager.pos_manager.update_actual_volume(s_name, code, real_pos_map[code])
manager.pos_manager.update_actual_volume(
s_name, code, real_pos_map[code]
)
self.has_settled = True
def reset_flag(self):
self.has_settled = False
# ================= 1.5 定时重连调度器 =================
class AutoReconnectScheduler:
"""每日定时自动重连调度器"""
@@ -132,12 +148,16 @@ class AutoReconnectScheduler:
"""从配置文件加载设置"""
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
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}")
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}")
@@ -146,21 +166,23 @@ class AutoReconnectScheduler:
config = {}
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
with open(self.config_file, "r", encoding="utf-8") as f:
config = json.load(f)
except:
pass
except Exception as e:
self.logger.warning(f"读取配置文件失败,将创建新配置: {e}")
if 'auto_reconnect' not in config:
config['auto_reconnect'] = {}
if "auto_reconnect" not in config:
config["auto_reconnect"] = {}
config['auto_reconnect']['reconnect_time'] = self.reconnect_time
config['auto_reconnect']['enabled'] = self.enabled
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:
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}")
self.logger.info(
f"自动重连配置已保存: 时间={self.reconnect_time}, 启用={self.enabled}"
)
except Exception as e:
self.logger.error(f"保存自动重连配置失败: {e}")
@@ -168,7 +190,9 @@ class AutoReconnectScheduler:
"""计算下一次执行时间"""
now = datetime.datetime.now()
try:
target_time = datetime.datetime.strptime(self.reconnect_time, "%H:%M").time()
target_time = datetime.datetime.strptime(
self.reconnect_time, "%H:%M"
).time()
next_run = datetime.datetime.combine(now.date(), target_time)
# 如果今天的时间已过,则安排到明天
@@ -179,7 +203,9 @@ class AutoReconnectScheduler:
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))
next_run = datetime.datetime.combine(
now.date() + datetime.timedelta(days=1), datetime.time(22, 0)
)
return next_run
def _scheduler_loop(self):
@@ -210,7 +236,9 @@ class AutoReconnectScheduler:
def _scheduled_reconnect(self):
"""执行定时重连任务(强制重连模式)"""
self.logger.info(f"[AutoReconnectScheduler] 执行定时重连任务,时间: {self.reconnect_time}")
self.logger.info(
f"[AutoReconnectScheduler] 执行定时重连任务,时间: {self.reconnect_time}"
)
# 设置重连中标志位,通知主循环暂停健康检查重连
self.manager.is_scheduled_reconnecting = True
@@ -222,9 +250,13 @@ class AutoReconnectScheduler:
try:
if unit.xt_trader:
unit.cleanup()
self.logger.info(f"[AutoReconnectScheduler] 已断开终端 {unit.alias} 的连接")
self.logger.info(
f"[AutoReconnectScheduler] 已断开终端 {unit.alias} 的连接"
)
except Exception as e:
self.logger.warning(f"[AutoReconnectScheduler] 断开终端 {unit.alias} 失败: {e}")
self.logger.warning(
f"[AutoReconnectScheduler] 断开终端 {unit.alias} 失败: {e}"
)
# 等待几秒后重新连接(固定等待时间)
self.logger.info("[AutoReconnectScheduler] 等待 3 秒后重新连接...")
@@ -236,11 +268,17 @@ class AutoReconnectScheduler:
for unit in self.manager.units.values():
if unit.connect():
success_count += 1
self.logger.info(f"[AutoReconnectScheduler] 终端 {unit.alias} 重连成功")
self.logger.info(
f"[AutoReconnectScheduler] 终端 {unit.alias} 重连成功"
)
else:
self.logger.warning(f"[AutoReconnectScheduler] 终端 {unit.alias} 重连失败")
self.logger.warning(
f"[AutoReconnectScheduler] 终端 {unit.alias} 重连失败"
)
self.logger.info(f"[AutoReconnectScheduler] 定时重连完成: {success_count}/{len(self.manager.units)} 个终端重连成功")
self.logger.info(
f"[AutoReconnectScheduler] 定时重连完成: {success_count}/{len(self.manager.units)} 个终端重连成功"
)
finally:
# 确保无论成功与否都重置标志位
self.manager.is_scheduled_reconnecting = False
@@ -252,9 +290,13 @@ class AutoReconnectScheduler:
return
self.stop_event.clear()
self.scheduler_thread = threading.Thread(target=self._scheduler_loop, name="AutoReconnectScheduler", daemon=True)
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}")
self.logger.info(
f"自动重连调度器已启动,重连时间: {self.reconnect_time}, 启用状态: {self.enabled}"
)
def stop(self):
"""停止定时任务"""
@@ -291,101 +333,126 @@ class AutoReconnectScheduler:
def get_config(self):
"""获取当前配置"""
return {
"reconnect_time": self.reconnect_time,
"enabled": self.enabled
}
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()
threading.Thread(
target=self._scheduled_reconnect, name="ManualReconnect", daemon=True
).start()
# ================= 2. 执行单元 (TradingUnit) =================
class UnitCallback(XtQuantTraderCallback):
def __init__(self, unit):
self.unit = unit
self.is_connected = False
def on_disconnected(self):
logging.getLogger("QMT_Engine").warning(f"终端 {self.unit.alias}({self.unit.qmt_id}) 物理连接断开")
logging.getLogger("QMT_Engine").warning(
f"终端 {self.unit.alias}({self.unit.qmt_id}) 物理连接断开"
)
self.is_connected = False
def on_stock_trade(self, trade):
try:
cache_info = self.unit.order_cache.get(trade.order_id)
if not cache_info: return
if not cache_info:
return
s_name, _, action = cache_info
manager = MultiEngineManager()
if action == 'BUY':
manager.pos_manager.update_actual_volume(s_name, trade.stock_code, trade.traded_volume)
elif action == 'SELL':
manager.pos_manager.update_actual_volume(s_name, trade.stock_code, -trade.traded_volume)
except:
if action == "BUY":
manager.pos_manager.update_actual_volume(
s_name, trade.stock_code, trade.traded_volume
)
elif action == "SELL":
manager.pos_manager.update_actual_volume(
s_name, trade.stock_code, -trade.traded_volume
)
except:
logging.getLogger("QMT_Engine").error(traceback.format_exc())
def on_order_error(self, err):
cache = self.unit.order_cache.get(err.order_id)
if cache and cache[2] == 'BUY':
if cache and cache[2] == "BUY":
MultiEngineManager().pos_manager.rollback_holding(cache[0], cache[1])
self.unit.order_cache.pop(err.order_id, None)
class TradingUnit:
"""终端实例执行单元,负责管理单个 QMT 进程"""
def __init__(self, t_cfg):
self.qmt_id = t_cfg['qmt_id']
self.alias = t_cfg.get('alias', self.qmt_id)
self.path = t_cfg['path']
self.account_id = t_cfg['account_id']
self.account_type = t_cfg['account_type']
self.qmt_id = t_cfg["qmt_id"]
self.alias = t_cfg.get("alias", self.qmt_id)
self.path = t_cfg["path"]
self.account_id = t_cfg["account_id"]
self.account_type = t_cfg["account_type"]
self.xt_trader = None
self.acc_obj = None
self.callback = None
self.settler = None
self.order_cache = {}
self.order_cache = {}
self.last_heartbeat = "N/A"
# 重连控制
self.reconnect_attempts = 0 # 累计重连次数
self.max_reconnect_attempts = 3 # 最大重连次数
self.last_reconnect_fail_time: Optional[float] = None # 上次重连失败时间
def cleanup(self):
"""强制销毁资源,确保文件句柄释放"""
if self.xt_trader:
try:
logging.getLogger("QMT_Engine").info(f"正在销毁终端 {self.alias} 的旧资源...")
logging.getLogger("QMT_Engine").info(
f"正在销毁终端 {self.alias} 的旧资源..."
)
self.xt_trader.stop()
self.xt_trader = None # 显式置空
self.callback = None
time.sleep(1.5) # 给 C++ 引擎留出释放 down_queue 锁的时间
except Exception as e:
logging.getLogger("QMT_Engine").warning(f"销毁终端 {self.alias} 资源时出现异常: {e}")
logging.getLogger("QMT_Engine").warning(
f"销毁终端 {self.alias} 资源时出现异常: {e}"
)
def connect(self):
"""连接 QMT 终端"""
self.cleanup() # 启动前先执行清理
self.cleanup() # 启动前先执行清理
try:
# 采用动态 Session ID 避免冲突
session_id = int(time.time()) + hash(self.qmt_id) % 1000
self.xt_trader = XtQuantTrader(self.path, session_id)
self.acc_obj = StockAccount(self.account_id, self.account_type)
self.callback = UnitCallback(self)
self.xt_trader.register_callback(self.callback)
self.xt_trader.start()
res = self.xt_trader.connect()
if res == 0:
self.xt_trader.subscribe(self.acc_obj)
self.callback.is_connected = True
self.settler = DailySettlement(self)
logging.getLogger("QMT_Engine").info(f"终端 {self.alias} 连接成功 (SID: {session_id})")
logging.getLogger("QMT_Engine").info(
f"终端 {self.alias} 连接成功 (SID: {session_id})"
)
return True
return False
except Exception as e:
logging.getLogger("QMT_Engine").error(f"终端 {self.alias} 连接异常: {repr(e)}")
logging.getLogger("QMT_Engine").error(
f"终端 {self.alias} 连接异常: {repr(e)}"
)
return False
# ================= 3. 总控中心 (MultiEngineManager) =================
class MultiEngineManager:
_instance = None
_lock = threading.Lock()
@@ -398,36 +465,42 @@ class MultiEngineManager:
return cls._instance
def __init__(self):
if hasattr(self, '_initialized'): return
if hasattr(self, "_initialized"):
return
self.units: Dict[str, TradingUnit] = {}
self.config = {}
self.is_running = True
self.start_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.is_scheduled_reconnecting = False # 定时重连调度器正在执行标志
self._initialized = True
def initialize(self, config_file='config.json'):
def initialize(self, config_file="config.json"):
self._setup_logger()
with open(config_file, 'r', encoding='utf-8') as f:
with open(config_file, "r", encoding="utf-8") as f:
self.config = json.load(f)
self.r = redis.Redis(**self.config['redis'], decode_responses=True)
self.r = redis.Redis(**self.config["redis"], decode_responses=True)
self.pos_manager = PositionManager(self.r)
for t_cfg in self.config.get('qmt_terminals', []):
for t_cfg in self.config.get("qmt_terminals", []):
unit = TradingUnit(t_cfg)
unit.connect()
self.units[unit.qmt_id] = unit
def _setup_logger(self):
log_dir = "logs"
if not os.path.exists(log_dir): os.makedirs(log_dir)
log_file = os.path.join(log_dir, f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = os.path.join(
log_dir, f"{datetime.date.today().strftime('%Y-%m-%d')}.log"
)
logger = logging.getLogger("QMT_Engine")
logger.setLevel(logging.INFO)
# 确保日志流为 UTF-8
fmt = logging.Formatter('[%(asctime)s] [%(threadName)s] %(message)s', '%H:%M:%S')
fh = logging.FileHandler(log_file, mode='a', encoding='utf-8')
fmt = logging.Formatter(
"[%(asctime)s] [%(threadName)s] %(message)s", "%H:%M:%S"
)
fh = logging.FileHandler(log_file, mode="a", encoding="utf-8")
fh.setFormatter(fmt)
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(fmt)
@@ -435,7 +508,11 @@ class MultiEngineManager:
logger.addHandler(sh)
def get_strategies_by_terminal(self, qmt_id):
return [s for s, cfg in self.config['strategies'].items() if cfg.get('qmt_id') == qmt_id]
return [
s
for s, cfg in self.config["strategies"].items()
if cfg.get("qmt_id") == qmt_id
]
def run_trading_loop(self):
self.logger = logging.getLogger("QMT_Engine")
@@ -444,8 +521,8 @@ class MultiEngineManager:
while self.is_running:
try:
now_t = time.time()
curr_hms = datetime.datetime.now().strftime('%H%M%S')
curr_hms = datetime.datetime.now().strftime("%H%M%S")
# --- 健康检查与自动修复 ---
if now_t - last_check > 25:
last_check = now_t
@@ -457,42 +534,96 @@ class MultiEngineManager:
asset = unit.xt_trader.query_stock_asset(unit.acc_obj)
if asset:
is_unit_alive = True
unit.last_heartbeat = datetime.datetime.now().strftime('%H:%M:%S')
unit.last_heartbeat = (
datetime.datetime.now().strftime("%H:%M:%S")
)
# 状态修正物理通但逻辑False时自动拉回
if unit.callback and not unit.callback.is_connected:
unit.callback.is_connected = True
self.logger.info(f"✅ 修正终端 {unit.alias} 状态为在线")
self.logger.info(
f"✅ 修正终端 {unit.alias} 状态为在线"
)
except Exception as e:
self.logger.error(f"健康检查失败 - 终端 {unit.alias}: {str(e)}", exc_info=True)
self.logger.error(
f"健康检查失败 - 终端 {unit.alias}: {str(e)}",
exc_info=True,
)
is_unit_alive = False
# 断线重连策略
if not is_unit_alive:
# 避让 QMT 夜间重启高峰 (21:32 - 21:50)
if not ('213200' <= curr_hms <= '215000'):
if not ("213200" <= curr_hms <= "215000"):
# 检查是否正在执行定时重连调度
if self.is_scheduled_reconnecting:
self.logger.info(f"⏳ 定时重连调度器正在执行,跳过健康检查重连...")
self.logger.info(
f"⏳ 定时重连调度器正在执行,跳过健康检查重连..."
)
else:
self.logger.warning(f"🚫 终端 {unit.alias} 物理连接丢失,执行重连...")
unit.connect()
# 检查重连次数是否超过限制
if (
unit.reconnect_attempts
>= unit.max_reconnect_attempts
):
self.logger.warning(
f"⚠️ 终端 {unit.alias} 重连失败次数已达上限 ({unit.reconnect_attempts}/{unit.max_reconnect_attempts}),停止自动重连"
)
# 如果距离上次失败超过5分钟重置计数器
if unit.last_reconnect_fail_time:
elapsed = (
time.time()
- unit.last_reconnect_fail_time
)
if elapsed > 300: # 5分钟
unit.reconnect_attempts = 0
self.logger.info(
f"⏰ 终端 {unit.alias} 重连计数器已重置 (距离上次失败 {elapsed / 60:.1f} 分钟)"
)
else:
self.logger.info(
f"⏳ 终端 {unit.alias} 需要等待 {300 - elapsed:.0f} 秒后重试"
)
continue
else:
continue
else:
self.logger.warning(
f"🚫 终端 {unit.alias} 物理连接丢失,执行重连 ({unit.reconnect_attempts + 1}/{unit.max_reconnect_attempts})..."
)
reconnect_success = unit.connect()
if reconnect_success:
unit.reconnect_attempts = (
0 # 重连成功后重置计数
)
unit.last_reconnect_fail_time = None
else:
unit.reconnect_attempts += 1
unit.last_reconnect_fail_time = time.time()
self.logger.error(
f"❌ 终端 {unit.alias} 重连失败,已尝试 {unit.reconnect_attempts}/{unit.max_reconnect_attempts}"
)
else:
self.logger.info(f"⏳ 处于 QMT 重启时段 ({curr_hms}),跳过重连操作...")
self.logger.info(
f"⏳ 处于 QMT 重启时段 ({curr_hms}),跳过重连操作..."
)
# --- 交易逻辑处理 ---
is_trading = ('091500' <= curr_hms <= '113030') or ('130000' <= curr_hms <= '150030')
is_trading = ("091500" <= curr_hms <= "113030") or (
"130000" <= curr_hms <= "150030"
)
if is_trading:
for s_name in self.config['strategies'].keys():
for s_name in self.config["strategies"].keys():
self.process_route(s_name)
# --- 收盘结算与标志位重置 ---
elif '150500' <= curr_hms <= '151500':
elif "150500" <= curr_hms <= "151500":
for unit in self.units.values():
if unit.settler and not unit.settler.has_settled:
if unit.settler and not unit.settler.has_settled:
unit.settler.run_settlement()
elif '153000' <= curr_hms <= '160000':
elif "153000" <= curr_hms <= "160000":
for unit in self.units.values():
if unit.settler: unit.settler.reset_flag()
if unit.settler:
unit.settler.reset_flag()
time.sleep(1 if is_trading else 5)
except:
@@ -501,103 +632,174 @@ class MultiEngineManager:
time.sleep(10)
def process_route(self, strategy_name):
strat_cfg = self.config['strategies'].get(strategy_name)
unit = self.units.get(strat_cfg.get('qmt_id'))
if not unit or not unit.callback or not unit.callback.is_connected: return
strat_cfg = self.config["strategies"].get(strategy_name)
unit = self.units.get(strat_cfg.get("qmt_id"))
if not unit or not unit.callback or not unit.callback.is_connected:
return
msg_json = self.r.lpop(f"{strategy_name}_real")
if not msg_json: return
if not msg_json:
return
try:
data = json.loads(msg_json)
# 严格校验消息日期
if data.get('timestamp', '').split(' ')[0] != datetime.date.today().strftime('%Y-%m-%d'):
if data.get("timestamp", "").split(" ")[
0
] != datetime.date.today().strftime("%Y-%m-%d"):
return
if data['action'] == 'BUY':
if data["action"] == "BUY":
self._execute_buy(unit, strategy_name, data)
elif data['action'] == 'SELL':
elif data["action"] == "SELL":
self._execute_sell(unit, strategy_name, data)
except:
pass
except json.JSONDecodeError as e:
self.logger.error(
f"[{strategy_name}] JSON解析失败: {e}, 消息: {msg_json[:200]}"
)
except KeyError as e:
self.logger.error(f"[{strategy_name}] 消息缺少必要字段: {e}")
except Exception as e:
self.logger.error(
f"[{strategy_name}] 消息处理异常: {str(e)}", exc_info=True
)
def _execute_buy(self, unit, strategy_name, data):
strat_cfg = self.config['strategies'][strategy_name]
strat_cfg = self.config["strategies"][strategy_name]
# 1. 槽位校验
if data['total_slots'] != strat_cfg['total_slots']:
self.logger.error(f"[{strategy_name}] 信号槽位({data['total_slots']})与配置({strat_cfg['total_slots']})不符")
if data["total_slots"] != strat_cfg["total_slots"]:
self.logger.error(
f"[{strategy_name}] 信号槽位({data['total_slots']})与配置({strat_cfg['total_slots']})不符"
)
return
# 2. 持仓数检查
if self.pos_manager.get_holding_count(strategy_name) >= strat_cfg['total_slots']:
if (
self.pos_manager.get_holding_count(strategy_name)
>= strat_cfg["total_slots"]
):
return
try:
asset = unit.xt_trader.query_stock_asset(unit.acc_obj)
# 计算该终端的总槽位之和
terminal_strategies = self.get_strategies_by_terminal(unit.qmt_id)
# 计算加权槽位总和(支持策略权重配置)
# 权重默认为 1支持通过 weight 字段调整资金分配比例
# 示例strategies = {"strategy_a": {"total_slots": 5, "weight": 1}, "strategy_b": {"total_slots": 5, "weight": 2}}
total_weighted_slots = sum(
self.config['strategies'][s].get('total_slots', 1) * self.config['strategies'][s].get('weight', 1)
self.config["strategies"][s].get("total_slots", 1)
* self.config["strategies"][s].get("weight", 1)
for s in terminal_strategies
)
if not asset or total_weighted_slots <= 0: return
if not asset or total_weighted_slots <= 0:
return
# 获取当前策略的权重
weight = strat_cfg.get('weight', 1)
weight = strat_cfg.get("weight", 1)
# 4. 资金加权分配 (基于该终端总资产)
total_equity = asset.cash + asset.market_value
target_amt = total_equity * weight / total_weighted_slots
actual_amt = min(target_amt, asset.cash * 0.98) # 预留手续费滑点
if actual_amt < 2000:
self.logger.warning(f"[{strategy_name}] 单笔预算 {actual_amt:.2f} 不足 2000 元,取消买入")
self.logger.warning(
f"[{strategy_name}] 单笔预算 {actual_amt:.2f} 不足 2000 元,取消买入"
)
return
# 4. 价格与股数
offset = strat_cfg.get('execution', {}).get('buy_price_offset', 0.0)
price = round(float(data['price']) + offset, 3)
offset = strat_cfg.get("execution", {}).get("buy_price_offset", 0.0)
price = round(float(data["price"]) + offset, 3)
vol = int(actual_amt / (price if price > 0 else 1.0) / 100) * 100
if vol < 100: return
oid = unit.xt_trader.order_stock(unit.acc_obj, data['stock_code'], xtconstant.STOCK_BUY,
vol, xtconstant.FIX_PRICE, price, strategy_name, 'PyBuy')
if vol < 100:
return
oid = unit.xt_trader.order_stock(
unit.acc_obj,
data["stock_code"],
xtconstant.STOCK_BUY,
vol,
xtconstant.FIX_PRICE,
price,
strategy_name,
"PyBuy",
)
if oid != -1:
unit.order_cache[oid] = (strategy_name, data['stock_code'], 'BUY')
self.pos_manager.mark_holding(strategy_name, data['stock_code'])
self.logger.info(f"√√√ [{unit.alias}] {strategy_name} 下单买入: {data['stock_code']} {vol}股 @ {price}")
except:
unit.order_cache[oid] = (strategy_name, data["stock_code"], "BUY")
self.pos_manager.mark_holding(strategy_name, data["stock_code"])
self.logger.info(
f"√√√ [{unit.alias}] {strategy_name} 下单买入: {data['stock_code']} {vol}股 @ {price}"
)
except:
self.logger.error(traceback.format_exc())
def _execute_sell(self, unit, strategy_name, data):
v_vol = self.pos_manager.get_position(strategy_name, data['stock_code'])
if v_vol <= 0: return
# 1. 查询实盘持仓(一切以实盘为准)
real_pos = unit.xt_trader.query_stock_positions(unit.acc_obj)
rp = next((p for p in real_pos if p.stock_code == data['stock_code']), None) if real_pos else None
rp = (
next((p for p in real_pos if p.stock_code == data["stock_code"]), None)
if real_pos
else None
)
can_use = rp.can_use_volume if rp else 0
# 取虚拟持仓和实盘可用持仓的最小值
final_vol = min(v_vol, can_use)
# 2. 检查虚拟持仓
v_vol = self.pos_manager.get_position(strategy_name, data["stock_code"])
# 3. 实盘无持仓 -> 拒绝卖出(清理幽灵持仓)
if can_use <= 0:
self.logger.warning(
f"[{strategy_name}] 卖出拦截: {data['stock_code']} 实盘无可用持仓"
)
# 如果虚拟持仓存在但实盘已清仓,清理幽灵持仓
if v_vol > 0:
self.pos_manager.force_delete(strategy_name, data["stock_code"])
self.logger.info(
f"[{strategy_name}] 已清理幽灵持仓: {data['stock_code']} 虚拟{v_vol}"
)
return
# 4. 实盘有持仓 -> 必须卖出(取虚拟和实盘的最小值,虚拟无持仓则取实盘)
if v_vol <= 0:
self.logger.warning(
f"[{strategy_name}] 卖出提醒: {data['stock_code']} 虚拟无持仓但实盘有{can_use}股,以实盘为准执行卖出"
)
final_vol = min(v_vol, can_use) if v_vol > 0 else can_use
if final_vol <= 0:
self.logger.warning(f"[{strategy_name}] 卖出拦截: {data['stock_code']} 实盘无可用持仓")
self.logger.warning(
f"[{strategy_name}] 卖出拦截: {data['stock_code']} 计算后卖出量为0"
)
return
try:
offset = self.config['strategies'][strategy_name].get('execution', {}).get('sell_price_offset', 0.0)
price = round(float(data['price']) + offset, 3)
offset = (
self.config["strategies"][strategy_name]
.get("execution", {})
.get("sell_price_offset", 0.0)
)
price = round(float(data["price"]) + offset, 3)
oid = unit.xt_trader.order_stock(unit.acc_obj, data['stock_code'], xtconstant.STOCK_SELL,
final_vol, xtconstant.FIX_PRICE, price, strategy_name, 'PySell')
oid = unit.xt_trader.order_stock(
unit.acc_obj,
data["stock_code"],
xtconstant.STOCK_SELL,
final_vol,
xtconstant.FIX_PRICE,
price,
strategy_name,
"PySell",
)
if oid != -1:
unit.order_cache[oid] = (strategy_name, data['stock_code'], 'SELL')
self.logger.info(f"√√√ [{unit.alias}] {strategy_name} 下单卖出: {data['stock_code']} {final_vol}股 @ {price}")
unit.order_cache[oid] = (strategy_name, data["stock_code"], "SELL")
self.logger.info(
f"√√√ [{unit.alias}] {strategy_name} 下单卖出: {data['stock_code']} {final_vol}股 @ {price}"
)
except:
self.logger.error(traceback.format_exc())
@@ -607,14 +809,16 @@ class MultiEngineManager:
# 先检查回调状态
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}")
logging.getLogger("QMT_Engine").warning(
f"终端 {self.alias} 物理连接验证失败: {e}"
)
return False
def get_all_status(self) -> List[TerminalStatus]:
@@ -631,35 +835,39 @@ class MultiEngineManager:
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
))
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")
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:
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
@@ -670,4 +878,4 @@ class MultiEngineManager:
def stop(self):
self.is_running = False
for u in self.units.values():
u.cleanup()
u.cleanup()