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

在BarGenerator利用tick数据合成1分钟bar的uptick函数中,当tick如下时,9:30的bar无法取到第1个tick内产生的最低值4516.8,原因是第一次初始化self.bar的时候, “ low_price=tick.last_price,”( if new_minute:的源码中)。请确认下是否有这种情况,以及是否会有补丁
第1个tick:时间=2025-09-01 09:30:00.5, last_price=4518.8, low_price=4516.8
第2个tick:时间=2025-09-01 09:30:01, last价格=4519.0, low_price=4516.8
...
相关源码:
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
)

Member
加入于:
帖子: 6138
声望: 382

都是更新last_price,tick.low_price是一天的最低价,不影响的


Member
avatar
加入于:
帖子: 23
声望: 0

xiaohe wrote:

都是更新last_price,tick.low_price是一天的最低价,不影响的
假设第3个tick里的last_price=4518.4,是9:30这根bar里的最小last_price,
那么按目前程序会出现9:30 的self.bar.low_price=4518.4,但实际上self.bar.low_price=4516.8(出现在第一个tick的low_price里,但不反映在last_price里,当然也是一天的最低价)

Member
加入于:
帖子: 6138
声望: 382

下面self.bar.low_price = min(self.bar.low_price, tick.low_price)会更新的


Member
avatar
加入于:
帖子: 23
声望: 0

xiaohe wrote:

下面self.bar.low_price = min(self.bar.low_price, tick.low_price)会更新的

第1个tick:时间=2025-09-01 09:30:00.5, last_price=4518.8, low_price=4516.8
第2个tick:时间=2025-09-01 09:30:01, last_price=4519.0, low_price=4516.8
第3个tick:时间=2025-09-01 09:30:01.5, last_price=4518.4, low_price=4516.8
...
第120个tick: 时间=2025-09-01 09:30:59.5, last_price=4519.0, low_price=4516.8

当收到第1个tick后,self.bar.low_price=4518.8,
当收到第3个tick后,self.bar.low_price=4518.4,
因为只有 if self.last_tick and tick.low_price < self.last_tick.low_price:
才能 self.bar.low_price = min(self.bar.low_price, tick.low_price),
但这个条件是没法实现的,因为后续的 tick.low_price = self.last_tick.low_price,没能出现比前一个更低价的tick.low_price,所以第一个tick的 low_price没法采集到了。我在想是否可以改成这样:
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
)
if (self.last_tick and tick.high_price > self.last_tick.high_price) or (self.last_tickNone) :
self.bar.high_price = max(self.bar.high_price, tick.high_price)
if (self.last_tick and tick.low_price < self.last_tick.low_price) or (self.last_tick
None) :
self.bar.low_price = min(self.bar.low_price, tick.low_price)
elif self.bar:

Member
加入于:
帖子: 6138
声望: 382

bar.low_price记录的是一根K线里的low_price,当收到第一个tick的时候,K线里只有一个tick,不存在high和low,收到第二个tick的时候才能有比较。所以bg的写法是对的


Member
avatar
加入于:
帖子: 23
声望: 0

“bar.low_price记录的是一根K线里的low_price,当收到第一个tick的时候,K线里只有一个tick,不存在high和low”
当9:40的第一个tick,
第1个tick:时间=2025-09-01 09:40:00.5, last_price=4518.8, low_price=4516.8(同时也是本日最低值)
第2个tick:时间=2025-09-01 09:40:01, last_price=4519.0, low_price=4516.8
第3个tick:时间=2025-09-01 09:40:01.5, last_price=4518.4, low_price=4516.8

在第一个tick时,因为是新的分钟,会创建一个新的bar,其中low_price被设置为4518.8(tick.last_price),而不是4516.8(tick.low_price)。
然后在第二个tick时,会更新bar:
self.bar.low_price = min(self.bar.low_price, tick.last_price)
if self.last_tick and tick.low_price < self.last_tick.low_price:
self.bar.low_price = min(self.bar.low_price, tick.low_price)
这里会比较当前bar的low_price(4518.8)和tick.last_price(4519.0),取最小值4518.8。 然后检查self.last_tick(即第一个tick)和当前tick的low_price,第一个tick的low_price是4516.8,当前tick的low_price也是4516.8,所以不会更新。
最终9:40的bar的low_price是4518.8,而不是应该的4516.8。
建议修改方法:当新的一分钟开始时,
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
)
if (self.last_tick and tick.high_price > self.last_tick.high_price) or (self.last_tickNone) :
self.bar.high_price = max(self.bar.high_price, tick.high_price)
if (self.last_tick and tick.low_price < self.last_tick.low_price) or (self.last_tick
None) :
self.bar.low_price = min(self.bar.low_price, tick.low_price)
elif self.bar:

Member
avatar
加入于:
帖子: 23
声望: 0

if (self.last_tick and tick.low_price < self.last_tick.low_price)对应9:40:0.5 这种跨分钟的新的一分钟;
(self.last_tick==None) 对应9:30:0.2 这种全天的第一分钟(全天的第一个tick也可能last_price与low_price不一致),同时全天第一根tick的交易量没有初始化,因为不满足下述“if self.last_tick ”的条件
if self.last_tick and self.bar:
volume_change: float = tick.volume - self.last_tick.volume
self.bar.volume += max(volume_change, 0)

Member
加入于:
帖子: 6138
声望: 382

bar.low_price记录的是一根K线里的low_price不是当日的low_price
tick.low_price是当日的low_price,有更低的价格API会自行更新tick.low_price


Member
avatar
加入于:
帖子: 23
声望: 0

xiaohe wrote:

bar.low_price记录的是一根K线里的low_price不是当日的low_price
tick.low_price是当日的low_price,有更低的价格API会自行更新tick.low_price

你说的完全正确,问题就出在当一根bar线的第一个tick里出现了当天的最低tick.low_price,这根bar线取不到这个tick.low_price,因为初始化self.bar的时候取的是last_price,后续第二个tick因为不满足self.last_tick and tick.low_price < self.last_tick.low_price,也无法更新self.bar.low_price
如图所示IF2509.CFFEX 2025-08-04 9:30分的bar线 代码运行后,最低价是4018.6,实际上应该是4017.4

description

Member
avatar
加入于:
帖子: 23
声望: 0

更新部分代码:

        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
            )
            '''
            修正新的bar线当第一个tick出现当日最高价、最低价时,原有算法取不到该值的问题,同步更新初始成交量
            '''
            if (self.last_tick and tick.high_price > self.last_tick.high_price) or (self.last_tick == None):
                self.bar.high_price = max(self.bar.high_price, tick.high_price)
            if (self.last_tick and tick.low_price < self.last_tick.low_price) or (self.last_tick == None):
                self.bar.low_price = min(self.bar.low_price, tick.low_price)

            if (self.last_tick == None):
                self.bar.volume = max(tick.volume, 0)
                self.bar.turnover = max(tick.volume, 0)
Member
加入于:
帖子: 6138
声望: 382

收到,感谢反馈。我们下个版本会更新,方便的话也可以去github发个PR


Member
avatar
加入于:
帖子: 23
声望: 0

xiaohe wrote:

收到,感谢反馈。我们下个版本会更新,方便的话也可以去github发个PR
好哒

Member
avatar
加入于:
帖子: 23
声望: 0

xiaohe wrote:

收到,感谢反馈。我们下个版本会更新,方便的话也可以去github发个PR
已经发起了PR

Member
avatar
加入于:
帖子: 23
声望: 0

大核桃 wrote:

更新部分代码:

        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
            )
            '''
            修正新的bar线当第一个tick出现当日最高价、最低价时,原有算法取不到该值的问题,同步更新初始成交量
            '''
            if (self.last_tick and tick.high_price > self.last_tick.high_price) or (self.last_tick == None):
                self.bar.high_price = max(self.bar.high_price, tick.high_price)
            if (self.last_tick and tick.low_price < self.last_tick.low_price) or (self.last_tick == None):
                self.bar.low_price = min(self.bar.low_price, tick.low_price)

            if (self.last_tick == None):
                self.bar.volume = max(tick.volume, 0)
                self.bar.turnover = max(tick.volume, 0)

新版vnpy4.5.1的BarGenerator的update_tick还是没有解决当self.last_tick == None的情形,需要按照这个PR进行修复。

一、验证vnpy4.5.1没有修复的代码:

from vnpy.trader.constant import Exchange
from vnpy.trader.database import get_database
from vnpy.trader.utility import BarGenerator  #4.5.1版本
from vnpy.trader.object import TickData, BarData
from datetime import datetime

# 全局变量,用来接住合成出来的bar,分别是2025年8月4日9:30和9:31分的分钟Bar,品种为IF2509.CFFEX
target_bar_930: BarData = None
target_bar_931: BarData = None

def on_bar(bar: BarData):
    """BarGenerator回调,收到合成完成的K线"""
    global target_bar_930, target_bar_931
    bar_naive = bar.datetime.replace(tzinfo=None)

    # 同时捕获 09:30 和 09:31 两根分钟Bar
    if (bar.symbol == "IF2509"
        and bar.exchange.value == "CFFEX"
        and bar_naive in (datetime(2025, 8, 4, 9, 30), datetime(2025, 8, 4, 9, 31))
    ):
        # 区分两根bar分别保存
        if bar_naive == datetime(2025, 8, 4, 9, 30):
            target_bar_930 = bar
        elif bar_naive == datetime(2025, 8, 4, 9, 31):
            target_bar_931 = bar

        print(f"✅捕获目标分钟Bar:{bar.datetime}")
        print(f"low_price: {bar.low_price}")
        print(f"high_price: {bar.high_price}")
        print(f"Volume: {bar.volume}")
        print(f"Turnover: {bar.turnover}")
        print("-" * 40)

def test_bar_generator_update_tick():
    db = get_database()
    symbol = "IF2509"
    exchange = Exchange.CFFEX
    start = datetime(2025, 8, 4, 9, 30, 0)
    end = datetime(2025, 8, 4, 9, 32, 0,microsecond=200000) #为了取得9:30和9:31分中数据
    # 从数据库读取该分钟全部tick数据
    tick_list: list[TickData] = db.load_tick_data(
        symbol=symbol,
        exchange=exchange,
        start=start,
        end=end
    )
    print(f"读取到 {len(tick_list)} 条tick")
    print("第一条tick时间:", tick_list[0].datetime)
    print("最后一条tick时间:", tick_list[-1].datetime)
    if not tick_list:
        print("❌数据库没有找到该时间段tick数据")
        return
    # 创建BarGenerator,合成1分钟K线,绑定on_bar回调
    bg = BarGenerator(on_bar)
    # 逐个tick送入update_tick,模拟实盘推送tick
    for tick in tick_list:
        bg.update_tick(tick)

    # 校验09:30结果
    print("\n===== 校验 09:30 Bar =====")
    if target_bar_930:
        #
        expect_low = 4017.4  # 来自米筐数据2025-08-04 9:30:0.2 的tick中出现tick.low_price
        expect_volume = 1619 # 来自米筐数据2025-08-04 9:30:59.7的tick中出现tick.volume,应为要包含第一个tick9:30:0.2的量157,现有算法不包含初始量157.

        actual_low = target_bar_930.low_price
        actual_volume = target_bar_930.volume

        if actual_low == expect_low:
            print(f"✅校验通过,09:30该分钟最低价{actual_low}")
        else:
            print(f"❌校验不匹配,实际low={actual_low}")

        if actual_volume == expect_volume:
            print(f"✅校验通过,09:30该分钟成交量{actual_volume}")
        else:
            print(f"❌校验不匹配,实际成交量={actual_volume}")
    else:
        print("❌没有生成09:30那根分钟bar,请检查tick数据或时间")

    # 校验09:31结果
    print("\n===== 校验 09:31 Bar =====")
    if target_bar_931:
        expect_low = 4019.4   # 来自米筐数据2025-08-04 9:31:09.7 的tick中出现tick.last_price
        expect_volume = 955  # 来自米筐数据2025-08-04 9:31:59.7的tick中出现tick.volume:2574-tick9:30:59.7的量1619=955.

        actual_low = target_bar_931.low_price
        actual_volume = target_bar_931.volume

        if actual_low == expect_low:
            print(f"✅校验通过,09:31该分钟最低价{actual_low}")
        else:
            print(f"❌校验不匹配,实际low={actual_low}")

        if actual_volume == expect_volume:
            print(f"✅校验通过,09:31该分钟成交量{actual_volume}")
        else:
            print(f"❌校验不匹配,实际成交量={actual_volume}")
    else:
        print("❌没有生成09:31那根分钟bar,请检查tick数据或时间")

if __name__ == "__main__":
    test_bar_generator_update_tick()

二、测试数据来自米筐的2025年8月4日9:30到9:32分的IF2509.CFFEX tick数据

Member
avatar
加入于:
帖子: 2000
声望: 160

欢迎在Github上开个Issue

Member
avatar
加入于:
帖子: 23
声望: 0

MTF wrote:

欢迎在Github上开个Issue
已经发起issue