vn.py官网
开源量化社区
Member
加入于:
帖子: 11
声望: 2

今天回测的策略需要用到日K线,仿照默认BarGeneratorupdate_bar()方法的小时单位K线合成的逻辑,再下面添加了合成日K线的代码:

# vnpy/trader/utility.py BarGenerator update_bar()
elif self.interval == Interval.HOUR:
    if self.last_bar and bar.datetime.hour != self.last_bar.datetime.hour:
        # 1-hour bar
        if self.window == 1:
            finished = True
        # x-hour bar
        else:
            self.interval_count += 1

            if not self.interval_count % self.window:
                finished = True
                self.interval_count = 0
elif self.interval == Interval.DAILY:
    # 以下是我添加的
    if self.last_bar and bar.datetime.day != self.last_bar.datetime.day:
        if self.window == 1:
            finished = True
        else:
            self.interval_count += 1

            if not self.interval_count % self.window:
                finished = True
                self.interval_count = 0

回测发现合成的日K线和从其它平台获取的有出入,特别是开盘价和收盘价都完全不同。仔细看了一下update_bar()方法逻辑,发现在判断是否合成完毕前,函数会将最新的bar的数据添加到self.window_bar里。这里貌似存在以下问题,例如我要合成1小时K线,那么9点开始的这个1小时K线的时间范围应该(按我理解)是从9:00到9:59。在发现新的barbar.datetime.hour值不是9而是10时,9点的K线合成完毕,下面是合成10点这1小时的K线。

但是update_bar()方法首先将最新的bar的数据添加到self.window_bar里,再进行判断,会导致10点的第1个bar被计算到了9点的1小时K线里。这也是为什么我直接在下面添加合成日线的逻辑,出来的日线数据会和实际数据的开盘、收盘价完全不同的原因,因为每次的开盘bar都被算到了前1个合成K线的收盘bar里,而收盘bar都算进了下一个合成K线的开盘bar里。

我想可能将判断是否合成完毕的代码放到update_bar()方法开头会比较合理,但我不知道自己的想法有没有错,纯作交流:

# vnpy/trader/utility.py BarGenerator
def update_bar(self, bar: BarData) -> None:
    """
    Update 1 minute bar into generator
    """

    if self.window_bar is not None:
        finished = False
        if self.interval == Interval.MINUTE:
            # 对于分钟,只能合成60分钟以内的K线
            # 不知道这么写对不对
            if not (bar.datetime.minute) % self.window:
                finished = True
        elif self.interval == Interval.HOUR:
            if self.last_bar and bar.datetime.hour != self.last_bar.datetime.hour:
                # 1-hour bar
                if self.window == 1:
                    finished = True
                # x-hour bar
                else:
                    self.interval_count += 1

                    if not self.interval_count % self.window:
                        finished = True
                        self.interval_count = 0
        elif self.interval == Interval.DAILY:
            if self.last_bar and bar.datetime.day != self.last_bar.datetime.day:
                if self.window == 1:
                    finished = True
                else:
                    self.interval_count += 1
                    if not self.interval_count % self.window:
                        finished = True
                        self.interval_count = 0
        if finished:
            self.on_window_bar(self.window_bar)
            self.window_bar = None

    # If not inited, creaate window bar object
    if not self.window_bar:
        # Generate timestamp for bar data
        if self.interval == Interval.MINUTE:
            dt = bar.datetime.replace(second=0, microsecond=0)
        elif self.interval == Interval.HOUR:
            dt = bar.datetime.replace(minute=0, second=0, microsecond=0)
        elif self.interval == Interval.DAILY:
            dt = bar.datetime.replace(hour=0, minute=0, 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 into window bar
    self.window_bar.close_price = bar.close_price
    self.window_bar.volume += int(bar.volume)
    self.window_bar.open_interest = bar.open_interest

    # Cache last bar object
    self.last_bar = bar

另外按照回测引擎的逻辑,会导致最后1天的日K线无法合成出来,比如我要回测4月20日到4月27日,由于回测时4月27日没有bar数据进入,就会导致4月26日的日K线生成条件bar.datetime.day != self.last_bar.datetime.day不会满足,即使4月26日的数据跑完了,也不会出来4月26日的日K线数据(小时K线也是同理的),不知道这算不算bug。

Member
avatar
加入于:
帖子: 32
声望: 1

小时周期的判定就是有问题的,不能通过

        elif self.interval == Interval.HOUR:
            # TODO: 此处有严重 bug ,会将下一个小时的第一分钟数据合并到前一个小时中
            if self.last_bar and bar.datetime.hour != self.last_bar.datetime.hour:
                # 1-hour bar
                if self.window == 1:
                    return True
                # x-hour bar
                else:
                    self.interval_count += 1
                    if not self.interval_count % self.window:
                        self.interval_count = 0
                        return True
            return False

代码中的方法去判定,因为当小时发生变化的时候,已经是新的一根 k 线了,再将其合并到当前 window_bar 中必然出错。
应该用类似于分钟的判定方式:

if (bar.datetime.minute + 1) % 60:

但是这种方式又会遇到另一个问题,有的品种收盘并不在整点,比如收盘在 23:30 的,会导致最后一根小时 k 出不来。
暂时还没想到好的解决方案,可能需要做一些特殊配置。

Member
avatar
加入于:
帖子: 32
声望: 1

另外还有一种解决思路就是类似于 tick 的方式:
先判定是否为一个新的 window_bar ,如果是,则调用 on_window_bar ,然后生成新 window_bar ;
这样做的一个坏处就是 on_window_bar 的触发要滞后一个 update_bar 周期,代码大致如下:

    def update_bar2(self, bar: BarData) -> None:
        """update bar to generator"""
        if self.check_window_bar_finished(bar):
            self.on_window_bar(self.window_bar)
            self.window_bar = None

        if not self.window_bar:
            dt = self.normalize_window_bar_datetime(bar.datetime)
            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 into window bar
        self.window_bar.close_price = bar.close_price
        self.window_bar.volume += int(bar.volume)
        self.window_bar.open_interest = bar.open_interest

        self.last_bar = bar
© 2015-2019 上海韦纳软件科技有限公司
备案服务号:沪ICP备18006526号-3

沪公网安备 31011502017034号