第 6 章 · 01 BarGenerator:Tick 到 K 线的合成器


文档摘要

第 6 章 · 01 BarGenerator:Tick 到 K 线的合成器 本节摘要:本节精读 的第一职责——把逐笔 Tick 合成 1 分钟 K 线。这是国内期货 CTA 策略的入口:CTP 推送的是高频 Tick(每秒多个),但绝大多数趋势策略基于 1 分钟(或更大周期)K 线决策,所以需要"降频"。BarGenerator 的 updatetick 方法接收 Tick,按"交易所时钟面的分钟切片"判定跨分钟,跨分钟时推送上一根并起一根新的;同分钟内累加 high/low/close,并用 算增量成交量。本节逐行拆解这套逻辑,并讲清为什么 volume 必须做增量而非直接用累计值。 内容来源:原项目源码 (BarGenerator 类 L166-485),精读并套用体系化模板。

第 6 章 · 01 BarGenerator:Tick 到 K 线的合成器

本节摘要:本节精读 BarGenerator 的第一职责——把逐笔 Tick 合成 1 分钟 K 线。这是国内期货 CTA 策略的入口:CTP 推送的是高频 Tick(每秒多个),但绝大多数趋势策略基于 1 分钟(或更大周期)K 线决策,所以需要"降频"。BarGenerator 的 update_tick 方法接收 Tick,按"交易所时钟面的分钟切片"判定跨分钟,跨分钟时推送上一根并起一根新的;同分钟内累加 high/low/close,并用 tick.volume - last_tick.volume 算增量成交量。本节逐行拆解这套逻辑,并讲清为什么 volume 必须做增量而非直接用累计值。

内容来源:原项目源码 vnpy/trader/utility.py(BarGenerator 类 L166-485),精读并套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. 说清 BarGenerator __init__ 的五个参数(on_bar/window/on_window_bar/interval/daily_end)。
  2. 逐行读懂 update_tick 的跨分钟判定与新 Bar 初始化。
  3. 解释 volume/turnover 为什么用"与上一个 tick 求差"做增量累加。
  4. 理解 high/low 为什么同时参考 last_price 和 tick 自带的 high/low。
  5. 知道 generate() 方法在什么时候被手动调用。

一、BarGenerator 的双重职责

BarGenerator 干两件事(本节讲第一件,第 02 节讲第二件):

  1. Tick → 1 分钟 Bar:update_tick(tick) 接收逐笔 Tick,跨分钟时通过 on_bar(bar) 回调推送 1 分钟 Bar。
  2. 1 分钟 Bar → N 分钟/N 小时/日 Bar:update_bar(bar) 接收 1 分钟 Bar,按 window/interval 配置合成更大周期,通过 on_window_bar(bar) 回调推送。

一个典型的 CTA 策略用法:

class MyStrategy(CtaTemplate): def __init__(self): # 1 分钟 Bar 回调:喂给 15 分钟合成器 self.bg = BarGenerator(self.on_bar, 15, self.on_15min_bar, Interval.MINUTE) # 15 分钟 Bar 的 ArrayManager self.am = ArrayManager() def on_tick(self, tick): self.bg.update_tick(tick) # Tick → 1 分钟 def on_bar(self, bar): self.bg.update_bar(bar) # 1 分钟 → 15 分钟 def on_15min_bar(self, bar): self.am.update_bar(bar) # 15 分钟 → 数组 if self.am.inited: ma = self.am.sma(10) # 算指标

二、init 参数

utility.py:176-202:

176 def __init__( 177 self, 178 on_bar: Callable, 179 window: int = 0, 180 on_window_bar: Callable | None = None, 181 interval: Interval = Interval.MINUTE, 182 daily_end: time | None = None 183 ) -> None: 184 self.bar: BarData | None = None # 当前正在合成的 1 分钟 bar 185 self.on_bar: Callable = on_bar # 1 分钟 bar 完成回调 186 187 self.interval: Interval = interval # 合成目标:MINUTE/HOUR/DAILY 188 self.interval_count: int = 0 # 小时 bar 的窗口计数 189 190 self.hour_bar: BarData | None = None # 合成小时 bar 的中间态 191 self.daily_bar: BarData | None = None # 合成日 bar 的中间态 192 193 self.window: int = window # 窗口大小(N) 194 self.window_bar: BarData | None = None # 合成 N 分钟/N 小时 bar 的中间态 195 self.on_window_bar: Callable | None = on_window_bar 196 197 self.last_tick: TickData | None = None # 上一个 tick,用于算 volume 增量 198 199 self.daily_end: time | None = daily_end 200 if self.interval == Interval.DAILY and not self.daily_end: 201 raise RuntimeError(_("合成日K线必须传入每日收盘时间"))

关键参数:

  • on_bar(必填):1 分钟 Bar 完成时的回调,策略在这里收到分钟 Bar。
  • window:窗口大小,比如 15 表示合成 15 分钟。0 表示不做窗口合成(只产出 1 分钟 Bar)。
  • on_window_bar:窗口 Bar(N 分钟/N 小时/日)完成时的回调。
  • interval:合成目标的周期枚举,默认 MINUTE。
  • daily_end:合成日 K 线时必须传每日收盘时间(否则报错),因为日 Bar 完成要靠"到收盘时间"判定。

三、update_tick:Tick → 1 分钟 Bar

核心方法 utility.py:204-260(节选关键):

204 def update_tick(self, tick: TickData) -> None: 205 new_minute: bool = False 206 207 # 过滤 last_price 为 0 的异常 tick 208 if not tick.last_price: 209 return 210 211 if not self.bar: 212 new_minute = True 213 elif ( 214 (self.bar.datetime.minute != tick.datetime.minute) 215 or (self.bar.datetime.hour != tick.datetime.hour) 216 ): 217 self.bar.datetime = self.bar.datetime.replace(second=0, microsecond=0) 218 self.on_bar(self.bar) 219 new_minute = True 220 221 if new_minute: 222 # 新一根 1 分钟 bar,OHLC 全部初始化为当前 last_price 223 self.bar = BarData( 224 symbol=tick.symbol, exchange=tick.exchange, 225 interval=Interval.MINUTE, datetime=tick.datetime, 226 gateway_name=tick.gateway_name, 227 open_price=tick.last_price, 228 high_price=tick.last_price, 229 low_price=tick.last_price, 230 close_price=tick.last_price, 231 open_interest=tick.open_interest 232 )

逐段解读:

1. 过滤异常 tick(208-209 行):last_price 为 0 通常是开盘前的废 tick,直接丢弃。

2. 跨分钟判定(211-219 行):两种情况算"新的一分钟"——

  • self.bar 为 None(第一根,还没起 bar);
  • 或当前 tick 的 minute/hourself.bar 的不同(跨分钟了)。

跨分钟时,先把上一根 bar 的 datetime 的秒/微秒抹零(217 行,让它代表"这一分钟起点"),再 on_bar(self.bar) 推送(218 行)。

💡 核心心法:跨分钟判定看的是交易所时钟面的 minute/hour,而非"距上个 tick 60 秒"。这是因为真实交易里 tick 到达间隔不均匀(可能 5 秒没 tick 然后 1 秒来 10 个),按钟面切片才能保证每根 Bar 严格对应一个完整分钟。

3. 新 Bar 初始化(221-232 行):跨分钟了,起一根新 bar,OHLC 全部初始化为当前 last_price(开盘价 = 最高 = 最低 = 收盘 = 第一笔成交价)。

4. 同分钟内更新(继续往下):

233 elif self.bar: 234 # 同一分钟内,更新 high/low/close 235 self.bar.high_price = max(self.bar.high_price, tick.last_price) 236 if self.last_tick and tick.high_price > self.last_tick.high_price: 237 self.bar.high_price = max(self.bar.high_price, tick.high_price) 238 239 self.bar.low_price = min(self.bar.low_price, tick.last_price) 240 if self.last_tick and tick.low_price < self.last_tick.low_price: 241 self.bar.low_price = min(self.bar.low_price, tick.low_price) 242 243 self.bar.close_price = tick.last_price 244 self.bar.open_interest = tick.open_interest 245 self.bar.datetime = tick.datetime

high/low 的更新有个细节——同时参考 last_price 和 tick 自带的 high_price/low_price(236-237 行)。因为有些交易所的 tick 里直接带"累计最高/最低价",取更宽松者保证不漏极值。close 永远是最新 last_price,datetime 也更新到最新 tick 时间。

四、volume 的增量累加(关键难点)

247 # volume/turnover 是累计值,要靠与上一个 tick 求差得到增量 248 if self.last_tick and self.bar: 249 volume_change: float = tick.volume - self.last_tick.volume 250 self.bar.volume += max(volume_change, 0) 251 252 turnover_change: float = tick.turnover - self.last_tick.turnover 253 self.bar.turnover += max(turnover_change, 0) 254 255 self.last_tick = tick

⚠️ 重要说明:这是 Tick 合成 Bar 最容易踩的坑。交易所推送的 tick.volume 通常是当日累计成交量(从开盘到现在总共成交了多少手),不是"这一个 tick 成交了多少"。如果直接 self.bar.volume = tick.volume,得到的 Bar volume 就是"到这根 Bar 为止的累计量",而非"这根 Bar 内的成交量"——毫无意义。

正确做法是与上一个 tick 求差:volume_change = tick.volume - last_tick.volume,得到"从上个 tick 到这个 tick 之间成交了多少",累加进 bar。max(..., 0) 是防御跨日重置(开盘第一笔 tick.volume 可能小于昨日最后一笔)导致的负数。

最后一行 self.last_tick = tick 把当前 tick 存起来,供下一个 tick 求差。

五、generate:收尾推送残缺 Bar

utility.py:474-485:

474 def generate(self) -> BarData | None: 475 bar: BarData | None = self.bar 476 if bar: 477 bar.datetime = bar.datetime.replace(second=0, microsecond=0) 478 self.on_bar(bar) 479 self.bar = None 480 return bar

用途:盘中如果一直有 tick 进来,跨分钟时会自然推送。但收盘后没有新 tick,最后一根未走完的 1 分钟 Bar 永远等不到"跨分钟"触发,会残留在 self.bar 里。

策略或引擎在合适时机(如收盘、断开连接、查询周期结束)手动调 generate(),把残留 Bar 按当前状态推送出去,避免丢最后一根。这就是为什么实盘里"15:00 收盘后还能收到最后一根 Bar"——靠的是收盘逻辑调 generate。

六、一个完整分钟的合成过程

假设 10:30:00 到 10:31:00 之间的 tick 流(简化):

10:30:15 last=100.0 volume=1000 → 新 bar(open=high=low=close=100.0) 10:30:30 last=101.5 volume=1050 → 更新 high=101.5, close=101.5, vol+=50 10:30:45 last=100.5 volume=1080 → 更新 low=100.5, close=100.5, vol+=30 10:31:05 last=102.0 volume=1120 → 跨分钟!推送 bar(O100 H101.5 L100.5 C100.5 V80) 起新 bar(open=high=low=close=102.0)

最终 10:30 这根 Bar:open=100.0, high=101.5, low=100.5, close=100.5, volume=80(1050-1000 + 1080-1050 = 80)。

本节要点回顾

  1. 双重职责:Tick→1 分钟 Bar(update_tick)+ 1 分钟→N分钟/小时/日 Bar(update_bar)。
  2. init 关键参数:on_bar(必填)、window(窗口)、on_window_bar(窗口回调)、interval(目标周期)、daily_end(日收盘)。
  3. 跨分钟判定看钟面 minute/hour,非"60 秒倒计时"。
  4. 新 Bar 初始化:OHLC 全部 = 第一笔 last_price。
  5. volume 增量累加:tick.volume - last_tick.volume,因为 tick.volume 是当日累计值。
  6. high/low 双参考:同时看 last_price 和 tick 自带的累计高低价,取更宽松。
  7. generate():收盘后手动推送残留的最后一根 Bar。

下一节,我们看 1 分钟 Bar 怎么合成 N 分钟/N 小时/日 Bar——update_bar 的三个分支,以及为什么 N 分钟必须整除 60。


发布者: 作者: 灏天文库 转发
评论区 (0)
U