前言
国内期货趋势量化程序的典型链路是:天勤 TqApi 订阅螺纹钢 5 分钟 K 线,双均线算出「目标净持仓 5 手」这一交易信号,执行层调用 TargetPosTask.set_target_volume(5),task 在后续每次 api.wait_update() 里向期货公司发单、撤单、对价改价,柜台回报刷新到 get_position("SHFE.rb2510").pos(净持仓手数,正为多、负为空)。
排查实盘时,日志连续三天写着 target=5,但 pos 一直是 4 或 6,差一两手对不齐。第一反应往往是「均线算错」,更常见却是:部分成交、场上还有 status=="ALIVE" 的在途单、同一合约重复建了 task、或手写 insert_order 与 task 抢仓位。set_target_volume 当下并不成交,target 与 pos 短期不一致可以是正常现象。下面按天勤单例约束、调仓时序和排查顺序,说明怎么区分正常过渡与真故障。
一、先弄清几个名词
| 名称 | 是什么 | 偏差排查里怎么用 |
|---|---|---|
set_target_volume(n) |
告诉 task 目标净持仓手数 | 只改意图,不立刻成交 |
get_position(symbol).pos |
柜台确认的净持仓 | 真相,正负表示多空 |
TargetPosTask |
天勤自动调仓类 | 在 wait_update 里执行 |
wait_update() |
收行情与交易回报 | 不调就不会推进 task |
get_order() |
当前委托字典 | 看在途 ALIVE 单 |
volume_left |
委托剩余未成交量 | 部分成交时大于 0 |
status |
委托状态 ALIVE/FINISHED | 是否在途 |
min_volume / max_volume |
大单拆分参数 | 分批导致短期对不齐 |
offset_priority |
开平顺序 | 平今失败时 pos 卡住 |
二、为什么 target 和 pos 可以短期不一致
TargetPosTask 文档写得很清楚:set_target_volume 当下不下单,下单撤单发生在之后的 wait_update()。因此下列情况属于正常现象:
- 刚调用
set_target_volume(5),下一行就读pos,仍是旧值。 - 单子已报入交易所,部分成交,
pos在 4 与 5 之间。 - 启用了
min_volume/max_volume拆分,多帧wait_update才凑满目标。 - 对价
price="ACTIVE"追价时,旧单撤完新单未成交,中间有空窗。
需要告警的是:连续多个交易时段、无 ALIVE 单、无拒单,而 pos 仍与 target 差值不变。
三、天勤单例约束:重复建 task 会埋雷
target_pos_task.py 里 TargetPosTask 用单例元类:同一账户同一 symbol 只能有一个 task 实例。若用不同 price、offset_priority、min_volume 再建一次,会直接抛异常。
更隐蔽的是:同一合约既用 TargetPosTask,又手写 insert_order。文档明确禁止混用,否则 task 内部状态与柜台持仓错位,表现为 target 已改但 pos 长期不对,或莫名多出一笔委托。
排查时全局搜索该 symbol 是否只有一处 TargetPosTask,以及是否还有 insert_order 调用。
四、推荐排查顺序
api.wait_update()至少跑几帧,确认不是读太早。- 读
pos = api.get_position(symbol).pos,记target(自己维护或 task 日志)。 - 遍历
get_order(),筛symbol相同且status == "ALIVE"的项,看volume_left与direction。 - 若有拒单,读
last_msg;开平、资金类错误会导致 task 停在某一手数。 - 查是否拆分模式:看创建 task 时的
max_volume。 - 多账户模式是否漏传
account参数,导致读到别的账户持仓。
可落地的日志字段建议每次调仓记:bar_datetime、target、pos、alive_volume(在途剩余量之和)、delta=target-pos。
python
def log_pos_gap(api, symbol, target, logger):
api.wait_update()
pos = api.get_position(symbol).pos
alive = sum(
o.volume_left for o in api.get_order().values()
if o.instrument_id in symbol and o.status == "ALIVE"
)
logger.info(
"pos_gap symbol=%s target=%s pos=%s alive_left=%s",
symbol, target, pos, alive,
)
五、纠偏与恢复
确认无在途单且 pos 仍不对时:
- 以
get_position为准重置本地 state,再set_target_volume到真实目标。 - 若怀疑 task 内部错乱,进程级重启:停机、对账、重建
TargetPosTask(改创建参数必须重启,见发版边界)。 - 不要连续狂调
set_target_volume试图「催成交」,容易触发报单频率规则。
六、典型 last_msg 与偏差共存
| last_msg 关键词 | 常见后果 |
|---|---|
| 资金不足 | target 不变,pos 落后 |
| 非交易时段 | 夜盘衔接误发单 |
| 平今不足 | 空头减仓卡住 |
| 超过最大委托量 | 部分成交 |
拒单后若不读 last_msg,会误以为 task 还在正常追价。
七、模拟与实盘偏差对比
TqSim 往往更快对齐;实盘 ACTIVE 追价在波动大时可能反复撤单重报,gap 持续更久。应用同一套 log_pos_gap 在两种环境各跑一天,建立「正常 gap 持续时间」基线。
总结
期货量化里目标仓与净持仓短期不一致,多数是天勤 TargetPosTask 的正常过渡:调仓在 wait_update 里分步完成,部分成交和拆单都会拉长对齐时间。持续对不齐时,按 pos、在途 ALIVE 单、last_msg、是否混用 insert_order、是否重复建 task 这条链排查;日志里同时记 target 与 pos,比单看信号可靠得多。在 TqSim 或 TqKq 上把偏差场景跑一遍,再挂实盘,能少踩很多「以为程序坏了其实是成交慢」的坑。
FAQ
1)差 1 手要不要强行补单?
先看在途单和部分成交;无在途且拒单已排除,再 set_target_volume 一次即可,避免与 task 内部循环打架。
2)反手时偏差更大正常吗?
反手要先平后开,中间帧 pos 可能经过 0 或反向,属于正常,用日志看全过程。
3)模拟盘会对得更齐吗?
TqSim 成交模型更理想,实盘偏差更大;模拟对齐不能代表实盘一定对齐。
4)能否不用 TargetPosTask?
可以全程 insert_order,但要自己管开平和在途单,工作量更大。
本文基于天勤 TqSdk 公开 API 与源码整理,不构成投资建议。实盘前请在仿真环境验证。