Files
NewStock/qmt/qmt_engine.py
liaozhaorun 555e7ebb43 feat(qmt): 新增 Pydantic 配置模型并重构引擎架构
- 新增 config_models.py: 使用 Pydantic 提供强类型配置校验
  - QMTConfig, QMTTerminalConfig, StrategyConfig 等数据模型
  - 支持 slots/percentage 两种下单模式
  - 兼容旧版配置格式迁移
- 新增 validate_config.py: 配置检测 CLI 工具
- 重构 TradingUnit 和 MultiEngineManager 使用新配置模型
- 新增百分比模式买卖逻辑 (_execute_percentage_buy/sell)
- 完善日志记录和错误处理
- 删除 TODO_FIX.md: 清理已完成的缺陷修复任务清单
2026-02-25 21:48:10 +08:00

2085 lines
84 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# coding:utf-8
import time
import datetime
import traceback
import sys
import json
import os
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
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant
# 导入 Redis Stream 消息处理器和日志模块
try:
from .message_processor import StreamMessageProcessor
from .logger import QMTLogger
from .config_models import QMTConfig, QMTTerminalConfig, StrategyConfig, load_config, ConfigError
except ImportError:
# 当作为脚本直接运行时
from message_processor import StreamMessageProcessor
from logger import QMTLogger
from config_models import QMTConfig, QMTTerminalConfig, StrategyConfig, load_config, ConfigError
import time
import datetime
import traceback
import sys
import json
import os
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
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant
# 导入 Redis Stream 消息处理器和日志模块
try:
from .message_processor import StreamMessageProcessor
from .logger import QMTLogger
except ImportError:
# 当作为脚本直接运行时
from message_processor import StreamMessageProcessor
from logger import QMTLogger
# ================= 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
is_connected: bool
callback_connected: bool
physical_connected: bool
last_heartbeat: str
# ================= 1. 业务逻辑辅助类 =================
class PositionManager:
"""Redis 虚拟持仓管理(全局单例)"""
def __init__(self, r_client):
self.r = r_client
def _get_key(self, strategy_name):
return f"POS:{strategy_name}"
def mark_holding(self, strategy_name, code):
self.r.hsetnx(self._get_key(strategy_name), code, 0)
def rollback_holding(self, strategy_name, code):
key = self._get_key(strategy_name)
val = self.r.hget(key, code)
if val is not None and int(val) == 0:
self.r.hdel(key, code)
def update_actual_volume(self, strategy_name, code, delta_vol):
key = self._get_key(strategy_name)
new_vol = self.r.hincrby(key, code, int(delta_vol))
if new_vol <= 0:
self.r.hdel(key, code)
new_vol = 0
return new_vol
def get_position(self, strategy_name, code):
vol = self.r.hget(self._get_key(strategy_name), code)
return int(vol) if vol else 0
def get_holding_count(self, strategy_name):
return self.r.hlen(self._get_key(strategy_name))
def get_all_virtual_positions(self, strategy_name):
return self.r.hgetall(self._get_key(strategy_name))
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
def run_settlement(self):
trader = self.unit.xt_trader
acc = self.unit.acc_obj
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 {}
)
manager = MultiEngineManager()
strategies = manager.get_strategies_by_terminal(self.unit.qmt_id)
for s_name in strategies:
virtual = manager.pos_manager.get_all_virtual_positions(s_name)
for code, v_str in virtual.items():
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]
)
self.has_settled = True
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 Exception as e:
self.logger.warning(f"读取配置文件失败,将创建新配置: {e}")
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"[AutoReconnectScheduler] 执行定时重连任务,时间: {self.reconnect_time}"
)
# 设置重连中标志位,通知主循环暂停健康检查重连
self.manager.is_scheduled_reconnecting = True
try:
# 强制断开所有终端连接(无论当前是否在线)
self.logger.info("[AutoReconnectScheduler] 正在断开所有终端连接...")
for unit in self.manager.units.values():
try:
if unit.xt_trader:
unit.cleanup()
self.logger.info(
f"[AutoReconnectScheduler] 已断开终端 {unit.alias} 的连接"
)
except Exception as e:
self.logger.warning(
f"[AutoReconnectScheduler] 断开终端 {unit.alias} 失败: {e}"
)
# 等待几秒后重新连接(固定等待时间)
self.logger.info("[AutoReconnectScheduler] 等待 3 秒后重新连接...")
time.sleep(3)
# 重新连接所有终端
self.logger.info("[AutoReconnectScheduler] 正在重新连接所有终端...")
success_count = 0
for unit in self.manager.units.values():
if unit.connect():
success_count += 1
self.logger.info(
f"[AutoReconnectScheduler] 终端 {unit.alias} 重连成功"
)
else:
self.logger.warning(
f"[AutoReconnectScheduler] 终端 {unit.alias} 重连失败"
)
self.logger.info(
f"[AutoReconnectScheduler] 定时重连完成: {success_count}/{len(self.manager.units)} 个终端重连成功"
)
finally:
# 确保无论成功与否都重置标志位
self.manager.is_scheduled_reconnecting = False
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):
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}) 物理连接断开"
)
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
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:
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":
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: QMTTerminalConfig):
self.qmt_id = t_cfg.qmt_id
self.alias = t_cfg.alias or t_cfg.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.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} 的旧资源..."
)
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}"
)
def connect(self):
"""连接 QMT 终端"""
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})"
)
return True
return False
except Exception as e:
logging.getLogger("QMT_Engine").error(
f"终端 {self.alias} 连接异常: {repr(e)}"
)
return False
# ================= 3. 总控中心 (MultiEngineManager) =================
class MultiEngineManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if hasattr(self, "_initialized"):
return
self.units: Dict[str, TradingUnit] = {}
self.config: Optional[QMTConfig] = None
self.is_running = True
self.start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.is_scheduled_reconnecting = False # 定时重连调度器正在执行标志
self._initialized = True
# Stream 处理器和日志器将在 initialize 中创建
self.stream_processor: Optional[StreamMessageProcessor] = None
self.qmt_logger: Optional[QMTLogger] = None
self.r = None
self.pos_manager = None
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.is_scheduled_reconnecting = False # 定时重连调度器正在执行标志
self._initialized = True
# Stream 处理器和日志器将在 initialize 中创建
self.stream_processor: Optional[StreamMessageProcessor] = None
self.qmt_logger: Optional[QMTLogger] = None
def initialize(self, config_file="config.json"):
self._setup_logger() # 先初始化 logger
# 使用新的配置模型加载配置
try:
self.config = load_config(config_file)
self.logger.info(f"✅ 配置加载成功: {config_file}")
self.logger.info(f" - 终端数量: {len(self.config.qmt_terminals)}")
self.logger.info(f" - 策略数量: {len(self.config.strategies)}")
except ConfigError as e:
self.logger.error(f"❌ 配置错误: {e}")
raise
except Exception as e:
self.logger.error(f"❌ 配置加载失败: {e}")
raise
# 从 .env.local 加载 Redis 配置
redis_config = self._load_redis_config()
self.r = redis.Redis(**redis_config, decode_responses=True)
self.pos_manager = PositionManager(self.r)
# 初始化 Redis Stream 处理器
self.stream_processor = StreamMessageProcessor(redis_client=self.r)
self.qmt_logger = QMTLogger(name="QMT_Engine_Stream")
self.logger.info("Redis Stream 处理器初始化完成")
for t_cfg in self.config.qmt_terminals:
unit = TradingUnit(t_cfg)
unit.connect()
self.units[unit.qmt_id] = unit
def _load_redis_config(self) -> Dict[str, Any]:
"""从 .env.local 加载 Redis 配置"""
# 尝试加载 python-dotenv
try:
from dotenv import load_dotenv
# 尝试多个路径
env_paths = [
os.path.join(os.path.dirname(os.path.abspath(__file__)), "config", ".env.local"),
os.path.join(os.path.dirname(__file__), "config", ".env.local"),
os.path.join(os.path.dirname(__file__), "..", "config", ".env.local"),
"/qmt/config/.env.local",
"config/.env.local",
]
loaded = False
for env_path in env_paths:
if os.path.exists(env_path):
load_dotenv(env_path)
self.logger.info(f"[Config] 加载 Redis 配置: {env_path}")
loaded = True
break
if not loaded:
self.logger.warning("[Config] 警告: 未找到 .env.local 文件,使用默认配置")
except ImportError:
self.logger.warning("[Config] 警告: 未安装 python-dotenv使用默认配置")
# 从环境变量读取 Redis 配置
redis_config = {
"host": os.getenv("REDIS_HOST", "localhost"),
"port": int(os.getenv("REDIS_PORT", "6379")),
"password": os.getenv("REDIS_PASSWORD") or None,
"db": int(os.getenv("REDIS_DB", "0")),
}
return redis_config
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"
)
self.logger = logging.getLogger("QMT_Engine")
self.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")
fh.setFormatter(fmt)
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(fmt)
self.logger.addHandler(fh)
self.logger.addHandler(sh)
def get_strategies_by_terminal(self, qmt_id):
if not self.config:
return []
return self.config.get_strategies_by_terminal(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")
self.logger.info(">>> 多终端交易主循环线程已启动 <<<")
last_check = 0
while self.is_running:
try:
now_t = time.time()
curr_hms = datetime.datetime.now().strftime("%H%M%S")
# --- 健康检查与自动修复 ---
if now_t - last_check > 25:
last_check = now_t
for unit in self.units.values():
is_unit_alive = False
if unit.xt_trader and unit.acc_obj:
try:
# 物理探测:通过查资产确认连接有效性
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")
)
# 状态修正物理通但逻辑False时自动拉回
if unit.callback and not unit.callback.is_connected:
unit.callback.is_connected = True
self.logger.info(
f"✅ 修正终端 {unit.alias} 状态为在线"
)
except Exception as e:
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 self.is_scheduled_reconnecting:
self.logger.info(
f"⏳ 定时重连调度器正在执行,跳过健康检查重连..."
)
else:
# 检查重连次数是否超过限制
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}),跳过重连操作..."
)
# --- 交易逻辑处理 ---
is_trading = ("091500" <= curr_hms <= "113030") or (
"130000" <= curr_hms <= "150030"
)
if is_trading:
for s_name in self.config.strategies.keys():
self.process_route(s_name)
self.process_route(s_name)
# --- 收盘结算与标志位重置 ---
elif "150500" <= curr_hms <= "151500":
for unit in self.units.values():
if unit.settler and not unit.settler.has_settled:
unit.settler.run_settlement()
elif "153000" <= curr_hms <= "160000":
for unit in self.units.values():
if unit.settler:
unit.settler.reset_flag()
time.sleep(1 if is_trading else 5)
except:
self.logger.error("主循环异常:")
self.logger.error(traceback.format_exc())
time.sleep(10)
def process_route(self, strategy_name):
"""处理策略消息路由 - 使用 Redis Stream
从 Redis Stream 消费消息,处理成功后 ACK失败则进入失败队列。
"""
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.warning(f"[{strategy_name}] 策略配置不存在")
return
unit = self.units.get(strat_cfg.qmt_id)
if not unit or not unit.callback or not unit.callback.is_connected:
return
unit = self.units.get(strat_cfg.get("qmt_id"))
if not unit or not unit.callback or not unit.callback.is_connected:
return
# 使用 StreamMessageProcessor 消费消息
if not self.stream_processor:
self.logger.error("[process_route] Stream处理器未初始化")
return
try:
# 消费消息 (非阻塞,立即返回)
messages = self.stream_processor.consume_message(
strategy_name=strategy_name,
consumer_id=None, # 自动生成
is_backtest=False,
block_ms=100, # 短阻塞,快速返回
)
if not messages:
return
# 处理每条消息
for msg_id, data in messages:
try:
# 1. 记录消息接收日志
stream_key = f"qmt:{strategy_name}:real"
self.qmt_logger.log_message_receive(stream_key, msg_id, data)
# 2. 严格校验消息日期
msg_date = data.get("timestamp", "").split(" ")[0]
today_str = datetime.date.today().strftime("%Y-%m-%d")
if msg_date != today_str:
self.qmt_logger.log_validation(
validation_type="date_check",
strategy_name=strategy_name,
details={"msg_date": msg_date, "today": today_str},
result=False,
)
# 日期不符的消息也 ACK避免重复处理
self.stream_processor.ack_message(
strategy_name, msg_id, is_backtest=False
)
continue
self.qmt_logger.log_validation(
validation_type="date_check",
strategy_name=strategy_name,
details={"msg_date": msg_date},
result=True,
)
# 3. 执行交易动作
action = data.get("action")
# 获取策略配置,确定下单模式
# 获取策略配置,确定下单模式
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.warning(f"[{strategy_name}] 策略配置不存在")
order_mode = strat_cfg.order_mode
order_mode = strat_cfg.order_mode
order_mode = strat_cfg.get("order_mode", "slots")
if action == "BUY":
self.qmt_logger.log_validation(
validation_type="action_check",
strategy_name=strategy_name,
details={"action": "BUY", "code": data.get("stock_code"), "order_mode": order_mode},
result=True,
)
# 根据下单模式执行相应逻辑
if order_mode == "percentage":
self._execute_percentage_buy(unit, strategy_name, data)
else:
self._execute_buy(unit, strategy_name, data)
elif action == "SELL":
self.qmt_logger.log_validation(
validation_type="action_check",
strategy_name=strategy_name,
details={"action": "SELL", "code": data.get("stock_code"), "order_mode": order_mode},
result=True,
)
# 根据下单模式执行相应逻辑
if order_mode == "percentage":
self._execute_percentage_sell(unit, strategy_name, data)
else:
self._execute_sell(unit, strategy_name, data)
else:
self.qmt_logger.log_validation(
validation_type="action_check",
strategy_name=strategy_name,
details={"action": action},
result=False,
)
self.logger.warning(f"[{strategy_name}] 未知动作: {action}")
# 4. 确认消息已处理
ack_success = self.stream_processor.ack_message(
strategy_name, msg_id, is_backtest=False
)
self.qmt_logger.log_ack(stream_key, msg_id, ack_success)
except json.JSONDecodeError as e:
error_msg = f"JSON解析失败: {e}"
self.logger.error(
f"[{strategy_name}] {error_msg}, 消息ID: {msg_id}"
)
self.qmt_logger.log_failure(stream_key, msg_id, error_msg)
# 解析失败的消息也发送 ACK避免死循环
self.stream_processor.ack_message(
strategy_name, msg_id, is_backtest=False
)
except KeyError as e:
error_msg = f"消息缺少必要字段: {e}"
self.logger.error(f"[{strategy_name}] {error_msg}")
self.qmt_logger.log_failure(stream_key, msg_id, error_msg)
self.stream_processor.send_to_failure_queue(
strategy_name, data, error_msg
)
# 发送失败队列后 ACK
self.stream_processor.ack_message(
strategy_name, msg_id, is_backtest=False
)
except Exception as e:
error_msg = f"消息处理异常: {str(e)}"
self.logger.error(f"[{strategy_name}] {error_msg}", exc_info=True)
self.qmt_logger.log_failure(stream_key, msg_id, error_msg)
# 异常消息进入失败队列
self.stream_processor.send_to_failure_queue(
strategy_name, data, error_msg
)
# 发送失败队列后 ACK
self.stream_processor.ack_message(
strategy_name, msg_id, is_backtest=False
)
except Exception as e:
self.logger.error(f"[process_route] 消费消息异常: {str(e)}", exc_info=True)
def _execute_buy(self, unit, strategy_name, data):
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
return
# 1. 槽位校验
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
):
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.get_strategy(s).total_slots * self.config.get_strategy(s).weight
for s in terminal_strategies
if self.config.get_strategy(s)
)
if not asset or total_weighted_slots <= 0:
return
# 获取当前策略的权重
weight = strat_cfg.weight
# 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 元,取消买入"
)
return
# 4. 价格与股数
offset = strat_cfg.execution.buy_price_offset
price = round(float(data["price"]) + offset, 3)
vol = int(actual_amt / (price if price > 0 else 1.0) / 100) * 100
if vol < 100:
return
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
)
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}"
)
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
def _execute_sell(self, unit, strategy_name, data):
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
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
)
can_use = rp.can_use_volume if rp else 0
# 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']} 计算后卖出量为0"
)
return
try:
offset = strat_cfg.execution.sell_price_offset
price = round(float(data["price"]) + offset, 3)
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
)
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}"
)
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
def _execute_percentage_buy(self, unit, strategy_name, data):
"""处理百分比模式的买入逻辑"""
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
return
# 获取目标持仓百分比
position_pct = float(data.get("position_pct", 0))
if position_pct <= 0:
self.logger.warning(f"[{strategy_name}] 百分比模式买入: position_pct 无效 ({position_pct})")
return
self.logger.info(f"[{strategy_name}] [百分比模式] 处理买入: {data['stock_code']}, 目标占比: {position_pct}")
try:
asset = unit.xt_trader.query_stock_asset(unit.acc_obj)
if not asset:
self.logger.error(f"[{strategy_name}] API 错误: query_stock_asset 返回 None")
return
total_asset = asset.total_asset
available_cash = asset.cash
self.logger.info(f"[{strategy_name}] [百分比模式] 账户总资产: {total_asset:.2f}, 可用资金: {available_cash:.2f}")
# 计算目标金额
target_amount = total_asset * position_pct
actual_amount = min(target_amount, available_cash * 0.98) # 预留手续费滑点
self.logger.info(f"[{strategy_name}] [百分比模式] 目标金额: {target_amount:.2f}, 实际可用: {actual_amount:.2f}")
# 检查最小金额限制
if actual_amount < 2000:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截买入: 金额过小 ({actual_amount:.2f} < 2000)")
return
# 价格校验
price = float(data.get("price", 0))
offset = strat_cfg.execution.buy_price_offset
price = round(price + offset, 3)
if price <= 0:
self.logger.warning(f"[{strategy_name}] [百分比模式] 价格异常: {price}强制设为1.0")
price = 1.0
# 计算股数
vol = int(actual_amount / price / 100) * 100
self.logger.info(f"[{strategy_name}] [百分比模式] 计算股数: 资金{actual_amount:.2f} / 价格{price} -> {vol}")
if vol < 100:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截买入: 股数不足 100 ({vol})")
return
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
)
oid = unit.xt_trader.order_stock(
unit.acc_obj,
data["stock_code"],
xtconstant.STOCK_BUY,
vol,
xtconstant.FIX_PRICE,
price,
strategy_name,
"PyBuyPct",
)
if oid != -1:
unit.order_cache[oid] = (strategy_name, data["stock_code"], "BUY")
self.logger.info(f"√√√ [{unit.alias}] [{strategy_name}] [百分比模式] 下单买入: {data['stock_code']} {vol}股 @ {price}")
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] [百分比模式] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
def _execute_percentage_sell(self, unit, strategy_name, data):
"""处理百分比模式的卖出逻辑(清仓)"""
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
return
self.logger.info(f"[{strategy_name}] [百分比模式] 处理卖出: {data['stock_code']} (清仓)")
try:
# 查询实盘持仓
real_pos = unit.xt_trader.query_stock_positions(unit.acc_obj)
if real_pos is None:
self.logger.error(f"[{strategy_name}] [百分比模式] API 错误: query_stock_positions 返回 None")
return
rp = next((p for p in real_pos if p.stock_code == data["stock_code"]), None)
can_use = rp.can_use_volume if rp else 0
self.logger.info(f"[{strategy_name}] [百分比模式] 股票 {data['stock_code']} 实盘可用持仓: {can_use}")
if can_use <= 0:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截卖出: 无可用持仓")
return
# 执行清仓
price = float(data.get("price", 0))
offset = strat_cfg.execution.sell_price_offset
price = round(price + offset, 3)
self.logger.info(f"[{strategy_name}] [百分比模式] 执行清仓: {data['stock_code']} @ {price}, 数量: {can_use}")
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
)
oid = unit.xt_trader.order_stock(
unit.acc_obj,
data["stock_code"],
xtconstant.STOCK_SELL,
can_use,
xtconstant.FIX_PRICE,
price,
strategy_name,
"PySellPct",
)
if oid != -1:
unit.order_cache[oid] = (strategy_name, data["stock_code"], "SELL")
self.logger.info(f"√√√ [{unit.alias}] [{strategy_name}] [百分比模式] 下单卖出: {data['stock_code']} {can_use}股 @ {price}")
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] [百分比模式] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
def _execute_sell(self, unit, strategy_name, data):
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
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
)
can_use = rp.can_use_volume if rp else 0
# 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']} 计算后卖出量为0"
)
return
try:
offset = strat_cfg.execution.sell_price_offset
price = round(float(data["price"]) + offset, 3)
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
)
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}"
)
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
"""处理百分比模式的卖出逻辑(清仓)"""
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
return
self.logger.info(f"[{strategy_name}] [百分比模式] 处理卖出: {data['stock_code']} (清仓)")
# 1. 槽位校验
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"]
):
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)
for s in terminal_strategies
)
if not asset or total_weighted_slots <= 0:
return
# 获取当前策略的权重
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 元,取消买入"
)
return
# 4. 价格与股数
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
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
)
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}"
)
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
def _execute_sell(self, unit, strategy_name, data):
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
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
)
can_use = rp.can_use_volume if rp else 0
# 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']} 计算后卖出量为0"
)
return
try:
offset = strat_cfg.execution.sell_price_offset
price = round(float(data["price"]) + offset, 3)
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
)
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}"
)
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
# 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
)
can_use = rp.can_use_volume if rp else 0
# 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']} 计算后卖出量为0"
)
return
try:
offset = (
self.config["strategies"][strategy_name]
.get("execution", {})
.get("sell_price_offset", 0.0)
)
price = round(float(data["price"]) + offset, 3)
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
)
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}"
)
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=final_vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
def _execute_percentage_buy(self, unit, strategy_name, data):
"""处理百分比模式的买入逻辑"""
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
return
# 获取目标持仓百分比
position_pct = float(data.get("position_pct", 0))
if position_pct <= 0:
self.logger.warning(f"[{strategy_name}] 百分比模式买入: position_pct 无效 ({position_pct})")
return
self.logger.info(f"[{strategy_name}] [百分比模式] 处理买入: {data['stock_code']}, 目标占比: {position_pct}")
try:
asset = unit.xt_trader.query_stock_asset(unit.acc_obj)
if not asset:
self.logger.error(f"[{strategy_name}] API 错误: query_stock_asset 返回 None")
return
total_asset = asset.total_asset
available_cash = asset.cash
self.logger.info(f"[{strategy_name}] [百分比模式] 账户总资产: {total_asset:.2f}, 可用资金: {available_cash:.2f}")
# 计算目标金额
target_amount = total_asset * position_pct
actual_amount = min(target_amount, available_cash * 0.98) # 预留手续费滑点
self.logger.info(f"[{strategy_name}] [百分比模式] 目标金额: {target_amount:.2f}, 实际可用: {actual_amount:.2f}")
# 检查最小金额限制
if actual_amount < 2000:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截买入: 金额过小 ({actual_amount:.2f} < 2000)")
return
# 价格校验
price = float(data.get("price", 0))
offset = strat_cfg.execution.buy_price_offset
price = round(price + offset, 3)
if price <= 0:
self.logger.warning(f"[{strategy_name}] [百分比模式] 价格异常: {price}强制设为1.0")
price = 1.0
# 计算股数
vol = int(actual_amount / price / 100) * 100
self.logger.info(f"[{strategy_name}] [百分比模式] 计算股数: 资金{actual_amount:.2f} / 价格{price} -> {vol}")
if vol < 100:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截买入: 股数不足 100 ({vol})")
return
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
)
oid = unit.xt_trader.order_stock(
unit.acc_obj,
data["stock_code"],
xtconstant.STOCK_BUY,
vol,
xtconstant.FIX_PRICE,
price,
strategy_name,
"PyBuyPct",
)
if oid != -1:
unit.order_cache[oid] = (strategy_name, data["stock_code"], "BUY")
self.logger.info(f"√√√ [{unit.alias}] [{strategy_name}] [百分比模式] 下单买入: {data['stock_code']} {vol}股 @ {price}")
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] [百分比模式] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
def _execute_percentage_sell(self, unit, strategy_name, data):
"""处理百分比模式的卖出逻辑(清仓)"""
strat_cfg = self.config.get_strategy(strategy_name)
if not strat_cfg:
self.logger.error(f"[{strategy_name}] 策略配置不存在")
return
self.logger.info(f"[{strategy_name}] [百分比模式] 处理卖出: {data['stock_code']} (清仓)")
try:
# 查询实盘持仓
real_pos = unit.xt_trader.query_stock_positions(unit.acc_obj)
if real_pos is None:
self.logger.error(f"[{strategy_name}] [百分比模式] API 错误: query_stock_positions 返回 None")
return
rp = next((p for p in real_pos if p.stock_code == data["stock_code"]), None)
can_use = rp.can_use_volume if rp else 0
self.logger.info(f"[{strategy_name}] [百分比模式] 股票 {data['stock_code']} 实盘可用持仓: {can_use}")
if can_use <= 0:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截卖出: 无可用持仓")
return
# 执行清仓
price = float(data.get("price", 0))
offset = strat_cfg.execution.sell_price_offset
price = round(price + offset, 3)
self.logger.info(f"[{strategy_name}] [百分比模式] 执行清仓: {data['stock_code']} @ {price}, 数量: {can_use}")
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
)
oid = unit.xt_trader.order_stock(
unit.acc_obj,
data["stock_code"],
xtconstant.STOCK_SELL,
can_use,
xtconstant.FIX_PRICE,
price,
strategy_name,
"PySellPct",
)
if oid != -1:
unit.order_cache[oid] = (strategy_name, data["stock_code"], "SELL")
self.logger.info(f"√√√ [{unit.alias}] [{strategy_name}] [百分比模式] 下单卖出: {data['stock_code']} {can_use}股 @ {price}")
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] [百分比模式] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
"""处理百分比模式的买入逻辑"""
strat_cfg = self.config["strategies"][strategy_name]
# 获取目标持仓百分比
position_pct = float(data.get("position_pct", 0))
if position_pct <= 0:
self.logger.warning(f"[{strategy_name}] 百分比模式买入: position_pct 无效 ({position_pct})")
return
self.logger.info(f"[{strategy_name}] [百分比模式] 处理买入: {data['stock_code']}, 目标占比: {position_pct}")
try:
asset = unit.xt_trader.query_stock_asset(unit.acc_obj)
if not asset:
self.logger.error(f"[{strategy_name}] API 错误: query_stock_asset 返回 None")
return
total_asset = asset.total_asset
available_cash = asset.cash
self.logger.info(f"[{strategy_name}] [百分比模式] 账户总资产: {total_asset:.2f}, 可用资金: {available_cash:.2f}")
# 计算目标金额
target_amount = total_asset * position_pct
actual_amount = min(target_amount, available_cash * 0.98) # 预留手续费滑点
self.logger.info(f"[{strategy_name}] [百分比模式] 目标金额: {target_amount:.2f}, 实际可用: {actual_amount:.2f}")
# 检查最小金额限制
if actual_amount < 2000:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截买入: 金额过小 ({actual_amount:.2f} < 2000)")
return
# 价格校验
price = float(data.get("price", 0))
offset = strat_cfg.get("execution", {}).get("buy_price_offset", 0.0)
price = round(price + offset, 3)
if price <= 0:
self.logger.warning(f"[{strategy_name}] [百分比模式] 价格异常: {price}强制设为1.0")
price = 1.0
# 计算股数
vol = int(actual_amount / price / 100) * 100
self.logger.info(f"[{strategy_name}] [百分比模式] 计算股数: 资金{actual_amount:.2f} / 价格{price} -> {vol}")
if vol < 100:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截买入: 股数不足 100 ({vol})")
return
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
)
oid = unit.xt_trader.order_stock(
unit.acc_obj,
data["stock_code"],
xtconstant.STOCK_BUY,
vol,
xtconstant.FIX_PRICE,
price,
strategy_name,
"PyBuyPct",
)
if oid != -1:
unit.order_cache[oid] = (strategy_name, data["stock_code"], "BUY")
self.logger.info(f"√√√ [{unit.alias}] [{strategy_name}] [百分比模式] 下单买入: {data['stock_code']} {vol}股 @ {price}")
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] [百分比模式] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="BUY",
volume=vol,
price=price,
error=error_msg,
)
except:
self.logger.error(traceback.format_exc())
def _execute_percentage_sell(self, unit, strategy_name, data):
"""处理百分比模式的卖出逻辑(清仓)"""
self.logger.info(f"[{strategy_name}] [百分比模式] 处理卖出: {data['stock_code']} (清仓)")
try:
# 查询实盘持仓
real_pos = unit.xt_trader.query_stock_positions(unit.acc_obj)
if real_pos is None:
self.logger.error(f"[{strategy_name}] [百分比模式] API 错误: query_stock_positions 返回 None")
return
rp = next((p for p in real_pos if p.stock_code == data["stock_code"]), None)
can_use = rp.can_use_volume if rp else 0
self.logger.info(f"[{strategy_name}] [百分比模式] 股票 {data['stock_code']} 实盘可用持仓: {can_use}")
if can_use <= 0:
self.logger.warning(f"[{strategy_name}] [百分比模式] 拦截卖出: 无可用持仓")
return
# 执行清仓
price = float(data.get("price", 0))
offset = self.config["strategies"][strategy_name].get("execution", {}).get("sell_price_offset", 0.0)
price = round(price + offset, 3)
self.logger.info(f"[{strategy_name}] [百分比模式] 执行清仓: {data['stock_code']} @ {price}, 数量: {can_use}")
# 记录订单执行请求
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
)
oid = unit.xt_trader.order_stock(
unit.acc_obj,
data["stock_code"],
xtconstant.STOCK_SELL,
can_use,
xtconstant.FIX_PRICE,
price,
strategy_name,
"PySellPct",
)
if oid != -1:
unit.order_cache[oid] = (strategy_name, data["stock_code"], "SELL")
self.logger.info(f"√√√ [{unit.alias}] [{strategy_name}] [百分比模式] 下单卖出: {data['stock_code']} {can_use}股 @ {price}")
# 记录订单执行成功
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
order_id=oid,
)
else:
error_msg = "下单请求被拒绝 (Result=-1)"
self.logger.error(f"[{strategy_name}] [百分比模式] {error_msg}")
self.qmt_logger.log_order_execution(
strategy_name=strategy_name,
stock_code=data["stock_code"],
action="SELL",
volume=can_use,
price=price,
error=error_msg,
)
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]:
"""获取所有状态,包含物理连接验证"""
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
for u in self.units.values():
u.cleanup()