VeighNa量化社区
你的开源社区量化交易平台 | vn.py | vnpy
Member
avatar
加入于:
帖子: 1
声望: 0

每次启动VNPY程序时如果用的是2小时、4小时、6小时K线周期时,如果启动时间是偶数时间(8:00、10:00、12:00),合成的K线锚定时间和交易所的锚定时间对不上,奇数时间启动(7:00、9:00、11:00)对2小时周期的K线合成没有影响,1小时以下的周期则对启动时间没有影响,不知道你们遇到过这个问题没有,如果遇到了你们是怎么解决的

Super Moderator
avatar
加入于:
帖子: 128
声望: 14

这是一个关于 VeighNa 中 BarGenerator(K线合成器)时间窗口对齐机制的问题。让我先查询相关文档和源代码来理解其工作原理。

让我查询 BarGenerator 的源代码,了解其时间窗口对齐的具体实现:

这是一个非常经典且重要的问题!你描述的现象确实存在,根本原因是 VeighNa 的 BarGenerator 是基于策略启动后的第一个数据时间点来锚定时间窗口,而不是固定的交易所时间锚点

问题根源分析

根据 vnpy.trader.utility.BarGenerator 的源代码(utility.py 第 175-280 行),小时K线合成的逻辑如下:

def on_hour_bar(self, bar: BarData) -> None:
    """"""
    if self.window == 1:
        if self.on_window_bar:
            self.on_window_bar(bar)
    else:
        if not self.window_bar:
            # 第一个小时K线到达时创建窗口,时间戳以第一个数据为准
            self.window_bar = BarData(
                datetime=bar.datetime,  # <-- 这里使用第一个数据的时间
                ...
            )
        ...
        self.interval_count += 1
        if not self.interval_count % self.window:
            # 完成一个窗口周期
            self.interval_count = 0
            if self.on_window_bar:
                self.on_window_bar(self.window_bar)
            self.window_bar = None

问题所在:

  • 当策略在 8:00 启动(偶数时间),2小时窗口为:8:00→10:00→12:00→14:00...
  • 当策略在 9:00 启动(奇数时间),2小时窗口为:9:00→11:00→13:00→15:00...
  • 交易所标准 2小时K线通常从 9:00 开始:9:00→11:00→13:00→15:00...

解决方案

方案一:修改 BarGenerator 支持固定时间锚点(推荐)

创建一个自定义的 AlignedBarGenerator,支持指定锚定时间:

from datetime import datetime, time
from collections.abc import Callable

from vnpy.trader.object import BarData
from vnpy.trader.constant import Interval
from vnpy.trader.utility import BarGenerator

class AlignedBarGenerator(BarGenerator):
    """
    支持固定时间锚定的K线生成器

    特点:
    - 小时线合成时,可以指定锚定时间(如9:00),确保K线周期与交易所对齐
    - 适用于2小时、4小时、6小时等非1小时周期的K线合成
    """

    def __init__(
        self,
        on_bar: Callable,
        window: int = 0,
        on_window_bar: Callable | None = None,
        interval: Interval = Interval.MINUTE,
        daily_end: time | None = None,
        anchor_hour: int | None = None  # 新增:锚定小时(如9表示从9点开始)
    ) -> None:
        """
        参数:
            anchor_hour: 小时线锚定的小时数(0-23),
                        例如设置为9表示小时线从9:00开始计算
        """
        super().__init__(on_bar, window, on_window_bar, interval, daily_end)
        self.anchor_hour: int | None = anchor_hour

    def on_hour_bar(self, bar: BarData) -> None:
        """重写小时线处理逻辑,支持固定时间锚定"""
        if self.window == 1:
            if self.on_window_bar:
                self.on_window_bar(bar)
            return

        # 计算当前小时相对于锚定时间的偏移
        if self.anchor_hour is not None:
            # 将时间对齐到锚定时间基准
            aligned_hour = (bar.datetime.hour - self.anchor_hour) % self.window
            is_window_end = aligned_hour == 0
        else:
            # 原逻辑:基于启动时间
            is_window_end = not self.interval_count % self.window

        if not self.window_bar:
            # 创建新的窗口K线,时间戳对齐到窗口开始
            dt: datetime = bar.datetime.replace(minute=0, second=0, microsecond=0)

            # 如果设置了锚定时间,调整时间戳到窗口起点
            if self.anchor_hour is not None:
                window_start_hour = bar.datetime.hour - ((bar.datetime.hour - self.anchor_hour) % self.window)
                dt = dt.replace(hour=window_start_hour)

            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
            )
        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
            )

        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

        # 使用锚定时间判断窗口是否结束
        if self.anchor_hour is not None:
            should_finish = is_window_end and bar.datetime.minute == 59
        else:
            self.interval_count += 1
            should_finish = not self.interval_count % self.window

        if should_finish:
            if self.anchor_hour is None:
                self.interval_count = 0

            if self.on_window_bar:
                self.on_window_bar(self.window_bar)
            self.window_bar = None

方案二:策略层面使用时间过滤(简单实用)

如果不想修改底层代码,可以在策略中通过时间过滤来对齐:

from datetime import time
from vnpy_ctastrategy import CtaTemplate, BarGenerator, ArrayManager
from vnpy.trader.constant import Interval
from vnpy.trader.object import BarData, TickData

class AlignedHourStrategy(CtaTemplate):
    """使用固定时间锚定的策略示例"""

    author: str = "Your Name"

    # 策略参数
    window: int = 2              # 2小时K线
    anchor_hour: int = 9         # 锚定时间9:00

    def on_init(self) -> None:
        """策略初始化"""
        self.write_log("策略初始化")

        # 使用标准BarGenerator,但在回调中处理时间对齐
        self.bg = BarGenerator(
            self.on_bar,
            window=1,  # 先合成1小时线
            on_window_bar=self.on_hour_bar,
            interval=Interval.HOUR
        )
        self.am = ArrayManager()

        # 用于累积小时线的临时存储
        self.hour_bars: list[BarData] = []

        self.load_bar(10)

    def on_hour_bar(self, bar: BarData) -> None:
        """
        处理1小时K线,手动合成对齐的N小时K线
        """
        # 只保留在正确时间窗口内的小时线
        # 例如2小时线,只保留9:00, 11:00, 13:00, 15:00...的小时线
        hour_offset = (bar.datetime.hour - self.anchor_hour) % self.window

        if hour_offset != 0:
            # 不在窗口起点,跳过或累积到下一个窗口
            return

        self.hour_bars.append(bar)

        # 当累积到足够数量的小时线时,合成窗口K线
        if len(self.hour_bars) >= self.window:
            # 合成窗口K线
            window_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=self.hour_bars[0].datetime,  # 使用窗口起点时间
                gateway_name=bar.gateway_name,
                open_price=self.hour_bars[0].open_price,
                high_price=max(b.high_price for b in self.hour_bars),
                low_price=min(b.low_price for b in self.hour_bars),
                close_price=self.hour_bars[-1].close_price,
                volume=sum(b.volume for b in self.hour_bars),
                turnover=sum(b.turnover for b in self.hour_bars),
                open_interest=self.hour_bars[-1].open_interest
            )

            # 调用策略逻辑
            self.on_aligned_hour_bar(window_bar)

            # 清空累积
            self.hour_bars = []

    def on_aligned_hour_bar(self, bar: BarData) -> None:
        """
        处理对齐后的N小时K线
        """
        self.cancel_all()

        am = self.am
        am.update_bar(bar)
        if not am.inited:
            return

        # 在这里实现你的交易逻辑
        # ...

        self.put_event()

方案三:启动时等待到对齐时间点(简单但不够优雅)

from datetime import datetime, time
import time as time_module

def wait_for_aligned_time(anchor_hour: int = 9, window: int = 2) -> None:
    """
    等待到下一个对齐的时间点再启动策略
    """
    now = datetime.now()
    current_hour = now.hour

    # 计算下一个对齐的小时
    next_aligned_hour = ((current_hour // window) * window) + window
    if next_aligned_hour >= 24:
        next_aligned_hour = anchor_hour  # 第二天从锚定时间开始

    # 如果当前时间正好在对齐点,直接返回
    if current_hour % window == anchor_hour % window:
        return

    # 计算等待时间
    target_time = now.replace(hour=next_aligned_hour, minute=0, second=0, microsecond=0)
    wait_seconds = (target_time - now).total_seconds()

    if wait_seconds > 0:
        print(f"等待 {wait_seconds/60:.1f} 分钟到对齐时间点 {next_aligned_hour}:00")
        time_module.sleep(wait_seconds)

推荐做法

方案 适用场景 优缺点
方案一 长期使用多周期K线 一劳永逸,但需要维护自定义类
方案二 偶尔使用,快速实现 无需修改底层,代码稍复杂
方案三 临时解决方案 简单但会延迟启动

建议:如果是生产环境长期使用,推荐方案一,将 AlignedBarGenerator 封装成可复用的组件,可以确保所有策略的K线时间一致性。

你目前使用的是哪种方式?如果需要,我可以帮你进一步完善方案一的代码。

© 2015-2022 上海韦纳软件科技有限公司
备案服务号:沪ICP备18006526号

沪公网安备 31011502017034号

【用户协议】
【隐私政策】
【免责条款】