【vn.py】源码解析之双均线(Double Moving Average)策略以及策略底层实现

双均线策略(Double MA)

双均线策略作为最常见最基础的CTA策略,也就是常说的金叉死叉信号组合得到的策略,常用于捕捉一段大趋势。
它的思想很简单,由一个短周期均线和一个长周期均线组成,短周期均线代表近期的走势,长周期均线则是较长时间的走势。当短周期均线从下往上突破长周期均线,也就意味着当前时间段具有上涨趋势,突破点也就是常说的金叉,意味着多头的信号;当长周期均线从上向下突破短周期信号,则意味着当前时间段具有下降趋势,突破点也就是常说的死叉,意味着空头信号。

下面就以vn.py中的Double MA策略源码为例,进行策略代码流程以及实现方式的解析。

DoubeMA源码分析
1、策略类初始化

由于是第一篇关于策略源码的分析,所以首先需要对策略代码的结构有所了解,以后的文章将会侧重于策略逻辑的分析而不是代码结构的解析。 类似于大多数回测框架的结构,vnpy中也是先定义了一个父类用于统一策略基类,其中作为CTA策略的基类CtaTemplate则同样设置了一系列的规范。

class DoubleMaStrategy(CtaTemplate):
    author = "用Python的交易员"

    fast_window = 10
    slow_window = 20

    fast_ma0 = 0.0
    fast_ma1 = 0.0

    slow_ma0 = 0.0
    slow_ma1 = 0.0

    parameters = ["fast_window", "slow_window"]
    variables = ["fast_ma0", "fast_ma1", "slow_ma0", "slow_ma1"]

    def __init__(self, cta_engine, strategy_name, vt_symbol, setting):
        """"""
        super(DoubleMaStrategy, self).__init__(
            cta_engine, strategy_name, vt_symbol, setting
        )

        self.bg = BarGenerator(self.on_bar)
        self.am = ArrayManager()

这里的策略参数都被设置为了类变量,也就是说它们是从属于类,而不是类对象,所以这里需要和成员变量区分开来,在vnpy中要求策略参数和变量需要定义在类中而不是__init()__函数中。为了可视化这些变量和参数,还需要把这些变量和参数的字符串名称分别添加到variables和parameters列表中。

在构造函数中,需要传递 cta_engine, strategy_name, vt_symbol, setting参数,也就是CTA引擎、策略名称、标的代码、设置信息。其中CTA引擎可以是实盘或者是回测引擎,分别用于实盘或者回测,这样就可以很方便的实现一套代码跑回测和实盘了。

在构造函数中,还创建了一个BarGenerator构造器并绑定了on_bar()回调函数,用于将tick级别数据合成分钟级别或者更大级别的Bar数据,以应对不同Bar级别的策略要求。除此之外,ArrayManager用于储存时间序列数据并在底部利用talib包来计算指标,默认的大小是100,其储存数据可以理解为deque的作用。

2、策略初始化

这里的策略初始化不同于上面的策略类的初始化,在VN Station中设置好参数,并添加CTA策略时,实际上是完成了策略类的初始化,然后点击策略初始化时,实际上是调用了其中的on_init()函数。

    def on_init(self):
        """
        Callback when strategy is inited.
        """
        self.write_log("策略初始化")
        self.load_bar(10)

在调用这个初始化策略的函数后,VN Station界面的日志中就会出现“策略初始化“。之后调用父类的load_bar()函数用于初始化策略变量,例如我们的双均线策略就需要之前至少20个窗口的数据来计算当前的MA指标。

加载bar数据参数默认是10,也就是加载10天的数据,数据的周期是1分钟级别。

    def load_bar(
        self,
        days: int,
        interval: Interval = Interval.MINUTE,
        callback: Callable = None,
    ):
        """
        Load historical bar data for initializing strategy.
        """
        if not callback:
            callback = self.on_bar       #设置回调函数

        self.cta_engine.load_bar(self.vt_symbol, days, interval, callback)

然后通过类初始化传递的cta引擎参数进行调用加载bar数据。从代码中也可以看出vnpy加载历史数据的方式有两种,一是默认通过rqdata API进行获取,前提是需要配置好rqdata的相关配置信息;二是在本地数据库中进行查找,也就是默认的.vntrader文件夹下的sqlite数据库。

    def load_bar(
        self, 
        vt_symbol: str, 
        days: int, 
        interval: Interval,
        callback: Callable[[BarData], None]
    ):
        """"""
        symbol, exchange = extract_vt_symbol(vt_symbol)
        end = datetime.now()
        start = end - timedelta(days)

        # Query bars from RQData by default, if not found, load from database.
        bars = self.query_bar_from_rq(symbol, exchange, interval, start, end)
        if not bars:
            bars = database_manager.load_bar_data(
                symbol=symbol,
                exchange=exchange,
                interval=interval,
                start=start,
                end=end,
            )

        for bar in bars:
            callback(bar)

从代码中可以看出,通过datetime模块获取当前时间作为end,然后减去10天的时间作为start进行查询。将得到的所有bar数据通过第一步load_bar()中设定的回调函数on_bar()进行调用,这样就在策略中触发了on_bar()函数。

3、策略启动

还是回想在VN Station中的操作,在初始化策略变量之后,需要启动策略,所以在点击启动策略后,实际上就是调用了下面的on_start()函数,界面的日志栏中就会出现策略启动的输出。其中put_event()函数的作用是通知图形界面更新,如果不调用该函数则界面不会变化。

    def on_start(self):
        """
        Callback when strategy is started.
        """
        self.write_log("策略启动")
        self.put_event()
4、接收Tick数据

在开始实盘后,CTP会不断推送Tick数据到我们策略中,处理Tick数据的函数是:

    def on_tick(self, tick: TickData):
        """
        Callback of new tick data update.
        """
        self.bg.update_tick(tick)

在接收到Tick数据后,全局变量bg调用update_tick用于生成Bar数据:

    def update_tick(self, tick: TickData):
        """
        Update new tick data into generator.
        """
        new_minute = 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:
            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)
            self.bar.low_price = min(self.bar.low_price, tick.last_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 = tick.volume - self.last_tick.volume
            self.bar.volume += max(volume_change, 0)

        self.last_tick = tick

其内部主要是通过判断当前的Tick数据与之前的Tick数据是否是属于同一分钟级别来决定是否有新的Bar生成,否则就会继续进行迭代来更新当前Bar的信息,也就是说只有当T+1分钟的Tick接收到了之后,T分钟的Bar数据才会生成。 由于在创建bg对象的时候,on_bar()作为回调函数传递了进去,所以在当新的Bar数据生成后,就会通过on_bar()函数进行回调。

5、处理Bar数据

在每个策略中最至关重要的就是策略的核心部分。如果策略是在Bar内部进行实现的,如海龟策略或者一些突破策略,则需要在on_tick()函数中进行实现,由于我们的Doube MA策略是以Bar进行驱动的,所以策略主要代码就是在on_bar()函数中进行实现:

       def on_bar(self, bar: BarData):
        """
        Callback of new bar data update.
        """

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

        fast_ma = am.sma(self.fast_window, array=True)
        self.fast_ma0 = fast_ma[-1]
        self.fast_ma1 = fast_ma[-2]

        slow_ma = am.sma(self.slow_window, array=True)
        self.slow_ma0 = slow_ma[-1]
        self.slow_ma1 = slow_ma[-2]

        cross_over = self.fast_ma0 > self.slow_ma0 and self.fast_ma1 < self.slow_ma1
        cross_below = self.fast_ma0 < self.slow_ma0 and self.fast_ma1 > self.slow_ma1

        if cross_over:
            if self.pos == 0:
                self.buy(bar.close_price, 1)
            elif self.pos < 0:
                self.cover(bar.close_price, 1)
                self.buy(bar.close_price, 1)

        elif cross_below:
            if self.pos == 0:
                self.short(bar.close_price, 1)
            elif self.pos > 0:
                self.sell(bar.close_price, 1)
                self.short(bar.close_price, 1)

        self.put_event()

首先,我们第一次调用on_bar()函数是在策略初始化时,通过load_bar()函数进行调用的,目的是为了初始化策略的变量。第二次在调用时,则是在on_tick()函数中,bg对象接收足够的tick数据生成一个Bar数据后进行调用,此时是在实盘的情景中。

在接收到Bar数据后,会将这个Bar数据放入ArrayManager()容器中进行更新,如果am这个实例化对象没有被初始化,也就是通过回测得到的数据没有达到100个,那么就会直接return,一般在加载了10天的分钟数据后就会直接初始化成功。这里还需要注意,如果没有通过历史数据进行初始化,那么am必然是没有初始化成功的,所以在实盘中就会延迟100个单位的Bar数据来填充am容器,直到am容器数据足够初始化成功后,才会执行后面的逻辑代码。

之后调用ma底部的talib库用于计算最新窗口内的技术指标,对应代码中也就是10窗口的MA和20窗口的MA指标,注意这里的am.sma()实际上是对talib中的SMA的进一步封装,计算的实际上是Bar数据的收盘价的MA指标:

        am = self.am
        am.update_bar(bar)
        if not am.inited:
            return
        fast_ma = am.sma(self.fast_window, array=True)
        self.fast_ma0 = fast_ma[-1]
        self.fast_ma1 = fast_ma[-2]

        slow_ma = am.sma(self.slow_window, array=True)
        self.slow_ma0 = slow_ma[-1]
        self.slow_ma1 = slow_ma[-2]
           

然后通过判断是否出现金叉死叉来决定是否触发交易逻辑:

        cross_over = self.fast_ma0 > self.slow_ma0 and self.fast_ma1 < self.slow_ma1
        cross_below = self.fast_ma0 < self.slow_ma0 and self.fast_ma1 > self.slow_ma1

如果出现了金叉,并且没有持仓则直接买入开仓;或者是持有空头,则先平仓再买入开仓。
如果出现了死叉,并且没有持仓则直接卖出开仓;或者是持有多头,则先平仓再卖出开仓。
交易通过父类中的函数来实现,最后再将产生的日志信息推送到界面中。注意这里并没有对交易进行撮合,也就是发出的交易指令只是收盘价,所以在实盘或者回测中需要对价格设置滑点。

另外,关于这里的self.pos的改变额外说明一下。我在看源码的时候,在CTATemplate中没有找到关于self.pos的改变,按照正常逻辑,仓位改变应该是交易撮合成功后发生的,所以应该是在on_trade回调时,确认交易成功后才改变。所以按照这个逻辑,在engine文件中找到了,并且跟想象的一样,是在一个process_trade_event()函数中,在获取到交易成功订单后对pos进行了改变,然后是策略on_trade()函数进行回调。

        if cross_over:
            if self.pos == 0:
                self.buy(bar.close_price, 1)
            elif self.pos < 0:
                self.cover(bar.close_price, 1)
                self.buy(bar.close_price, 1)

        elif cross_below:
            if self.pos == 0:
                self.short(bar.close_price, 1)
            elif self.pos > 0:
                self.sell(bar.close_price, 1)
                self.short(bar.close_price, 1)

        self.put_event()
6、订单以及交易的回调

其余剩下的三个on开头的函数顾名思义也可以知道分别用于发生委托单、交易成交以及停止单的回调函数。注意on_order是向交易所发出订单之后得到的回调,on_trade是订单撮合成功之后的回调,所以说如果进行了撤单,那么只会有on_order的回调而不会有on_trade的回调。

    def on_order(self, order: OrderData):
        """
        Callback of new order data update.
        """
        pass

    def on_trade(self, trade: TradeData):
        """
        Callback of new trade data update.
        """
        self.put_event()

    def on_stop_order(self, stop_order: StopOrder):
        """
        Callback of stop order update.
        """
        pass
7、策略结束

最后就是策略停止时,也就是在VN Station中停止策略的触发函数:

    def on_stop(self):
        """
        Callback when strategy is stopped.
        """
        self.write_log("策略停止")

        self.put_event()
流程框图

最后,用我制作的这个思维导图,以Double Moving Average策略为例来梳理一下vnpy对于策略实现以及执行的流程:

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值