更新qmt代码,支持多端qmt登录

This commit is contained in:
2026-01-10 04:06:35 +08:00
parent dd60589280
commit 50ee1a5a0a
6 changed files with 487 additions and 495 deletions

View File

@@ -16,32 +16,27 @@ from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant
# ================= 0. Windows 控制台防卡死补丁 =================
# ================= 0. Windows 补丁 =================
try:
import ctypes
kernel32 = ctypes.windll.kernel32
# 禁用快速编辑模式 (0x0040),防止鼠标点击终端导致程序挂起
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 128)
except:
pass
@dataclass
class QMTStatus:
"""系统状态封装"""
is_connected: bool
start_time: str
last_heartbeat: str
class TerminalStatus:
"""终端实例状态封装"""
qmt_id: str
alias: str
account_id: str
is_running: bool
is_connected: bool
last_heartbeat: str
# ================= 1. 虚拟持仓与对账辅助类 =================
# ================= 1. 业务逻辑辅助类 =================
class PositionManager:
"""Redis 持仓管理器:负责维护每个子策略的虚拟仓位"""
"""Redis 虚拟持仓管理(全局单例)"""
def __init__(self, r_client):
self.r = r_client
@@ -49,18 +44,15 @@ class PositionManager:
return f"POS:{strategy_name}"
def mark_holding(self, strategy_name, code):
"""下单时先在 Redis 占位0股占用一个槽位"""
self.r.hsetnx(self._get_key(strategy_name), code, 0)
def rollback_holding(self, strategy_name, code):
"""报单失败时回滚,释放 Redis 占位"""
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):
"""成交回调时更新 Redis 实际股数"""
key = self._get_key(strategy_name)
new_vol = self.r.hincrby(key, code, int(delta_vol))
if new_vol <= 0:
@@ -69,12 +61,10 @@ class PositionManager:
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):
@@ -83,98 +73,120 @@ class PositionManager:
def force_delete(self, strategy_name, code):
self.r.hdel(self._get_key(strategy_name), code)
def clean_stale_placeholders(self, strategy_name, xt_trader, acc):
"""清理长时间未成交且实盘无持仓的占位符"""
try:
key = self._get_key(strategy_name)
all_pos = self.r.hgetall(key)
if not all_pos: return
active_orders = xt_trader.query_stock_orders(acc, cancelable_only=True)
active_codes = [o.stock_code for o in active_orders] if active_orders else []
real_positions = xt_trader.query_stock_positions(acc)
real_holdings = [p.stock_code for p in real_positions if p.volume > 0] if real_positions else []
for code, vol_str in all_pos.items():
if int(vol_str) == 0:
if (code not in real_holdings) and (code not in active_codes):
self.r.hdel(key, code)
except Exception as e:
logging.getLogger("QMT_Engine").error(f"清理占位异常: {e}")
class DailySettlement:
"""收盘对账逻辑"""
def __init__(self, xt_trader, acc, pos_mgr, strategies_config):
self.trader = xt_trader
self.acc = acc
self.pos_mgr = pos_mgr
self.strategies_config = strategies_config
"""终端级别的日终对账"""
def __init__(self, unit):
self.unit = unit
self.has_settled = False
def run_settlement(self):
"""收盘后强制同步 Redis 和实盘持仓"""
real_positions = self.trader.query_stock_positions(self.acc)
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 {}
for strategy in self.strategies_config.keys():
virtual = self.pos_mgr.get_all_virtual_positions(strategy)
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():
v = int(v_str)
if code not in real_pos_map:
self.pos_mgr.force_delete(strategy, code)
elif v == 0 and code in real_pos_map:
self.pos_mgr.update_actual_volume(strategy, code, real_pos_map[code])
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
# ================= 2. 执行单元 (TradingUnit) =================
# ================= 2. QMT 核心引擎 =================
class MyXtQuantTraderCallback(XtQuantTraderCallback):
"""交易回调事件监听"""
def __init__(self, pos_mgr):
self.pos_mgr = pos_mgr
class UnitCallback(XtQuantTraderCallback):
def __init__(self, unit):
self.unit = unit
self.is_connected = False
self.logger = logging.getLogger("QMT_Engine")
def on_disconnected(self):
self.logger.warning(">> 回调通知: 交易端连接断开")
logging.getLogger("QMT_Engine").warning(f"终端 {self.unit.alias}({self.unit.qmt_id}) 物理连接断开")
self.is_connected = False
def on_stock_trade(self, trade):
try:
# QMTEngine 是单例,可直接通过类访问
cache_info = QMTEngine().order_cache.get(trade.order_id)
cache_info = self.unit.order_cache.get(trade.order_id)
if not cache_info: return
strategy, _, action = cache_info
self.logger.info(f">>> [成交] {strategy} | {trade.stock_code} | {trade.traded_volume}")
s_name, _, action = cache_info
manager = MultiEngineManager()
if action == 'BUY':
self.pos_mgr.update_actual_volume(strategy, trade.stock_code, trade.traded_volume)
manager.pos_manager.update_actual_volume(s_name, trade.stock_code, trade.traded_volume)
elif action == 'SELL':
self.pos_mgr.update_actual_volume(strategy, trade.stock_code, -trade.traded_volume)
except:
traceback.print_exc()
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):
self.qmt_id = t_cfg['qmt_id']
self.alias = t_cfg.get('alias', self.qmt_id)
self.path = t_cfg['path']
self.account_id = t_cfg['account_id']
self.account_type = t_cfg['account_type']
self.xt_trader = None
self.acc_obj = None
self.callback = None
self.settler = None
self.order_cache = {}
self.last_heartbeat = "N/A"
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:
pass
def connect(self):
"""连接 QMT 终端"""
self.cleanup() # 启动前先执行清理
try:
self.logger.error(f"下单失败回调: {err.error_msg} ID:{err.order_id}")
cache = QMTEngine().order_cache.get(err.order_id)
if cache and cache[2] == 'BUY':
self.pos_mgr.rollback_holding(cache[0], cache[1])
if err.order_id in QMTEngine().order_cache:
del QMTEngine().order_cache[err.order_id]
except:
pass
# 采用动态 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 QMTEngine:
"""QMT 交易引擎单例"""
class MultiEngineManager:
_instance = None
_lock = threading.Lock()
@@ -187,244 +199,196 @@ class QMTEngine:
def __init__(self):
if hasattr(self, '_initialized'): return
self.logger = None
self.units: Dict[str, TradingUnit] = {}
self.config = {}
self.xt_trader = None
self.acc = None
self.pos_manager = None
self.callback = None
self.is_running = True
self.start_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.last_heartbeat = "Initializing..."
self.order_cache = {} # OrderID -> (Strategy, Code, Action)
self.settler = None
self._initialized = True
def initialize(self, config_file='config.json'):
self._setup_logger()
self.config = self._load_config(config_file)
# 初始化 Redis
try:
self.redis_client = redis.Redis(**self.config['redis'], decode_responses=True)
self.redis_client.ping()
self.pos_manager = PositionManager(self.redis_client)
self.logger.info("Redis 建立连接成功")
except Exception as e:
self.logger.critical(f"Redis 连接失败: {e}")
raise
self._reconnect_qmt()
with open(config_file, 'r', encoding='utf-8') as f:
self.config = json.load(f)
self.r = redis.Redis(**self.config['redis'], decode_responses=True)
self.pos_manager = PositionManager(self.r)
for t_cfg in self.config.get('qmt_terminals', []):
unit = TradingUnit(t_cfg)
unit.connect()
self.units[unit.qmt_id] = unit
def _setup_logger(self):
log_dir = "logs"
if not os.path.exists(log_dir): os.makedirs(log_dir)
log_file = os.path.join(log_dir, f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
self.logger = logging.getLogger("QMT_Engine")
self.logger.setLevel(logging.INFO)
if self.logger.handlers:
for h in self.logger.handlers[:]: h.close(); self.logger.removeHandler(h)
fmt = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(threadName)s] %(message)s', '%Y-%m-%d %H:%M:%S')
logger = logging.getLogger("QMT_Engine")
logger.setLevel(logging.INFO)
# 确保日志流为 UTF-8
fmt = logging.Formatter('[%(asctime)s] [%(threadName)s] %(message)s', '%H:%M:%S')
fh = logging.FileHandler(log_file, mode='a', encoding='utf-8')
fh.setFormatter(fmt)
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(fmt)
self.logger.addHandler(fh);
self.logger.addHandler(sh)
logger.addHandler(fh)
logger.addHandler(sh)
def _load_config(self, config_file):
base = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(
os.path.abspath(__file__))
path = os.path.join(base, config_file)
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def _get_global_total_slots(self):
"""本地引擎计算:所有策略总共分配了多少个槽位"""
return sum(info.get('total_slots', 0) for info in self.config.get('strategies', {}).values())
def _get_execution_setting(self, strategy_name, key, default=None):
"""扩展性配置读取:读取 execution 字典中的参数"""
strat_cfg = self.config.get('strategies', {}).get(strategy_name, {})
exec_cfg = strat_cfg.get('execution', {})
return exec_cfg.get(key, default)
def _reconnect_qmt(self):
q = self.config['qmt']
if self.xt_trader:
try:
self.xt_trader.stop()
except:
pass
self.xt_trader = XtQuantTrader(q['path'], int(time.time()))
self.acc = StockAccount(q['account_id'], q['account_type'])
self.callback = MyXtQuantTraderCallback(self.pos_manager)
self.xt_trader.register_callback(self.callback)
self.xt_trader.start()
if self.xt_trader.connect() == 0:
self.xt_trader.subscribe(self.acc)
self.callback.is_connected = True
self.settler = DailySettlement(self.xt_trader, self.acc, self.pos_manager, self.config['strategies'])
for s in self.config['strategies'].keys():
self.pos_manager.clean_stale_placeholders(s, self.xt_trader, self.acc)
self.logger.info("✅ QMT 终端连接成功")
return True
return False
def process_strategy_queue(self, strategy_name):
"""处理 Redis 中的策略信号"""
queue_key = f"{strategy_name}_real"
msg_json = self.redis_client.lpop(queue_key)
if not msg_json: return
try:
self.redis_client.rpush(f"{queue_key}:history", msg_json)
data = json.loads(msg_json)
if data.get('is_backtest'): return
today = datetime.date.today().strftime('%Y-%m-%d')
if data.get('timestamp', '').split(' ')[0] != today: return
action = data.get('action')
stock = data.get('stock_code')
price = float(data.get('price', 0))
msg_slots = int(data.get('total_slots', 0))
if action == 'BUY':
self._process_buy(strategy_name, stock, price, msg_slots)
elif action == 'SELL':
self._process_sell(strategy_name, stock, price)
except Exception as e:
self.logger.error(f"消息解析异常: {e}")
def _process_buy(self, strategy_name, stock_code, price, msg_slots):
"""核心开仓逻辑"""
# 1. 验证配置
strat_cfg = self.config.get('strategies', {}).get(strategy_name)
if not strat_cfg: return
local_slots = strat_cfg.get('total_slots', 0)
# 2. 安全校验:信号槽位与本地实盘配置必须严格一致
if msg_slots != local_slots:
self.logger.error(f"⚠️ [{strategy_name}] 槽位不匹配!拒绝下单。信号预期:{msg_slots} | 本地配置:{local_slots}")
return
# 3. 检查子策略占用
if self.pos_manager.get_holding_count(strategy_name) >= local_slots:
self.logger.warning(f"[{strategy_name}] 槽位已满,拦截买入 {stock_code}")
return
# 4. 资金计算(由本地引擎统筹全局)
try:
asset = self.xt_trader.query_stock_asset(self.acc)
global_total = self._get_global_total_slots()
if not asset or global_total <= 0: return
# 单笔预算 = (总资产现金 + 持仓市值) / 全局总槽位
total_equity = asset.cash + asset.market_value
target_amt = total_equity / global_total
# 实际可用金额不超过现金的 98%(预留滑点/手续费)
actual_amt = min(target_amt, asset.cash * 0.98)
if actual_amt < 2000:
self.logger.warning(f"[{strategy_name}] 可用金额不足2000取消买入 {stock_code}")
return
# --- 价格偏移处理 ---
offset = self._get_execution_setting(strategy_name, 'buy_price_offset', 0.0)
final_price = round(price + offset, 3)
vol = int(actual_amt / (final_price if final_price > 0 else 1.0) / 100) * 100
if vol < 100: return
oid = self.xt_trader.order_stock(self.acc, stock_code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE,
final_price, strategy_name, 'PyBuy')
if oid != -1:
self.logger.info(
f"√√√ [{strategy_name}] 开仓下单: {stock_code} | 价格:{final_price}(加价:{offset}) | 数量:{vol}")
self.order_cache[oid] = (strategy_name, stock_code, 'BUY')
self.pos_manager.mark_holding(strategy_name, stock_code)
else:
self.logger.error(f"XXX [{strategy_name}] 开仓发单拒绝")
except Exception as e:
self.logger.error(f"买入异常: {e}", exc_info=True)
def _process_sell(self, strategy_name, stock_code, price):
"""核心平仓逻辑"""
v_vol = self.pos_manager.get_position(strategy_name, stock_code)
if v_vol <= 0: return
real_pos = self.xt_trader.query_stock_positions(self.acc)
rp = next((p for p in real_pos if p.stock_code == stock_code), None) if real_pos else None
can_use = rp.can_use_volume if rp else 0
final_vol = min(v_vol, can_use)
if final_vol <= 0:
self.logger.warning(f"[{strategy_name}] {stock_code} 无可用平仓额度 (Redis:{v_vol}, 实盘:{can_use})")
return
# --- 价格偏移处理 ---
offset = self._get_execution_setting(strategy_name, 'sell_price_offset', 0.0)
final_price = round(price + offset, 3)
oid = self.xt_trader.order_stock(self.acc, stock_code, xtconstant.STOCK_SELL, final_vol, xtconstant.FIX_PRICE,
final_price, strategy_name, 'PySell')
if oid != -1:
self.logger.info(
f"√√√ [{strategy_name}] 平仓下单: {stock_code} | 价格:{final_price}(偏移:{offset}) | 数量:{final_vol}")
self.order_cache[oid] = (strategy_name, stock_code, 'SELL')
def get_strategies_by_terminal(self, qmt_id):
return [s for s, cfg in self.config['strategies'].items() if cfg.get('qmt_id') == qmt_id]
def run_trading_loop(self):
"""交易主线程循环"""
self.logger.info(">>> 交易主循环线程已启动 <<<")
self.logger = logging.getLogger("QMT_Engine")
self.logger.info(">>> 多终端交易主循环线程已启动 <<<")
last_check = 0
while self.is_running:
try:
self.last_heartbeat = datetime.datetime.now().strftime('%H:%M:%S')
# 健康检查
if time.time() - last_check > 15:
last_check = time.time()
try:
if not (self.xt_trader and self.acc and self.xt_trader.query_stock_asset(self.acc)):
self._reconnect_qmt()
except:
self._reconnect_qmt()
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:
is_unit_alive = False
# 交易时间判断
curr = datetime.datetime.now().strftime('%H%M%S')
is_trading = ('091500' <= curr <= '113000') or ('130000' <= curr <= '150000')
# 断线重连策略
if not is_unit_alive:
# 避让 QMT 夜间重启高峰 (21:32 - 21:50)
if not ('213200' <= curr_hms <= '215000'):
self.logger.warning(f"🚫 终端 {unit.alias} 物理连接丢失,执行重连...")
unit.connect()
else:
self.logger.info(f"⏳ 处于 QMT 重启时段 ({curr_hms}),跳过重连操作...")
if is_trading and self.callback and self.callback.is_connected:
if self.settler: self.settler.reset_flag()
for s in self.config.get('strategies', {}).keys():
self.process_strategy_queue(s)
elif '150500' <= curr <= '151500' and self.settler and not self.settler.has_settled:
self.settler.run_settlement()
# --- 交易逻辑处理 ---
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)
# --- 收盘结算与标志位重置 ---
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 Exception as e:
self.logger.error(f"主循环异常: {e}")
except:
self.logger.error("主循环异常")
self.logger.error(traceback.format_exc())
time.sleep(10)
# ================= 外部接口 =================
def process_route(self, strategy_name):
strat_cfg = self.config['strategies'].get(strategy_name)
unit = self.units.get(strat_cfg.get('qmt_id'))
if not unit or not unit.callback or not unit.callback.is_connected: return
def get_status(self) -> QMTStatus:
conn = self.callback.is_connected if self.callback else False
return QMTStatus(conn, self.start_time, self.last_heartbeat,
self.acc.account_id if self.acc else "Unknown", self.is_running)
msg_json = self.r.lpop(f"{strategy_name}_real")
if not msg_json: return
try:
data = json.loads(msg_json)
# 严格校验消息日期
if data.get('timestamp', '').split(' ')[0] != datetime.date.today().strftime('%Y-%m-%d'):
return
def get_positions(self) -> Dict[str, Any]:
real = []
if self.callback and self.callback.is_connected:
pos = self.xt_trader.query_stock_positions(self.acc)
if pos:
real = [{"code": p.stock_code, "volume": p.volume, "can_use": p.can_use_volume, "value": p.market_value}
for p in pos if p.volume > 0]
virtual = {s: self.pos_manager.get_all_virtual_positions(s) for s in self.config.get('strategies', {}).keys()}
return {"real_positions": real, "virtual_positions": virtual}
if data['action'] == 'BUY':
self._execute_buy(unit, strategy_name, data)
elif data['action'] == 'SELL':
self._execute_sell(unit, strategy_name, data)
except:
pass
def get_logs(self, lines=50):
log_path = os.path.join("logs", f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
if not os.path.exists(log_path): return ["今日暂无日志"]
with open(log_path, 'r', encoding='utf-8') as f:
return [l.strip() for l in f.readlines()[-lines:]]
def _execute_buy(self, unit, strategy_name, data):
strat_cfg = self.config['strategies'][strategy_name]
# 1. 槽位校验
if data['total_slots'] != strat_cfg['total_slots']:
self.logger.error(f"[{strategy_name}] 信号槽位({data['total_slots']})与配置({strat_cfg['total_slots']})不符")
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)
total_slots = sum(self.config['strategies'][s]['total_slots'] for s in terminal_strategies)
if not asset or total_slots <= 0: return
# 3. 资金等权分配 (基于该终端总资产)
total_equity = asset.cash + asset.market_value
target_amt = total_equity / total_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
oid = unit.xt_trader.order_stock(unit.acc_obj, data['stock_code'], xtconstant.STOCK_BUY,
vol, xtconstant.FIX_PRICE, price, strategy_name, 'PyBuy')
if oid != -1:
unit.order_cache[oid] = (strategy_name, data['stock_code'], 'BUY')
self.pos_manager.mark_holding(strategy_name, data['stock_code'])
self.logger.info(f"√√√ [{unit.alias}] {strategy_name} 下单买入: {data['stock_code']} {vol}股 @ {price}")
except:
self.logger.error(traceback.format_exc())
def _execute_sell(self, unit, strategy_name, data):
v_vol = self.pos_manager.get_position(strategy_name, data['stock_code'])
if v_vol <= 0: return
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
# 取虚拟持仓和实盘可用持仓的最小值
final_vol = min(v_vol, can_use)
if final_vol <= 0:
self.logger.warning(f"[{strategy_name}] 卖出拦截: {data['stock_code']} 实盘无可用持仓")
return
try:
offset = self.config['strategies'][strategy_name].get('execution', {}).get('sell_price_offset', 0.0)
price = round(float(data['price']) + offset, 3)
oid = unit.xt_trader.order_stock(unit.acc_obj, data['stock_code'], xtconstant.STOCK_SELL,
final_vol, xtconstant.FIX_PRICE, price, strategy_name, 'PySell')
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}")
except:
self.logger.error(traceback.format_exc())
def get_all_status(self) -> List[TerminalStatus]:
return [TerminalStatus(u.qmt_id, u.alias, u.account_id, (u.callback.is_connected if u.callback else False), u.last_heartbeat) for u in self.units.values()]
def stop(self):
self.is_running = False
self.logger.info("收到引擎停止指令")
for u in self.units.values():
u.cleanup()