问题描述
这种情况不常见,但的确会出现。今天就在实盘中出现,返回了不准确的数据,导致一支票卖飞掉了。
请看,第 1 个截图,是我量化系统在 11:01 份左右,通过 get_full_tick 获取的 002984 的最新价格为 15.13。但实际上,这个数据不准,可能是一个过期数据。请看截图 2,11:01 分左右的价格准确的应该是在 15.19 左右,而 15.13 这个价格,大概是 10:50 分左右的数据。然而,复盘一下,就是这 6 分钱之差,导致系统错误的执行了止损。然后,请看截图 3,真的不想说什么了....
ps:这不是个例。在差不多的时间段,数据不准确的情况在其它的票上也有发生,只是没造成损失罢了。
解决方案
非常理解您在实盘中遇到这种数据延迟导致错误止损的心情。在量化交易中,数据源的准确性和及时性是策略的生命线。针对您提到的 get_full_tick 在特定时刻返回了 10 分钟前的过期数据的问题,这在基于本地客户端架构的量化软件中确实偶有发生。以下是深度原因分析以及相应的解决方案。
一、 为什么 get_full_tick 会返回过期数据?
在 QMT 的底层架构中,get_full_tick 函数的作用是直接从本地客户端的内存中读取该股票最新的分笔快照。出现数据不准确(延迟)通常由以下几个原因导致:
- 未建立有效的行情订阅:如果您的策略没有显式订阅该股票的 tick 行情,客户端可能只会依赖基础的轮询机制或主图品种的推送。对于非主图品种,本地内存中的 tick 数据可能不会高频刷新,从而读到“上一次缓存”的旧数据。
- 网络抖动或主推拥堵:在早盘或行情剧烈波动时,交易所下发的数据量极大。如果网络出现微小抖动,或者客户端处理主推数据的线程出现短暂拥堵,本地内存中的 tick 数据更新就会滞后。
- 轮询机制的天然缺陷:
get_full_tick是一种“主动拉取(Polling)”的方法。如果在handlebar中调用,它只反映调用那一瞬间本地缓存的状态,缺乏对数据新鲜度的校验。
二、 解决方案与代码优化建议
为了避免再次发生“卖飞”或错误止损,建议对策略的数据获取模块进行以下升级:
方案 1:改用 subscribe_quote 进行事件驱动(强烈推荐)
不要主动去拉取数据,而是让系统在收到最新 tick 时主动通知策略。使用 ContextInfo.subscribe_quote 订阅目标股票的 tick 数据,并在回调函数中处理止损逻辑。这样可以确保你处理的永远是刚刚到达的最新数据。
def init(ContextInfo):
# 订阅目标股票的 tick 行情,设置回调函数
ContextInfo.subscribe_quote('002984.SZ', period='tick', dividend_type='none', callback=on_quote)
def on_quote(datas):
# 当有最新 tick 到达时,此函数会被触发
for stock_code, data in datas.items():
if not data.empty:
# 获取最新的一条 tick 数据
latest_tick = data.iloc[-1]
latest_price = latest_tick['close'] # tick周期下的close即为最新价
# 在这里执行你的止损判断逻辑
# print(f"{stock_code} 最新推送价格: {latest_price}")
def handlebar(ContextInfo):
# 使用了 subscribe_quote 后,常规的监控逻辑可以转移到回调函数中
pass
方案 2:使用 get_market_data_ex 替代并开启订阅
如果您仍然希望在 handlebar 中按周期(如 1 分钟线)进行判断,建议废弃 get_full_tick,改用更健壮的 get_market_data_ex,并务必将 subscribe 参数设置为 True。这会在后台强制建立行情订阅链路,保证本地数据的实时性。
def handlebar(ContextInfo):
# 获取最新 tick 数据,强制开启订阅 (subscribe=True)
tick_data = ContextInfo.get_market_data_ex(
fields=['close'],
stock_code=['002984.SZ'],
period='tick',
count=1,
subscribe=True
)
if '002984.SZ' in tick_data and not tick_data['002984.SZ'].empty:
# 获取最新价
latest_price = tick_data['002984.SZ']['close'].iloc[-1]
# 获取该数据的时间戳索引,用于校验数据新鲜度
data_time = tick_data['002984.SZ'].index[-1]
# 增加时间戳校验防线:如果数据时间与当前系统时间相差过大(例如超过30秒),则拒绝执行交易
# (此处需引入 datetime 模块进行时间差计算)
# 执行止损逻辑...
方案 3:增加“交叉验证”防线
在实盘中,特别是涉及止损、清仓等不可逆的敏感操作时,永远不要相信单一的数据源。如果 get_full_tick 触发了止损条件,可以在代码中增加一步交叉验证:
def handlebar(ContextInfo):
target_stock = '002984.SZ'
tick_dict = ContextInfo.get_full_tick([target_stock])
if target_stock in tick_dict:
price_from_tick = tick_dict[target_stock]['lastPrice']
# 假设 15.15 是止损线
if price_from_tick < 15.15:
# 【防线】不要立刻下单,用 get_market_data_ex 再查一次 1分钟线的收盘价或最新 tick
verify_data = ContextInfo.get_market_data_ex(['close'], [target_stock], period='1m', count=1)
if not verify_data[target_stock].empty:
price_from_kline = verify_data[target_stock]['close'].iloc[-1]
# 如果两者偏差过大(例如超过 1%),说明数据异常,放弃本次操作
if abs(price_from_tick - price_from_kline) / price_from_kline > 0.01:
print(f"警告:数据异常!Tick价格 {price_from_tick} 与 K线价格 {price_from_kline} 偏差过大,拒绝止损。")
return
# 验证通过,执行下单
# passorder(...)
三、 总结
- 弃用裸调:尽量避免在未显式订阅的情况下直接调用
get_full_tick获取非主图品种的最新价。 - 改用推送:实盘高频监控(如止损)强烈建议使用
subscribe_quote事件驱动模式。 - 增加校验:在触发关键交易信号时,增加时间戳校验或多接口交叉验证,是成熟量化系统的必备容错机制。