更新qmt代码

1、优化日志打印
2、断连重连
3、redis持久化
This commit is contained in:
2025-12-02 23:38:49 +08:00
parent 122ffbefba
commit bbf1d2248c
2 changed files with 161 additions and 64 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ model
**/mnt/ **/mnt/
/qmt/config.json /qmt/config.json
/qmt/logs/*

View File

@@ -1,48 +1,82 @@
# coding:utf-8 # coding:utf-8
import time, datetime, traceback, sys, json, os import time, datetime, traceback, sys, json, os
import logging
from logging.handlers import TimedRotatingFileHandler
import redis import redis
from xtquant import xtdata from xtquant import xtdata
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount from xtquant.xttype import StockAccount
from xtquant import xtconstant from xtquant import xtconstant
# 全局变量占位 (稍后在 main 中初始化) # ================= 1. 日志系统初始化 =================
def setup_logger():
log_dir = "logs"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
today = datetime.date.today().strftime('%Y-%m-%d')
log_file = os.path.join(log_dir, f"qmt_trade_{today}.log")
logger = logging.getLogger("QMT_Trader")
logger.setLevel(logging.INFO)
if logger.handlers:
logger.handlers.clear()
formatter = logging.Formatter(
'[%(asctime)s] [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler = TimedRotatingFileHandler(
filename=log_file,
when='MIDNIGHT',
interval=1,
backupCount=30,
encoding='utf-8'
)
file_handler.setFormatter(formatter)
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(stream_handler)
return logger
logger = setup_logger()
# ================= 全局变量 =================
CONFIG = {} CONFIG = {}
ORDER_CACHE = {} ORDER_CACHE = {}
# ================= 配置加载模块 ================= # ================= 配置加载模块 =================
def load_config(config_file='config.json'): def load_config(config_file='config.json'):
"""
读取同级目录下的配置文件
"""
# 获取脚本所在目录
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
# 如果被打包为exe
base_path = os.path.dirname(sys.executable) base_path = os.path.dirname(sys.executable)
else: else:
# 普通脚本运行
base_path = os.path.dirname(os.path.abspath(__file__)) base_path = os.path.dirname(os.path.abspath(__file__))
full_path = os.path.join(base_path, config_file) full_path = os.path.join(base_path, config_file)
if not os.path.exists(full_path): if not os.path.exists(full_path):
# 尝试直接读取(兼容 QMT 内置 Python 的路径行为)
if os.path.exists(config_file): if os.path.exists(config_file):
full_path = config_file full_path = config_file
else: else:
print(f"[错误] 找不到配置文件: {full_path}") logger.error(f"找不到配置文件: {full_path}")
sys.exit(1) sys.exit(1)
try: try:
with open(full_path, 'r', encoding='utf-8') as f: with open(full_path, 'r', encoding='utf-8') as f:
config = json.load(f) config = json.load(f)
print(f"成功加载配置: {full_path}") logger.info(f"成功加载配置: {full_path}")
return config return config
except Exception as e: except Exception as e:
print(f"[错误] 配置文件格式错误: {e}") logger.error(f"配置文件格式错误: {e}")
sys.exit(1) sys.exit(1)
# ================= 业务逻辑类 (保持不变) ================= # ================= 业务逻辑类 =================
class PositionManager: class PositionManager:
def __init__(self, r_client): def __init__(self, r_client):
@@ -53,8 +87,9 @@ class PositionManager:
def mark_holding(self, strategy_name, code): def mark_holding(self, strategy_name, code):
"""乐观占位""" """乐观占位"""
self.r.hsetnx(self._get_key(strategy_name), code, 0) key = self._get_key(strategy_name)
# print(f"[{strategy_name}] 乐观占位: {code}") if self.r.hsetnx(key, code, 0):
logger.info(f"[{strategy_name}] Redis乐观占位成功: {code}")
def rollback_holding(self, strategy_name, code): def rollback_holding(self, strategy_name, code):
"""失败回滚""" """失败回滚"""
@@ -62,7 +97,7 @@ class PositionManager:
val = self.r.hget(key, code) val = self.r.hget(key, code)
if val is not None and int(val) == 0: if val is not None and int(val) == 0:
self.r.hdel(key, code) self.r.hdel(key, code)
print(f"[{strategy_name}] 回滚释放槽位: {code}") logger.warning(f"[{strategy_name}] 回滚释放槽位: {code}")
def update_actual_volume(self, strategy_name, code, delta_vol): def update_actual_volume(self, strategy_name, code, delta_vol):
"""成交更新""" """成交更新"""
@@ -96,17 +131,21 @@ class DailySettlement:
self.has_settled = False self.has_settled = False
def run_settlement(self): def run_settlement(self):
print("\n" + "="*40) logger.info("="*40)
print(f"开始收盘清算: {datetime.datetime.now()}") logger.info("开始执行收盘清算流程...")
# 1. 撤单 try:
orders = self.trader.query_stock_orders(self.acc, cancelable_only=True) orders = self.trader.query_stock_orders(self.acc, cancelable_only=True)
if orders: if orders:
for o in orders: for o in orders:
self.trader.cancel_order_stock(self.acc, o.order_id) self.trader.cancel_order_stock(self.acc, o.order_id)
time.sleep(2) logger.info(f" -> 撤销未成交挂单: {o.stock_code} {o.order_volume}")
time.sleep(2)
else:
logger.info(" -> 无未成交订单")
except Exception as e:
logger.error(f"查询/撤单异常: {e}")
# 2. 获取实盘真实持仓
real_positions = self.trader.query_stock_positions(self.acc) real_positions = self.trader.query_stock_positions(self.acc)
real_pos_map = {} real_pos_map = {}
if real_positions: if real_positions:
@@ -114,15 +153,15 @@ class DailySettlement:
if p.volume > 0: if p.volume > 0:
real_pos_map[p.stock_code] = p.volume real_pos_map[p.stock_code] = p.volume
# 3. 校准 Redis
for strategy in self.strategies: for strategy in self.strategies:
virtual_data = self.pos_mgr.get_all_virtual_positions(strategy) virtual_data = self.pos_mgr.get_all_virtual_positions(strategy)
for code, v_vol_str in virtual_data.items(): for code, v_vol_str in virtual_data.items():
if code not in real_pos_map: if code not in real_pos_map:
print(f" [修正] {strategy} 幽灵持仓 {code} -> 强制释放") logger.warning(f" [修正] 策略[{strategy}] 幽灵持仓 {code} (Redis={v_vol_str}) -> 强制释放")
self.pos_mgr.force_delete(strategy, code) self.pos_mgr.force_delete(strategy, code)
print("清算完成") logger.info("收盘清算完成")
logger.info("="*40)
self.has_settled = True self.has_settled = True
def reset_flag(self): def reset_flag(self):
@@ -132,9 +171,15 @@ class DailySettlement:
class MyXtQuantTraderCallback(XtQuantTraderCallback): class MyXtQuantTraderCallback(XtQuantTraderCallback):
def __init__(self, pos_mgr): def __init__(self, pos_mgr):
self.pos_mgr = pos_mgr self.pos_mgr = pos_mgr
# 【新增】连接状态标记默认False连接成功后外部置为True
self.is_connected = False
def on_disconnected(self): def on_disconnected(self):
print(">> 连接断开") """
连接断开回调
"""
logger.warning(">> 检测到交易端连接断开 (QMT可能正在重启)")
self.is_connected = False
def on_stock_trade(self, trade): def on_stock_trade(self, trade):
try: try:
@@ -146,27 +191,32 @@ class MyXtQuantTraderCallback(XtQuantTraderCallback):
if not cache_info: return if not cache_info: return
strategy_name, cached_code, action_type = cache_info strategy_name, cached_code, action_type = cache_info
print(f">>> [成交] {strategy_name} {stock_code} 成交量:{traded_vol}") logger.info(f">>> [成交回报] 策略:{strategy_name} 代码:{stock_code} 成交量:{traded_vol}")
if action_type == 'BUY': if action_type == 'BUY':
self.pos_mgr.update_actual_volume(strategy_name, stock_code, traded_vol) new_pos = self.pos_mgr.update_actual_volume(strategy_name, stock_code, traded_vol)
logger.info(f" [记账] {strategy_name} 持仓增加 -> {new_pos}")
elif action_type == 'SELL': elif action_type == 'SELL':
self.pos_mgr.update_actual_volume(strategy_name, stock_code, -traded_vol) new_pos = self.pos_mgr.update_actual_volume(strategy_name, stock_code, -traded_vol)
logger.info(f" [记账] {strategy_name} 持仓扣减 -> {new_pos}")
except Exception: except Exception:
traceback.print_exc() logger.error("成交回调处理异常", exc_info=True)
def on_order_error(self, order_error): def on_order_error(self, order_error):
try: try:
order_id = order_error.order_id order_id = order_error.order_id
print(f">>> [下单失败] ID:{order_id} Msg:{order_error.error_msg}") err_msg = order_error.error_msg
logger.error(f">>> [下单失败] ID:{order_id} Msg:{err_msg}")
cache_info = ORDER_CACHE.get(order_id) cache_info = ORDER_CACHE.get(order_id)
if cache_info: if cache_info:
strategy_name, stock_code, action_type = cache_info strategy_name, stock_code, action_type = cache_info
if action_type == 'BUY': if action_type == 'BUY':
self.pos_mgr.rollback_holding(strategy_name, stock_code) self.pos_mgr.rollback_holding(strategy_name, stock_code)
del ORDER_CACHE[order_id] del ORDER_CACHE[order_id]
except: pass except:
pass
def process_strategy_queue(strategy_name, r_client, xt_trader, acc, pos_manager): def process_strategy_queue(strategy_name, r_client, xt_trader, acc, pos_manager):
@@ -175,14 +225,17 @@ def process_strategy_queue(strategy_name, r_client, xt_trader, acc, pos_manager)
if not msg_json: return if not msg_json: return
try: try:
history_key = f"{queue_key}:history"
r_client.rpush(history_key, msg_json)
data = json.loads(msg_json) data = json.loads(msg_json)
# 校验
if data.get('is_backtest', False): return if data.get('is_backtest', False): return
msg_ts = data.get('timestamp') msg_ts = data.get('timestamp')
if msg_ts: if msg_ts:
msg_date = datetime.datetime.strptime(msg_ts, '%Y-%m-%d %H:%M:%S').date() msg_date = datetime.datetime.strptime(msg_ts, '%Y-%m-%d %H:%M:%S').date()
if msg_date != datetime.date.today(): if msg_date != datetime.date.today():
logger.warning(f"忽略过期消息: {msg_ts}")
return return
stock_code = data['stock_code'] stock_code = data['stock_code']
@@ -190,17 +243,25 @@ def process_strategy_queue(strategy_name, r_client, xt_trader, acc, pos_manager)
price = float(data['price']) price = float(data['price'])
target_total_slots = int(data.get('total_slots', 1)) target_total_slots = int(data.get('total_slots', 1))
logger.info(f"收到信号 [{strategy_name}]: {stock_code} {action} Slot:{target_total_slots}")
if action == 'BUY': if action == 'BUY':
current_holding_count = pos_manager.get_holding_count(strategy_name) current_holding_count = pos_manager.get_holding_count(strategy_name)
empty_slots = target_total_slots - current_holding_count empty_slots = target_total_slots - current_holding_count
if empty_slots <= 0: return if empty_slots <= 0:
logger.warning(f"[{strategy_name}] 槽位已满,忽略买入")
return
asset = xt_trader.query_stock_asset(acc) asset = xt_trader.query_stock_asset(acc)
if not asset: return if not asset:
logger.error("无法查询资产信息(可能连接未就绪)")
return
target_amount = asset.cash / empty_slots target_amount = asset.cash / empty_slots
if target_amount < 2000: return if target_amount < 2000:
logger.info(f"[{strategy_name}] 下单金额过小 ({target_amount:.2f}),忽略")
return
if price <= 0: price = 1.0 if price <= 0: price = 1.0
vol = int(target_amount / price / 100) * 100 vol = int(target_amount / price / 100) * 100
@@ -208,9 +269,11 @@ def process_strategy_queue(strategy_name, r_client, xt_trader, acc, pos_manager)
if vol >= 100: if vol >= 100:
order_id = xt_trader.order_stock(acc, stock_code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE, price, strategy_name, 'PyBuy') order_id = xt_trader.order_stock(acc, stock_code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE, price, strategy_name, 'PyBuy')
if order_id != -1: if order_id != -1:
print(f"[{strategy_name}] 买入 {stock_code} {vol}股 (1/{empty_slots})") logger.info(f"[{strategy_name}] 发出买单: {stock_code} {vol}股 (1/{empty_slots}) ID:{order_id}")
ORDER_CACHE[order_id] = (strategy_name, stock_code, 'BUY') ORDER_CACHE[order_id] = (strategy_name, stock_code, 'BUY')
pos_manager.mark_holding(strategy_name, stock_code) pos_manager.mark_holding(strategy_name, stock_code)
else:
logger.error(f"[{strategy_name}] 下单请求被柜台拒绝 (-1)")
elif action == 'SELL': elif action == 'SELL':
virtual_vol = pos_manager.get_position(strategy_name, stock_code) virtual_vol = pos_manager.get_position(strategy_name, stock_code)
@@ -223,27 +286,27 @@ def process_strategy_queue(strategy_name, r_client, xt_trader, acc, pos_manager)
if final_vol > 0: if final_vol > 0:
order_id = xt_trader.order_stock(acc, stock_code, xtconstant.STOCK_SELL, final_vol, xtconstant.FIX_PRICE, price, strategy_name, 'PySell') order_id = xt_trader.order_stock(acc, stock_code, xtconstant.STOCK_SELL, final_vol, xtconstant.FIX_PRICE, price, strategy_name, 'PySell')
if order_id != -1: if order_id != -1:
print(f"[{strategy_name}] 卖出 {stock_code} {final_vol}") logger.info(f"[{strategy_name}] 发出卖单: {stock_code} {final_vol} ID:{order_id}")
ORDER_CACHE[order_id] = (strategy_name, stock_code, 'SELL') ORDER_CACHE[order_id] = (strategy_name, stock_code, 'SELL')
else:
logger.warning(f"[{strategy_name}] 账本有货但实盘不足 (Redis:{virtual_vol}, Real:{real_can_use})")
else:
logger.info(f"[{strategy_name}] Redis无持仓忽略卖出")
except Exception: except Exception:
traceback.print_exc() logger.error("消息处理异常", exc_info=True)
# ================= 主程序入口 ================= # ================= 主程序入口 =================
if __name__ == '__main__': if __name__ == '__main__':
print("正在启动...") logger.info("正在启动 QMT 策略监听系统...")
# 1. 加载配置 # 1. 加载配置
CONFIG = load_config('config.json') CONFIG = load_config('config.json')
# 从配置中提取参数
redis_cfg = CONFIG['redis'] redis_cfg = CONFIG['redis']
qmt_cfg = CONFIG['qmt'] qmt_cfg = CONFIG['qmt']
watch_list = CONFIG['strategies'] watch_list = CONFIG['strategies']
print(f"Redis目标: {redis_cfg['host']}:{redis_cfg['port']}") logger.info(f"监听策略列表: {watch_list}")
print(f"QMT路径: {qmt_cfg['path']}")
print(f"监听策略: {watch_list}")
# 2. 连接 Redis # 2. 连接 Redis
try: try:
@@ -255,60 +318,93 @@ if __name__ == '__main__':
decode_responses=True decode_responses=True
) )
r.ping() r.ping()
print("Redis 连接成功") logger.info("Redis 连接成功")
pos_manager = PositionManager(r) pos_manager = PositionManager(r)
except Exception as e: except Exception as e:
print(f"[FATAL] Redis 连接失败: {e}") logger.critical(f"Redis 连接失败: {e}")
sys.exit(1) sys.exit(1)
# 3. 连接 QMT # 3. 初始化 QMT 对象 (暂不连接)
try: try:
session_id = int(time.time()) session_id = int(time.time())
xt_trader = XtQuantTrader(qmt_cfg['path'], session_id) xt_trader = XtQuantTrader(qmt_cfg['path'], session_id)
acc = StockAccount(qmt_cfg['account_id'], qmt_cfg['account_type']) acc = StockAccount(qmt_cfg['account_id'], qmt_cfg['account_type'])
callback = MyXtQuantTraderCallback(pos_manager) callback = MyXtQuantTraderCallback(pos_manager)
xt_trader.register_callback(callback) xt_trader.register_callback(callback)
xt_trader.start() xt_trader.start()
connect_res = xt_trader.connect()
# 初次连接
connect_res = xt_trader.connect()
if connect_res == 0: if connect_res == 0:
print(f"QMT 连接成功: {qmt_cfg['account_id']}") logger.info(f"QMT 初次连接成功: {qmt_cfg['account_id']}")
xt_trader.subscribe(acc) xt_trader.subscribe(acc)
callback.is_connected = True # 标记为已连接
else: else:
print(f"[FATAL] QMT 连接失败,错误码: {connect_res}") logger.error(f"QMT 初次连接失败 ({connect_res}),将进入重连循环")
sys.exit(1) callback.is_connected = False
except Exception as e: except Exception as e:
print(f"[FATAL] QMT 初始化异常: {e}") logger.critical(f"QMT 初始化异常: {e}")
sys.exit(1) sys.exit(1)
# 4. 初始化清算器 # 4. 初始化清算器
settler = DailySettlement(xt_trader, acc, pos_manager, watch_list) settler = DailySettlement(xt_trader, acc, pos_manager, watch_list)
print("=== 系统就绪,开始监听 ===") logger.info("=== 系统就绪,开始循环监听 ===")
try: try:
while True: while True:
# ================= [新增] 断线重连逻辑 =================
if not callback.is_connected:
logger.info("正在尝试重连 QMT...")
try:
# 尝试连接
res = xt_trader.connect()
if res == 0:
logger.info("QMT 重连成功!重新订阅账户...")
# 重连后必须重新订阅
xt_trader.subscribe(acc)
callback.is_connected = True
else:
logger.warning(f"QMT 重连失败 (Code: {res})60秒后重试...")
time.sleep(60)
continue # 跳过本次循环,直接进入下一次检查
except Exception as e:
logger.error(f"重连过程发生异常: {e}")
time.sleep(60)
continue
# ====================================================
now = datetime.datetime.now() now = datetime.datetime.now()
current_time_str = now.strftime('%H%M') current_time_str = now.strftime('%H%M')
# 交易时段 # 默认休眠时间
sleep_interval = 60
# === 交易时段 ===
if '0900' <= current_time_str <= '1500': if '0900' <= current_time_str <= '1500':
# 高频轮询时段
if '0920' <= current_time_str <= '1000':
sleep_interval = 10
else:
sleep_interval = 60
if settler.has_settled: if settler.has_settled:
settler.reset_flag() settler.reset_flag()
for strategy in watch_list: for strategy in watch_list:
process_strategy_queue(strategy, r, xt_trader, acc, pos_manager) process_strategy_queue(strategy, r, xt_trader, acc, pos_manager)
# 收盘清算时段 # === 收盘清算时段 ===
elif '1505' <= current_time_str <= '1510': elif '1505' <= current_time_str <= '1510':
if not settler.has_settled: if not settler.has_settled:
settler.run_settlement() settler.run_settlement()
sleep_interval = 60
time.sleep(60) time.sleep(sleep_interval)
except KeyboardInterrupt: except KeyboardInterrupt:
print("用户终止程序") logger.info("用户手动终止程序")
except Exception as e:
logger.critical("主循环发生未捕获异常", exc_info=True)