问题描述
解决方案
在QMT平台上从回测转向真仓(实盘)交易时,环境的差异会导致许多在回测中不易察觉的问题暴露出来。以下是针对QMT Python API整理的常见编程错误及避免方法,涵盖了信号处理、下单逻辑、数据获取和环境配置四个维度。
QMT 真仓策略开发:常见编程错误与避坑指南
1. 信号闪烁与“未来函数”陷阱
在实盘中,最致命的错误是信号闪烁(Repainting)。这是指在当前K线尚未走完时,根据盘中临时价格发出了信号,但K线收盘后信号消失,导致实盘买入但回测中没有该笔交易。
错误示例:直接使用当前Bar的收盘价
在 handlebar 函数中,对于最后一根K线(实时K线),close 价格是随 Tick 变动的。
# 错误写法
def handlebar(ContextInfo):
# 获取当前K线数据
close = ContextInfo.get_market_data(['close'])
# 如果盘中价格突破,立即下单
if close > ma20:
passorder(...)
后果:盘中价格瞬间突破MA20触发买入,但收盘回落跌破MA20。你的持仓还在,但策略逻辑上不该持有。
正确做法:
- 使用上一根K线判断:只依据已完成的K线(
barpos - 1)做决策。 - 固定时间下单:如果必须做日内,配合
ContextInfo.get_tick_timetag()判断时间。 - 利用
is_last_bar和is_new_bar:
# 正确写法:在下一根K线的起始时刻,基于上一根K线的收盘价下单
def handlebar(ContextInfo):
# 如果不是最新的一根K线,或者是最新K线但不是新的一根(即盘中Tick),则跳过
if not ContextInfo.is_last_bar():
return
# 只有在实盘中,新K线生成的第一个Tick才执行逻辑(避免盘中重复计算)
# 注意:is_new_bar() 在回测和实盘表现略有不同,需结合具体逻辑
if ContextInfo.is_new_bar():
# 获取历史数据(不包含当前正在跳动的K线)
history_data = ContextInfo.get_market_data_ex(..., count=2)
# 使用 history_data.iloc[-2] (上一根) 进行判断
2. 下单逻辑与重复交易(Order Spamming)
QMT的 handlebar 在实盘模式下,最后一根K线每收到一个Tick(分笔数据)就会运行一次。如果不加控制,策略会在几秒钟内把资金全部打光。
错误示例:未检查持仓或未控制频率
# 错误写法
def handlebar(ContextInfo):
# 只要条件满足,每个Tick都会触发一次下单
if condition:
passorder(23, 1101, account, code, 5, -1, 100, ContextInfo)
正确做法:
- 检查持仓:下单前必须查询
get_trade_detail_data确认当前没有持仓。 - 全局变量控制:使用
ContextInfo存储状态标记。
# 正确写法
def init(ContextInfo):
ContextInfo.has_ordered = False # 初始化标记
def handlebar(ContextInfo):
# 每日开盘重置标记(示例逻辑)
if is_new_day(ContextInfo):
ContextInfo.has_ordered = False
if condition and not ContextInfo.has_ordered:
passorder(...)
ContextInfo.has_ordered = True # 标记已下单
3. 数据获取接口的返回值类型混淆
QMT的 get_market_data 接口非常灵活,但也是新手噩梦。根据传入参数的不同(单股vs多股,单时间点vs时间序列),它返回的数据类型会在 pandas.Series、pandas.DataFrame 和 pandas.Panel 之间变化。
常见错误:
假设返回的是 DataFrame,结果只传了一只股票导致返回了 Series,代码报错 AttributeError。
建议做法:
- 优先使用
get_market_data_ex:该接口返回格式更统一(字典形式{code: DataFrame}),不易出错。 - 明确参数:始终清楚自己是在请求一只股票还是一篮子股票。
# 推荐写法
def handlebar(ContextInfo):
# 获取多只股票数据,返回字典
data_map = ContextInfo.get_market_data_ex(
['close'],
['600000.SH', '000001.SZ'],
period='1d',
count=10
)
# 即使只传一只股票,也建议按列表传,处理方式保持一致
df_600000 = data_map['600000.SH'] # 这是一个DataFrame
4. 忽略停牌与涨跌停
在回测中,QMT可能会自动处理停牌(取决于 skip_paused 参数),但在真仓交易中,向停牌或跌停的股票发送卖单,或者向涨停的股票发送买单,会导致废单或无法成交。
避坑指南:
- 判断停牌:使用
ContextInfo.is_suspended_stock(code)。 - 判断涨跌停:获取当天的
UpStopPrice和DownStopPrice(通过get_instrumentdetail或行情数据),下单价格不要超过限制。
def check_validity(ContextInfo, code, price):
# 检查停牌
if ContextInfo.is_suspended_stock(code):
return False
# 检查涨跌停
detail = ContextInfo.get_instrumentdetail(code)
if price >= detail['UpStopPrice'] or price <= detail['DownStopPrice']:
# 根据策略逻辑决定是否追涨杀跌,或者避开
pass
return True
5. 编码与全局变量作用域
编码问题
QMT底层是C++,Python脚本文件默认编码格式要求严格。
- 必须在文件第一行加上
# -*- coding: gbk -*-或#coding:gbk。如果不加,中文注释会导致编译错误或乱码。
变量作用域
不要使用 Python 原生的 global 关键字来跨函数传递策略状态(如持仓标记、计数器)。QMT 的运行机制可能会重置脚本环境。
- 必须将全局变量挂载到
ContextInfo对象上。
# 错误
my_counter = 0
def handlebar(ContextInfo):
global my_counter
my_counter += 1
# 正确
def init(ContextInfo):
ContextInfo.my_counter = 0
def handlebar(ContextInfo):
ContextInfo.my_counter += 1
6. 账户ID与交易品种后缀
- 账户ID:
passorder函数需要传入准确的资金账号字符串。新手常犯错误是传了数字类型而不是字符串,或者弄错了账号类型(股票 vs 期货)。 - 代码后缀:QMT严格区分市场后缀。
- 上海:
.SH(如600000.SH) - 深圳:
.SZ(如000001.SZ) - 注意:不要漏掉后缀,也不要用小写(虽然部分接口兼容,但建议统一大写)。
- 上海:
Q&A: 常见疑问解答
Q: 为什么我的策略在回测很完美,实盘却不交易?
A:
- 检查
init中是否调用了ContextInfo.set_account(account_id)绑定账号。 - 检查
passorder中的quickTrade参数。回测通常默认在K线结束时撮合,实盘如果需要在信号触发瞬间下单,需设置quickTrade=1(立即下单)或2(即使是历史K线也下单,慎用)。 - 检查是否处于非交易时间或股票停牌。
Q: 如何在盘中获取最新的实时价格(Tick数据)?
A: 使用 ContextInfo.get_full_tick([code])。它返回一个字典,包含最新的买一卖一、最新价、成交量等详细盘口数据。不要用 get_market_data 去取高频切片,效率较低。
Q: passorder 下单后,如何确认成交了?
A: 不要假设下单即成交。必须通过 order_callback (委托回报) 和 deal_callback (成交回报) 回调函数来接收通知,或者在 handlebar 中轮询 get_trade_detail_data 查询委托状态。
Q: 策略报错 "name 'ContextInfo' is not defined" 是怎么回事?
A: 这是一个常见的误解。ContextInfo 是QMT平台回调 init 和 handlebar 时自动传入的参数。你不能在这些函数之外(例如模块层级)直接使用 ContextInfo。所有的逻辑都必须包含在平台调用的函数内部。