- 新增 config_models.py: 使用 Pydantic 提供强类型配置校验 - QMTConfig, QMTTerminalConfig, StrategyConfig 等数据模型 - 支持 slots/percentage 两种下单模式 - 兼容旧版配置格式迁移 - 新增 validate_config.py: 配置检测 CLI 工具 - 重构 TradingUnit 和 MultiEngineManager 使用新配置模型 - 新增百分比模式买卖逻辑 (_execute_percentage_buy/sell) - 完善日志记录和错误处理 - 删除 TODO_FIX.md: 清理已完成的缺陷修复任务清单
2085 lines
84 KiB
Python
2085 lines
84 KiB
Python
# 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()
|