前言
国内期货趋势量化里,开仓信号多在 K 线上算:天勤程序 get_kline_serial("DCE.m2509", 300) 订豆粕 5 分钟线,均线金叉时 TargetPosTask.set_target_volume(5) 开多。止损若也等下一根 K 线收盘才检查,夜盘一波急杀时,可能要等 5 分钟才发现亏损扩大,比 tick 级止损多滑很多个最小变动价位。去年改一条豆粕策略:开仓逻辑在 K 线,止损改到 tick(或 quote.last_price)触发,回撤明显收敛,代价是主循环要分清「谁有权改 target」。
天勤允许同一 TqApi 里同时订 get_kline_serial 和 get_tick_serial,共用 api.wait_update() 收包。下面说明 K 线管开仓、tick 管止损的触发分工,避免两者同帧打架。
一、两个序列各自负责什么
| 序列 | API | 触发粒度 | 适合逻辑 |
|---|---|---|---|
| K 线 | get_kline_serial(symbol, duration_seconds, data_length) |
新 bar | 开仓、加减仓信号 |
| Tick | get_tick_serial(symbol, data_length) |
每笔成交聚合 | 止损、移动止盈 |
get_tick_serial 返回 tick 表,含 datetime、last_price、volume 等(以 objs.py 为准)。tick 表也会随 wait_update 更新,用 is_changing(ticks.iloc[-1], "datetime") 判断新 tick。
二、主循环结构示意
原则:开仓权在 K 线,平仓权可拆给 tick;同一帧只应有一个模块写 target。
python
klines = api.get_kline_serial(SYMBOL, 300, data_length=500)
ticks = api.get_tick_serial(SYMBOL, data_length=2000)
task = TargetPosTask(api, SYMBOL, price="ACTIVE")
target = 0
entry_price = None
stop_price = None
while True:
api.wait_update()
# K 线:新 bar 算信号(用已收盘 bar)
if api.is_changing(klines.iloc[-1], "datetime"):
bar = klines.iloc[-2]
signal = calc_signal(klines) # 你的均线/突破逻辑
if signal == 1 and target <= 0:
target = 3
entry_price = bar["close"]
stop_price = entry_price - 2 * quote.price_tick * 10
task.set_target_volume(target)
# Tick:仅在有仓时检查止损
if target != 0 and api.is_changing(ticks.iloc[-1], "datetime"):
last = ticks.iloc[-1]["last_price"]
if target > 0 and last <= stop_price:
target = 0
task.set_target_volume(0)
elif target < 0 and last >= stop_price:
target = 0
task.set_target_volume(0)
quote 需 api.get_quote(SYMBOL) 提前取得,用于 price_tick。
三、常见坑
- tick 止损与 K 线反向信号同帧触发:应定优先级,通常止损优先于开新仓。
- 未用
is_changing过滤,每个包都扫 tick,CPU 升高。 data_length太小,tick 表被截断,极端行情下指标辅助字段丢失。- 停盘期间 tick 不更新,止损应暂停或改用 last_price 快照规则。
- 同一合约混用
insert_order与TargetPosTask,tick 层改 target 后 task 状态错乱。
四、止损价维护
移动止损可在 tick 分支更新 stop_price,但不要每 tick 调 set_target_volume;只有触发击穿时才调仓。若用 ATR 止损,ATR 仍建议用 K 线算,tick 只比较价格与阈值。
五、回测与实盘差异
TqBacktest 对 tick 精度取决于订阅与回测设置;K+tick 双序列在回测里能跑通,但 tick 级止损的成交假设仍比实盘理想。应用 TqSim 对价模式试一轮,记录止损滑点。
六、用 quote.last_price 的轻量替代
若觉得 get_tick_serial 订阅太重,可以只对交易品种 get_quote,在止损分支判断 api.is_changing(quote, "last_price")。这在不少商品上足够驱动止损,但要注意:行情推送频率低于真实 tick 时,止损会有延迟。股指等高频品种仍建议 tick 表。
python
quote = api.get_quote(SYMBOL)
while True:
api.wait_update()
if target > 0 and api.is_changing(quote, "last_price"):
if quote.last_price <= stop_price:
target = 0
task.set_target_volume(0)
七、状态机:避免双写 target
维护显式状态 FLAT / LONG / STOPPING 比仅用 target 整数更安全。进入 STOPPING 后忽略 K 线开仓信号,直到 pos 确认为 0 再回 FLAT。这能避免 tick 止损与 K 线反向信号同帧打架。
| 状态 | 允许 K 线开仓 | 允许 tick 止损 |
|---|---|---|
| FLAT | 是 | 否 |
| LONG/SHORT | 否(除非加仓规则) | 是 |
| STOPPING | 否 | 否 |
总结
K 线算信号、tick 管止损,是国内期货程序化里很常见的分工。天勤允许在同一 TqApi 里并行 get_kline_serial 与 get_tick_serial,靠 wait_update 统一收包,用 is_changing 把两种触发拆开;开仓逻辑放在新 K 线,止损逻辑放在新 tick,且避免双写 target。把优先级和停盘过滤写清楚,比单纯把止损周期改成 1 分钟 K 线更接近真实风控意图。
FAQ
1)tick 数据量很大怎么办?
缩小 data_length,止损只关心最新价;或用 is_changing(quote, "last_price") 替代全表扫描。
2)能否只用 quote 不用 tick_serial?
可以,get_quote 的 last_price 更新也能驱动止损,粒度取决于行情推送频率。
3)多品种怎么订?
每个交易品种各一对 serial;watch 列表外的品种不要订 tick。
4)反手算开仓还是止损?
建议拆成先平后开两帧,由 task 自动处理开平顺序。
本文基于天勤 TqSdk 公开 API 整理,不构成投资建议。