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_barNone,故不会推送
  • 若因各种原因,导致59min缺失,则可在此推送finished_bar

与策略的联系

可以定制策略在什么样的频率上面进行回测或者实盘。这可以通过自定义回调函数实现,关键是intervalupdate函数匹配。

此处先略写一下X分钟频率策略思路:

  • 假设传入的是Tick数据,首先会调用策略的on_tick()函数
    • self.bar_generator.update_tick(tick)
    • 调用BarGeneratorupdate_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()逻辑
posted @   superzzh  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示