CTA:BarGenerator
K线合成器
VNPY通过CTP接口连接交易所,订阅行情后就能几乎实时地收到Tick数据。如果策略是基于Tick数据的,可以直接使用;如果策略是基于K线的,则K线需要在本地合成。
BarGenerator
支持灵活地合成数据:
- 基于Tick合成1分钟K线
- 基于1分钟K线合成X分钟K线/X小时K线
代码解读
BarGenerator
定义在vnpy.trader.utility
中。
初始设置
class BarGenerator:
def __init__(
self,
on_bar: Callable,
window: int = 0,
on_window_bar: Callable = None,
interval: Interval = Interval.MINUTE,
daily_end: time = None
) -> None:
"""Constructor"""
self.bar: BarData = None
self.on_bar: Callable = on_bar
self.interval: Interval = interval
self.interval_count: int = 0
self.hour_bar: BarData = None
self.daily_bar: BarData = None
self.window: int = window
self.window_bar: BarData = None
self.on_window_bar: Callable = on_window_bar
self.last_tick: TickData = None
self.daily_end: time = daily_end
if self.interval == Interval.DAILY and not self.daily_end:
raise RuntimeError(_("合成日K线必须传入每日收盘时间"))
具体看__init__()
传入的参数:
on_bar
:在策略类中,传入的就是策略的回调函数window
:数据窗口大小,对应灵活定制K线频率,如:5分钟K线,则window=5
on_window_bar
:定制频率K线的回调函数
合成K线
由Tick数据合成K线
每一笔Tick数据包含的主要内容如下:
- 逐笔数据
- 交易品种、交易所和时间戳
- 上一笔成交的金额和成交量(最新成交价、成交量)
- 订单簿快照
- Level1行情:买价5档,卖价5档
def update_tick(self, tick: TickData) -> None:
"""
Update new tick data into generator.
"""
new_minute: bool = False
# Filter tick data with 0 last price
if not tick.last_price:
return
if not self.bar:
new_minute = True
elif (
(self.bar.datetime.minute != tick.datetime.minute)
or (self.bar.datetime.hour != tick.datetime.hour)
):
self.bar.datetime = self.bar.datetime.replace(
second=0, microsecond=0
)
self.on_bar(self.bar)
new_minute = True
if new_minute:
self.bar = BarData(
symbol=tick.symbol,
exchange=tick.exchange,
interval=Interval.MINUTE,
datetime=tick.datetime,
gateway_name=tick.gateway_name,
open_price=tick.last_price,
high_price=tick.last_price,
low_price=tick.last_price,
close_price=tick.last_price,
open_interest=tick.open_interest
)
else:
self.bar.high_price = max(self.bar.high_price, tick.last_price)
if tick.high_price > self.last_tick.high_price:
self.bar.high_price = max(self.bar.high_price, tick.high_price)
self.bar.low_price = min(self.bar.low_price, tick.last_price)
if tick.low_price < self.last_tick.low_price:
self.bar.low_price = min(self.bar.low_price, tick.low_price)
self.bar.close_price = tick.last_price
self.bar.open_interest = tick.open_interest
self.bar.datetime = tick.datetime
if self.last_tick:
volume_change: float = tick.volume - self.last_tick.volume
self.bar.volume += max(volume_change, 0)
turnover_change: float = tick.turnover - self.last_tick.turnover
self.bar.turnover += max(turnover_change, 0)
self.last_tick = tick
逻辑梳理如下:
- 收到一笔Tick数据,与当前Bar数据的时间对比
- 如果这笔Tick数据的时间戳和Bar数据的时间戳在同一分钟
- 则更新当前Bar数据
- Bar数据的最大值是同分钟内Tick数据的成交价最大值,最小值同理,OHLC数据如此生成
- 如果这笔Tick数据的时间戳和Bar数据的时间戳不在在同一分钟
- 则当前Bar数据已完成,将其置空,推送给回调函数
- 生成新的K线
细节有:
- 过滤掉上一笔没有成交价/成交价为0的Tick数据,这会出现在流动性不好的品种上,或是出现错误
if not tick.last_price: return
- 若当前Bar数据为
None
,说明还没有开始K线,设置为开始新的一分钟if not self.bar: new_minute = True
- 结束当前这一分钟时,会将Bar数据时间戳更新,并调用回调函数,这一步使得
BarGenerator
与策略建立了联系(self.bar.datetime.minute != tick.datetime.minute) or (self.bar.datetime.hour != tick.datetime.hour) ): self.bar.datetime = self.bar.datetime.replace( second=0, microsecond=0 ) self.on_bar(self.bar)
由1minK线合成各种频率K线
def update_bar(self, bar: BarData) -> None:
"""
Update 1 minute bar into generator
"""
if self.interval == Interval.MINUTE:
self.update_bar_minute_window(bar)
elif self.interval == Interval.HOUR:
self.update_bar_hour_window(bar)
else:
self.update_bar_daily_window(bar)
先调用update_bar
,再根据interval
设置调用不同频率的合成函数。
def update_bar_minute_window(self, bar: BarData) -> None:
""""""
# If not inited, create window bar object
if not self.window_bar:
dt: datetime = bar.datetime.replace(second=0, microsecond=0)
self.window_bar = BarData(
symbol=bar.symbol,
exchange=bar.exchange,
datetime=dt,
gateway_name=bar.gateway_name,
open_price=bar.open_price,
high_price=bar.high_price,
low_price=bar.low_price
)
# Otherwise, update high/low price into window bar
else:
self.window_bar.high_price = max(
self.window_bar.high_price,
bar.high_price
)
self.window_bar.low_price = min(
self.window_bar.low_price,
bar.low_price
)
# Update close price/volume/turnover into window bar
self.window_bar.close_price = bar.close_price
self.window_bar.volume += bar.volume
self.window_bar.turnover += bar.turnover
self.window_bar.open_interest = bar.open_interest
# Check if window bar completed
if not (bar.datetime.minute + 1) % self.window:
self.on_window_bar(self.window_bar)
self.window_bar = None
update_bar_minute_window
可以合成任意分钟频率的K线
- 不断接收新的Bar数据,更新WindowBar数据
- 当Bar的分钟是windows的倍数时,说明前一个时间窗口已经结束,故推送WindowBar到回调函数,并将WindowBar置空
if not (bar.datetime.minute + 1) % self.window: self.on_window_bar(self.window_bar) self.window_bar = None
(bar.datetime.minute + 1) % self.window
:由于分钟数是从0开始的,故当bar.datetime.minute + 1
能够整除window
时,说明已经过了window
分钟。为了方便,windows
只能为60的公因数。
def update_bar_hour_window(self, bar: BarData) -> None:
""""""
# If not inited, create window bar object
if not self.hour_bar:
dt: datetime = bar.datetime.replace(minute=0, second=0, microsecond=0)
self.hour_bar = BarData(
symbol=bar.symbol,
exchange=bar.exchange,
datetime=dt,
gateway_name=bar.gateway_name,
open_price=bar.open_price,
high_price=bar.high_price,
low_price=bar.low_price,
close_price=bar.close_price,
volume=bar.volume,
turnover=bar.turnover,
open_interest=bar.open_interest
)
return
finished_bar: BarData = None
# If minute is 59, update minute bar into window bar and push
if bar.datetime.minute == 59:
self.hour_bar.high_price = max(
self.hour_bar.high_price,
bar.high_price
)
self.hour_bar.low_price = min(
self.hour_bar.low_price,
bar.low_price
)
self.hour_bar.close_price = bar.close_price
self.hour_bar.volume += bar.volume
self.hour_bar.turnover += bar.turnover
self.hour_bar.open_interest = bar.open_interest
finished_bar = self.hour_bar
self.hour_bar = None
# If minute bar of new hour, then push existing window bar
elif bar.datetime.hour != self.hour_bar.datetime.hour:
finished_bar = self.hour_bar
dt: datetime = bar.datetime.replace(minute=0, second=0, microsecond=0)
self.hour_bar = BarData(
symbol=bar.symbol,
exchange=bar.exchange,
datetime=dt,
gateway_name=bar.gateway_name,
open_price=bar.open_price,
high_price=bar.high_price,
low_price=bar.low_price,
close_price=bar.close_price,
volume=bar.volume,
turnover=bar.turnover,
open_interest=bar.open_interest
)
# Otherwise only update minute bar
else:
self.hour_bar.high_price = max(
self.hour_bar.high_price,
bar.high_price
)
self.hour_bar.low_price = min(
self.hour_bar.low_price,
bar.low_price
)
self.hour_bar.close_price = bar.close_price
self.hour_bar.volume += bar.volume
self.hour_bar.turnover += bar.turnover
self.hour_bar.open_interest = bar.open_interest
# Push finished window bar
if finished_bar:
self.on_hour_bar(finished_bar)
update_bar_hour_window
合成小时线
- 在59min时,结束HourBar的合成,并推送HourBar到回调函数
- 当新接受的Bar的Hour与当前HourBar不同时,说明开始合成新的HourBar
细节:为什么在判断新接受的Bar的Hour与当前HourBar是否相同时,要有如下语句?
elif bar.datetime.hour != self.hour_bar.datetime.hour:
finished_bar = self.hour_bar
适应两种情况:
- 若59min存在,则
finished_bar
已经推送,当前的hour_bar
是None
,故不会推送 - 若因各种原因,导致59min缺失,则可在此推送
finished_bar
与策略的联系
可以定制策略在什么样的频率上面进行回测或者实盘。这可以通过自定义回调函数实现,关键是interval
和update
函数匹配。
此处先略写一下X分钟频率策略思路:
- 假设传入的是Tick数据,首先会调用策略的
on_tick()
函数self.bar_generator.update_tick(tick)
- 调用
BarGenerator
的update_tick()
,它会合成Bar数据,并调用策略的on_bar()
函数 self.on_bar(self.bar)
- 策略的
on_bar()
函数接收到了BarData
- 调用
self.bar_generator.update_bar(bar)
- 则
BarGenerator
会根据interval
调用相应的update
函数合成WindowBar数据,并调用策略的on_windows_bar()
函数
- 调用
- 策略的
on_windows_bar()
函数里写真正的策略逻辑- 同样维护一个K线池,只是K线池里面放WindowBar
- 用基于WindowBar的技术指标实现CTA策略
与数据接口的联系
实盘时,通过CTP接口拿到实时Tick数据。这样的Tick数据再送到BarGenerator
用于合成K线。
回测时
- 若为分钟频率回测,并且回测数据就是分钟级别,则无需
BarGenerator
- 若为X分钟频率回测,则可以按照上面的思路自行编写
on_windows_bar()
逻辑
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话