更新qmt代码

This commit is contained in:
2025-11-30 23:41:35 +08:00
parent aca9e43e53
commit 122ffbefba
2 changed files with 277 additions and 143 deletions

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ model
**/mlruns/ **/mlruns/
**/mnt/ **/mnt/
/qmt/config.json

View File

@@ -1,182 +1,314 @@
# coding:utf-8 # coding:utf-8
import time, datetime, traceback, sys, json import time, datetime, traceback, sys, json, os
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 中初始化)
QMT_PATH = r'D:\qmt\投研\迅投极速交易终端睿智融科版\userdata' CONFIG = {}
ACCOUNT_ID = '2000128' ORDER_CACHE = {}
ACCOUNT_TYPE = 'STOCK'
REDIS_HOST = '127.0.0.1' # ================= 配置加载模块 =================
REDIS_PORT = 6379 def load_config(config_file='config.json'):
REDIS_PASS = None """
读取同级目录下的配置文件
"""
# 获取脚本所在目录
if getattr(sys, 'frozen', False):
# 如果被打包为exe
base_path = os.path.dirname(sys.executable)
else:
# 普通脚本运行
base_path = os.path.dirname(os.path.abspath(__file__))
# 策略基础名称 (不需要加 _real代码会自动加) full_path = os.path.join(base_path, config_file)
STRATEGY_BASE_NAME = 'default_strategy'
# ===========================================
# 定义监听的队列名称 (只监听实盘队列,物理屏蔽回测数据) if not os.path.exists(full_path):
LISTEN_QUEUE = f"{STRATEGY_BASE_NAME}_real" # 尝试直接读取(兼容 QMT 内置 Python 的路径行为)
if os.path.exists(config_file):
full_path = config_file
else:
print(f"[错误] 找不到配置文件: {full_path}")
sys.exit(1)
try:
with open(full_path, 'r', encoding='utf-8') as f:
config = json.load(f)
print(f"成功加载配置: {full_path}")
return config
except Exception as e:
print(f"[错误] 配置文件格式错误: {e}")
sys.exit(1)
# ================= 业务逻辑类 (保持不变) =================
class PositionManager:
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)
# print(f"[{strategy_name}] 乐观占位: {code}")
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)
print(f"[{strategy_name}] 回滚释放槽位: {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, xt_trader, acc, pos_mgr, strategies):
self.trader = xt_trader
self.acc = acc
self.pos_mgr = pos_mgr
self.strategies = strategies
self.has_settled = False
def run_settlement(self):
print("\n" + "="*40)
print(f"开始收盘清算: {datetime.datetime.now()}")
# 1. 撤单
orders = self.trader.query_stock_orders(self.acc, cancelable_only=True)
if orders:
for o in orders:
self.trader.cancel_order_stock(self.acc, o.order_id)
time.sleep(2)
# 2. 获取实盘真实持仓
real_positions = self.trader.query_stock_positions(self.acc)
real_pos_map = {}
if real_positions:
for p in real_positions:
if p.volume > 0:
real_pos_map[p.stock_code] = p.volume
# 3. 校准 Redis
for strategy in self.strategies:
virtual_data = self.pos_mgr.get_all_virtual_positions(strategy)
for code, v_vol_str in virtual_data.items():
if code not in real_pos_map:
print(f" [修正] {strategy} 幽灵持仓 {code} -> 强制释放")
self.pos_mgr.force_delete(strategy, code)
print("清算完成")
self.has_settled = True
def reset_flag(self):
self.has_settled = False
class MyXtQuantTraderCallback(XtQuantTraderCallback): class MyXtQuantTraderCallback(XtQuantTraderCallback):
def on_disconnected(self): def __init__(self, pos_mgr):
print("连接断开") self.pos_mgr = pos_mgr
def on_stock_order(self, order): def on_disconnected(self):
print(f"委托回报: {order.order_id} {order.order_remark}") print(">> 连接断开")
def on_stock_trade(self, trade): def on_stock_trade(self, trade):
print(f"成交: {trade.stock_code} {trade.traded_volume}") try:
order_id = trade.order_id
stock_code = trade.stock_code
traded_vol = trade.traded_volume
cache_info = ORDER_CACHE.get(order_id)
if not cache_info: return
strategy_name, cached_code, action_type = cache_info
print(f">>> [成交] {strategy_name} {stock_code} 成交量:{traded_vol}")
if action_type == 'BUY':
self.pos_mgr.update_actual_volume(strategy_name, stock_code, traded_vol)
elif action_type == 'SELL':
self.pos_mgr.update_actual_volume(strategy_name, stock_code, -traded_vol)
except Exception:
traceback.print_exc()
def on_order_error(self, order_error): def on_order_error(self, order_error):
print(f"下单失败: {order_error.error_msg}")
def init_redis():
try: try:
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASS, decode_responses=True) order_id = order_error.order_id
r.ping() print(f">>> [下单失败] ID:{order_id} Msg:{order_error.error_msg}")
return r cache_info = ORDER_CACHE.get(order_id)
except Exception as e: if cache_info:
print(f"Redis连接失败: {e}") strategy_name, stock_code, action_type = cache_info
return None if action_type == 'BUY':
self.pos_mgr.rollback_holding(strategy_name, stock_code)
del ORDER_CACHE[order_id]
except: pass
def is_msg_valid(data): def process_strategy_queue(strategy_name, r_client, xt_trader, acc, pos_manager):
""" queue_key = f"{strategy_name}_real"
【安全核心】校验消息时效性与合法性 msg_json = r_client.lpop(queue_key)
"""
try:
# 1. 检查是否为回测标记 (防御性编程,虽然队列已物理隔离)
if data.get('is_backtest', False):
print(f"警报:拦截到回测数据,已丢弃!")
return False
# 2. 检查时间戳
msg_time_str = data.get('timestamp')
if not msg_time_str:
print("数据缺失时间戳,丢弃")
return False
# 解析消息时间
# 格式必须匹配策略端发送的 '%Y-%m-%d %H:%M:%S'
msg_dt = datetime.datetime.strptime(msg_time_str, '%Y-%m-%d %H:%M:%S')
msg_date = msg_dt.date()
# 获取当前服务器日期
today = datetime.date.today()
# 3. 【核心】判断是否为当天的消息
if msg_date != today:
print(f"拦截过期消息: 消息日期[{msg_date}] != 今日[{today}]")
return False
# 可选如果你想更严格可以判断时间差不能超过5分钟
# delta = datetime.datetime.now() - msg_dt
# if abs(delta.total_seconds()) > 300: ...
return True
except Exception as e:
print(f"校验逻辑异常: {e}")
return False
def process_redis_signal(r_client, xt_trader, acc):
try:
msg_json = r_client.lpop(LISTEN_QUEUE)
if not msg_json: return if not msg_json: return
print(f"收到信号: {msg_json}") try:
data = json.loads(msg_json) data = json.loads(msg_json)
if not is_msg_valid(data): return # 之前的校验逻辑 # 校验
if data.get('is_backtest', False): return
msg_ts = data.get('timestamp')
if msg_ts:
msg_date = datetime.datetime.strptime(msg_ts, '%Y-%m-%d %H:%M:%S').date()
if msg_date != datetime.date.today():
return
stock_code = data['stock_code'] stock_code = data['stock_code']
action = data['action'] action = data['action']
price = float(data['price']) price = float(data['price'])
target_total_slots = int(data.get('total_slots', 1))
# 获取切分份数
# 兼容性处理如果redis里还是旧key 'weight',也可以尝试获取
div_count = float(data.get('div_count', data.get('weight', 1)))
# =========================================================
# 买入逻辑:资金切片法
# =========================================================
if action == 'BUY': if action == 'BUY':
# 1. 必须查最新的可用资金 (Available Cash) current_holding_count = pos_manager.get_holding_count(strategy_name)
empty_slots = target_total_slots - current_holding_count
if empty_slots <= 0: return
asset = xt_trader.query_stock_asset(acc) asset = xt_trader.query_stock_asset(acc)
if not asset: if not asset: return
print("错误:无法查询资产")
return
current_cash = asset.cash target_amount = asset.cash / empty_slots
if target_amount < 2000: return
# 2. 计算下单金额
# 逻辑Amount = Cash / div_count
if div_count <= 0: div_count = 1 # 防止除0
target_amount = current_cash / div_count
# 3. 打印调试信息 (非常重要)
print(f"【资金分配】可用现金:{current_cash:.2f} / 切分份数:{div_count} = 下单金额:{target_amount:.2f}")
# 4. 计算股数
if price <= 0: price = 1.0 if price <= 0: price = 1.0
# 过滤小额杂单
if target_amount < 2000:
print(f"忽略:金额过小 ({target_amount:.2f})")
return
vol = int(target_amount / price / 100) * 100 vol = int(target_amount / price / 100) * 100
if vol >= 100: if vol >= 100:
xt_trader.order_stock(acc, stock_code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE, price, order_id = xt_trader.order_stock(acc, stock_code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE, price, strategy_name, 'PyBuy')
STRATEGY_BASE_NAME, 'PyBuy') if order_id != -1:
print(f"买入下单: {stock_code} {vol}") print(f"[{strategy_name}] 买入 {stock_code} {vol} (1/{empty_slots})")
else: ORDER_CACHE[order_id] = (strategy_name, stock_code, 'BUY')
print(f"计算股数不足100股") pos_manager.mark_holding(strategy_name, stock_code)
# =========================================================
# 卖出逻辑 (清仓)
# =========================================================
elif action == 'SELL': elif action == 'SELL':
positions = xt_trader.query_stock_positions(acc) virtual_vol = pos_manager.get_position(strategy_name, stock_code)
target_pos = next((p for p in positions if p.stock_code == stock_code), None) if virtual_vol > 0:
real_positions = xt_trader.query_stock_positions(acc)
real_pos = next((p for p in real_positions if p.stock_code == stock_code), None)
real_can_use = real_pos.can_use_volume if real_pos else 0
if target_pos and target_pos.can_use_volume > 0: final_vol = min(virtual_vol, real_can_use)
xt_trader.order_stock(acc, stock_code, xtconstant.STOCK_SELL, target_pos.can_use_volume, if final_vol > 0:
xtconstant.FIX_PRICE, price, STRATEGY_BASE_NAME, 'PySell') order_id = xt_trader.order_stock(acc, stock_code, xtconstant.STOCK_SELL, final_vol, xtconstant.FIX_PRICE, price, strategy_name, 'PySell')
print(f"卖出下单: {stock_code} {target_pos.can_use_volume}") if order_id != -1:
else: print(f"[{strategy_name}] 卖出 {stock_code} {final_vol}")
print(f"无可用持仓: {stock_code}") ORDER_CACHE[order_id] = (strategy_name, stock_code, 'SELL')
except Exception as e: except Exception:
print(f"处理异常: {e}")
traceback.print_exc() traceback.print_exc()
# ================= 主程序入口 =================
if __name__ == '__main__': if __name__ == '__main__':
r_client = init_redis() print("正在启动...")
# 1. 加载配置
CONFIG = load_config('config.json')
# 从配置中提取参数
redis_cfg = CONFIG['redis']
qmt_cfg = CONFIG['qmt']
watch_list = CONFIG['strategies']
print(f"Redis目标: {redis_cfg['host']}:{redis_cfg['port']}")
print(f"QMT路径: {qmt_cfg['path']}")
print(f"监听策略: {watch_list}")
# 2. 连接 Redis
try:
r = redis.Redis(
host=redis_cfg['host'],
port=redis_cfg['port'],
password=redis_cfg['password'],
db=redis_cfg['db'],
decode_responses=True
)
r.ping()
print("Redis 连接成功")
pos_manager = PositionManager(r)
except Exception as e:
print(f"[FATAL] Redis 连接失败: {e}")
sys.exit(1)
# 3. 连接 QMT
try:
session_id = int(time.time()) session_id = int(time.time())
xt_trader = XtQuantTrader(QMT_PATH, session_id) xt_trader = XtQuantTrader(qmt_cfg['path'], session_id)
acc = StockAccount(ACCOUNT_ID, ACCOUNT_TYPE)
callback = MyXtQuantTraderCallback() acc = StockAccount(qmt_cfg['account_id'], qmt_cfg['account_type'])
callback = MyXtQuantTraderCallback(pos_manager)
xt_trader.register_callback(callback) xt_trader.register_callback(callback)
xt_trader.start() xt_trader.start()
xt_trader.connect() connect_res = xt_trader.connect()
if connect_res == 0:
print(f"QMT 连接成功: {qmt_cfg['account_id']}")
xt_trader.subscribe(acc) xt_trader.subscribe(acc)
else:
print(f"[FATAL] QMT 连接失败,错误码: {connect_res}")
sys.exit(1)
print(f"=== 启动监听: {LISTEN_QUEUE} ===") except Exception as e:
print("只处理当日的实盘/模拟信号,自动过滤回测数据及历史遗留数据。") print(f"[FATAL] QMT 初始化异常: {e}")
sys.exit(1)
# 4. 初始化清算器
settler = DailySettlement(xt_trader, acc, pos_manager, watch_list)
print("=== 系统就绪,开始监听 ===")
try:
while True: while True:
if r_client: now = datetime.datetime.now()
process_redis_signal(r_client, xt_trader, acc) current_time_str = now.strftime('%H%M')
# 交易时段
if '0900' <= current_time_str <= '1500':
if settler.has_settled:
settler.reset_flag()
for strategy in watch_list:
process_strategy_queue(strategy, r, xt_trader, acc, pos_manager)
# 收盘清算时段
elif '1505' <= current_time_str <= '1510':
if not settler.has_settled:
settler.run_settlement()
time.sleep(60) time.sleep(60)
except KeyboardInterrupt:
print("用户终止程序")