1. 错误现象的发现

1.1 假定您是这么做的:

  • 您在交易日的盘前某个时间(如20:50)启动VNStation,
  • 您备使用CTA策略模块运行你CTA策略;
  • 用鼠标点击您创建好的策略实例界面的“初始化”按钮;
  • 按照通常的做法,您的策略一般会加载一定天数的历史1分钟K线数据;
  • 过短暂的等待,策略的初始化完成了,标志是"启动"按钮由无效变成有效;
  • 用鼠标点击策略实例界面的“启动”按钮,OK,策略 启动完成了!

1.2 错误也发生了

尽管此时还是没有开盘,甚至还没有开始集合竞价,可是您的策略已经从on_tick()接口被推送了一个tick,而且该tick的时间不是当天下午的收盘,也不是您订阅该合约的时间 ! 我把这个tick打印出来了,请看:

TickData(
    gateway_name='CTP', 
    symbol='TA205', 
    exchange=<Exchange.CZCE: 'CZCE'>, 
    datetime=datetime.datetime(2022, 4, 12, 20, 1, 18, 500000, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), 
    name='精对苯二甲酸205', 
    volume=0, 
    turnover=0.0, 
    open_interest=441112.0, 
    last_price=6128.0, 
    last_volume=0, 
    limit_up=6374.0, 
    limit_down=5538.0, 
    open_price=0, 
    high_price=0, 
    low_price=0, 
    pre_close=6128.0, 
    bid_price_1=0, 
    bid_price_2=0, 
    bid_price_3=0, 
    bid_price_4=0, 
    bid_price_5=0, 
    ask_price_1=0, 
    ask_price_2=0, 
    ask_price_3=0, 
    ask_price_4=0, 
    ask_price_5=0, 
    bid_volume_1=0, 
    bid_volume_2=0, 
    bid_volume_3=0, 
    bid_volume_4=0, 
    bid_volume_5=0, 
    ask_volume_1=0, 
    ask_volume_2=0, 
    ask_volume_3=0, 
    ask_volume_4=0, 
    ask_volume_5=0, 
    localtime=None
)

请注意,该tick的时间是:2022-4-12 20:1:18.500000 !!!

很快开始集合竞价,在20:59的时候,策略可能又会收到一个包含开盘价的tick。
1分钟后是21:00,正式进入连续竞价阶段,策略又会收到等多的tick。

为了后面叙述的方便,我们把20:50时收到tick叫tick1,20:59时收到tick叫tick2。

假如CTA策略使用了30分钟K线,那么随着集合竞价结束,在21:00的时候,策略中的BarGeneraor对象,就会为您生成两个莫名其妙的30分钟K线:

  • 20:59时,生成第一个30分钟K线,它只包含一个tick1,
  • 21:00时,生成第二个30分钟K线,它只包含一个tick2,

启动策略不到10分钟时间,就已经虚多了2个30分钟K线。

这是错误的!!!

1.3 这不是偶然现象,它是一定会发生的!

发生这种tick时间戳错误的时机:

  1. vnpy系统启动CTP网关,首次连接行情服务器接口CtpMdApi时
  2. 客户端已经接行过情服务器接口CtpMdApi,客户端因为网络问题断开了连接,再次自动或手动连接情服务器接口CtpMdApi时
  3. 客户端已经接行过情服务器接口CtpMdApi,交易所的行情服务器关闭或者重启,再次自动或手动连接情服务器接口CtpMdApi时

这种tick时间戳错误的表现:

  1. tick时间戳在有效交易时间段外,如可能是23:00、07:28或者07:45。
  2. tick时间戳在有效交易时间段内,如可能是9:08:03。这种情况可能是因为盘中客户端网络原因,再次连接情服务器接口CtpMdApi时造成的。这种情况最麻烦!因为你无法通过时间戳的特征去辨别出其是否为无效的tick,但是该tick有可能已经在断开之前已经收到过了,只是重新连接之后被冠以重连CtpMdApi接口之时的时间戳再次发生给客户端,这会导致我们合成出的1分钟K线的错误。如果1分钟K线无法保证正确,由1分钟K线合成的n分钟、n小时、日K线也是无法保证的。

2. CtaEngine对策略的初始化过程

CTA策略的初始化是由CtaEngine驱动的,其执行逻辑在vnpy_ctastrategy\engine的CtaEngine._init_strategy()中:

    def _init_strategy(self, strategy_name: str):
        """
        Init strategies in queue.
        """
        strategy = self.strategies[strategy_name]

        if strategy.inited:
            self.write_log(f"{strategy_name}已经完成初始化,禁止重复操作")
            return

        self.write_log(f"{strategy_name}开始执行初始化")

        # Call on_init function of strategy
        self.call_strategy_func(strategy, strategy.on_init)

        # Restore strategy data(variables)
        data = self.strategy_data.get(strategy_name, None)
        if data:
            for name in strategy.variables:
                value = data.get(name, None)
                if value is not None:
                    setattr(strategy, name, value)

        # Subscribe market data
        contract = self.main_engine.get_contract(strategy.vt_symbol)
        if contract:
            req = SubscribeRequest(
                symbol=contract.symbol, exchange=contract.exchange)
            self.main_engine.subscribe(req, contract.gateway_name)
        else:
            self.write_log(f"行情订阅失败,找不到合约{strategy.vt_symbol}", strategy)

        # Put event to update init completed status.
        strategy.inited = True
        self.put_strategy_event(strategy)
        self.write_log(f"{strategy_name}初始化完成")

_init_strategy()执行过程是先为策略加载历史数据,再订阅策略交易合约的行情。

3. 怎么解决这些问题?

要想解决问题,就必须问题的根源在哪里?

3.1 多出来的第一个30分钟K线的原因

因为订阅合约行情执行的是CtpMdApi的subscribe():

    def subscribe(self, req: SubscribeRequest) -> None:
        """订阅行情"""
        if self.login_status:
            self.subscribeMarketData(req.symbol)
        self.subscribed.add(req.symbol)

self.subscribeMarketData(req.symbol)只要是首次订阅,接口都会立即从OnRtnDepthMarketData推送1个该合约最新的深度行情通知,而时间是:

    ///最后修改时间
    TThostFtdcTimeType   UpdateTime;
    ///最后修改毫秒
    TThostFtdcMillisecType   UpdateMillisec;

这里的最后修改时间和最后修改毫秒本应该是该合约最后交易的时间,也可能是交易所行情服务器中CTP行情接口重新打开的时间!这就是为什么我们开动tick1的时间是2022-4-12 20:1:18.500000的原因。

3.2 多出来的第二个30分钟K线的原因

TA205.CZCE在每个交易日的集合竞价时段的第4分钟会产生一个集合竞价tick。
你可能会说这个没有毛病,从20:30~21:00,确实是可以生成一个30分钟K线,为什么它不可以只包含一个tick呢?
这么说也过得去,可是问题是咱们在加载其他历史数据的时候,无论我们使用米筐、tushare或者什么其他第三方历史数据时,加载的1分钟K线,从来都没有这样的数据。
或者我们把策略产生的30分钟K线与通达信、大智慧或者文华6等软件生成的30分钟K线比较一下,它们都是没有出现这第二个30分钟K线情况的。从这种种也可以看出来这个tick的处理是不对的,tick2必须归入到21:00~21:30。

3.3 一副K线图表截图

description

从前面所说,无论你使用什么样的BarGenerator来生成K线,离开了合约交易时间段,仅仅用时间特征去合成K线,那么一定会在连接实际行情接口的时候出现上述错误!

如果说vnpy自带的BarGenerator在合成由第三方提供的历史K线还撮合着能够用的话,那也是因为所有的无效数据是由第三方为您过滤和清新掉了,掩盖了BarGenerator的问题而已!!!

3.4 策略在收到tick推送的时候,必须先进行有效性过滤

  • tick时间戳必须在有效时段内,否则做丢弃处理
  • tick时间戳在集合竞价时段内,将它视作其后的第一个连续交易时段的开始时间
  • 对tick时间戳在交易日中间的休市时段内的,将它视作其之前一个连续交易时段的结束时间
  • 对tick时间戳在交易日中间的休市时段内的,将它视作其之前一个连续交易时段的结束时间
    .