当前位置: 首页 > 工具软件 > VN.PY > 使用案例 >

【vn.py】量化策略开发

赵驰
2023-12-01

写在前面

一个完整的量化流程至少需要包括策略开发、历史回测与实盘交易三个步骤,下面就以vn.py为例,先整理一下vn.py中如何进行量化策略的开发,本文的主要内容还是多来自vn.py的官方教程

开发环境

在vn.py的官方教程中使用的IDE是VS Code,这里的IDE没有什么特殊要求,我在使用的时候使用的环境是Pycharm+VN Studio。相比于以前自己在源码基础上的策略开发,以现在这种的开发方式可以更好地专注于策略本身。

为了方便管理自己的策略代码,需要创建一个strategies的文件夹存放策略代码,这个文件夹的目录位置需要:
如果是按照官方默认配置的话,也就是.vntrader在C:/Users/YourName/下,strategies放在.vntrader的同级目录下即可。
如果把.vntrader放在了其他位置,也需要在它的同级目录下创建strategies文件夹。
因为在启动VN Trader的时候,它会在.vntrader文件在所在的目录下查找strategies文件夹,并加载其中的策略代码。

之后在strategies文件夹中创建一个命名为demo_strategy.py的文件,并用IDE打开。

策略代码编写

策略这里选用vn.py官方教程中的双均线策略demo,它的完整代码:

from vnpy.app.cta_strategy import (
    CtaTemplate,
    StopOrder,
    TickData,
    BarData,
    TradeData,
    OrderData,
    BarGenerator,
    ArrayManager,
)


class DemoStrategy(CtaTemplate):
    """演示用的简单双均线"""

    # 策略作者
    author = "Smart Trader"

    # 定义参数
    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().__init__(cta_engine, strategy_name, vt_symbol, setting)

        # K线合成器:从Tick合成分钟K线用
        self.bg = BarGenerator(self.on_bar)

        # 时间序列容器:计算技术指标用
        self.am = ArrayManager()

    def on_init(self):
        """
        当策略被初始化时调用该函数。
        """
        # 输出个日志信息,下同
        self.write_log("策略初始化")

        # 加载10天的历史数据用于初始化回放
        self.load_bar(10)

    def on_start(self):
        """
        当策略被启动时调用该函数。
        """
        self.write_log("策略启动")

        # 通知图形界面更新(策略最新状态)
        # 不调用该函数则界面不会变化
        self.put_event()

    def on_stop(self):
        """
        当策略被停止时调用该函数。
        """
        self.write_log("策略停止")

        self.put_event()

    def on_tick(self, tick: TickData):
        """
        通过该函数收到Tick推送。
        """
        self.bg.update_tick(tick)

    def on_bar(self, bar: BarData):
        """
        通过该函数收到新的1分钟K线推送。
        """
        am = self.am

        # 更新K线到时间序列容器中
        am.update_bar(bar)

        # 若缓存的K线数量尚不够计算技术指标,则直接返回
        if not am.inited:
            return

        # 计算快速均线
        fast_ma = am.sma(self.fast_window, array=True)
        self.fast_ma0 = fast_ma[-1]     # T时刻数值
        self.fast_ma1 = fast_ma[-2]     # T-1时刻数值

        # 计算慢速均线
        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:
            # 为了保证成交,在K线收盘价上加5发出限价单
            price = bar.close_price + 5

            # 当前无仓位,则直接开多
            if self.pos == 0:
                self.buy(price, 1)
            # 当前持有空头仓位,则先平空,再开多
            elif self.pos < 0:
                self.cover(price, 1)
                self.buy(price, 1)

        # 如果发生了死叉
        elif cross_below:
            price = bar.close_price - 5

            # 当前无仓位,则直接开空
            if self.pos == 0:
                self.short(price, 1)
            # 当前持有空头仓位,则先平多,再开空
            elif self.pos > 0:
                self.sell(price, 1)
                self.short(price, 1)

        self.put_event()

    def on_order(self, order: OrderData):
        """
        通过该函数收到委托状态更新推送。
        """
        pass

    def on_trade(self, trade: TradeData):
        """
        通过该函数收到成交推送。
        """
        # 成交后策略逻辑仓位发生变化,需要通知界面更新。
        self.put_event()

    def on_stop_order(self, stop_order: StopOrder):
        """
        通过该函数收到本地停止单推送。
        """
        pass

下面是每一部分的讲解:

参数设置

最前面的import导入的也就是我们需要用到vnpy框架中依赖的类和函数,其中最重要的就是CtaTemplate这个类,它是所有CTA策略的基类,其中包括了很多接口,包括一系列以on_开头的回调函数,用于接受事件推送,以及其他主动函数用于执行操作(委托、撤单、记录日志等)。所有CTA策略都需要继承这个父类。

from vnpy.app.cta_strategy import (
    CtaTemplate,
    StopOrder,
    TickData,
    BarData,
    TradeData,
    OrderData,
    BarGenerator,
    ArrayManager,
)

创建了这个策略之后,一个策略必不可少的还有参数,所以下一步我们需要定义策略所需要的参数,并且在定义完之后,还需要将参数的名称以字符串的形式添加到parameters列表之中,方便后续窗口界面的参数初始化设置以及后续参数优化。

    # 定义参数
    fast_window = 10
    slow_window = 20

    # 添加参数和变量名到对应的列表
    parameters = ["fast_window", "slow_window"]

除了参数之外,还有一些在策略的执行过程中的变量,为了可视化这些变量还需要把这些变量的字符串名称添加到variables列表中,同时在保存策略运行状态到缓存文件中时将这些变量写入进去(实盘中每天关闭策略时会自动缓存)。由于这些变量需要历史数据进行初始化,所以在定义这些变量时,如果没有特殊要求以默认数值定义即可。

    # 定义变量
    fast_ma0 = 0.0
    fast_ma1 = 0.0
    slow_ma0 = 0.0
    slow_ma1 = 0.0

    # 添加参数和变量名到对应的列表
    variables = ["fast_ma0", "fast_ma1", "slow_ma0", "slow_ma1"]

上面变量和参数都是全局变量,便于全局调用。变量和参数的区别在于变量是一直在策略的更新过程中变化的,而参数则是经过调优之后策略的运行过程中自始至终不会变化。

需要注意的是:
1、无论变量还是参数,都必须定义在策略类中,而非策略类的__init__函数中;
2、参数和变量,均只支持Python中的四种基础数据类型:str、int、float、bool,使用其他类型会导致各种出错(尤其注意不要用list、dict等容器);
3、如果在策略逻辑中,确实需要使用list、dict之类的容器用于数据缓存,请在__init__函数中创建这些容器。

交易逻辑

首先,在创建策略实例化时,__init__函数首先被调用:

        # K线合成器:从Tick合成分钟K线用
        self.bg = BarGenerator(self.on_bar)

        # 时间序列容器:计算技术指标用
        self.am = ArrayManager()

BarGenerator的作用是用于合成K线,由于在实际交易中,CTP推送的数据并不是以K线的形式进行推送的,而我们的策略需要以K线为基础进行交易逻辑,所以就需要在本地将接受的tick数据合成K线。其中传入的参数(self.on_bar)是当1分钟K线走完时触发的回调函数。
在实盘策略收到最新的Tick推送时,我们只需要将Tick数据通过on_tick函数更新到BarGenerator中:

def on_tick(self, tick: TickData):
    """
    通过该函数收到Tick推送。
    """
    self.bg.update_tick(tick)

当BarGenerator发现某根K线走完时,会将过去1分钟内的Tick数据合成的1分钟K线推送给策略,自动调用策略的最关键的on_bar函数,执行交易逻辑。

在vn.py的CTA策略模块中,所有的策略逻辑都是由事件来驱动的。对于事件,举例来说:

  • 策略被执行初始化操作,会收到on_init函数的调用,此时可以加载历史数据,来进行技术指标的初始化运算
  • 新一根1分钟K线走完时,会收到on_bar函数的调用,参数为该根K线对象的BarData
  • 策略发出的委托,状态发生变化时,会收到on_order函数的调用,参数为该委托的最新状态OrderData

对于最简单的双均线策略的DemoStrategy来说,我们不用关注委托状态变化和成交推送之类的细节,只需要在收到K线推送时(on_bar函数中)执行交易相关的逻辑判断即可。

每次新的一根K线走完时,策略会通过on_bar函数收到该根K线的数据推送。注意此时收到的数据只有该K线,但大部分技术指标计算时都需要过去N个周期的历史数据。

ArrayManager的作用适用于计算均线技术指标,用于实现了K线历史的缓存和技术指标计算。在on_bar函数的逻辑中,第一步需要将K线对象推送到该时间序列容器中:

# 纯粹为了后续可以少写一些self.
am = self.am

# 更新K线到时间序列容器中
am.update_bar(bar)

# 若缓存的K线数量尚不够计算技术指标,则直接返回
if not am.inited:
    return

为了满足技术指标计算的需求,我们通常需要最少N根K线的缓存(N默认为100),在推送进ArrayManager对象的数据不足N之前,是无法计算出需要的技术指标的,对于缓存数据是否已经足够的判断,通过am.inited变量可以很方便的判断,在inited变为True之前,都应该只是缓存数据而不进行任何其他操作。

当缓存的数据量满足需求后,我们可以很方便的通过am.sma函数来计算均线指标的数值:

# 计算快速均线
fast_ma = am.sma(self.fast_window, array=True)
self.fast_ma0 = fast_ma[-1]     # T时刻数值
self.fast_ma1 = fast_ma[-2]     # T-1时刻数值

# 计算慢速均线
slow_ma = am.sma(self.slow_window, array=True)
self.slow_ma0 = slow_ma[-1]
self.slow_ma1 = slow_ma[-2]

注意这里我们传入了可选参数array=True,因此返回的fast_ma为最新移动平均线的数组,其中最新一个周期(T时刻)的移动均线ma数值可以通过-1下标获取,上一个周期(T-1时刻)的ma数值可以通过-2下标获取。

有了快慢两根均线在T时刻和T-1时刻的数值后,我们就可以进行双均线策略的核心逻辑判断,即是否发生了均线金叉或者死叉:

# 判断是否金叉
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)

所谓的均线金叉,是指T-1时刻的快速均线fast_ma1低于慢速均线slow_ma1,而T时刻时快速均线fast_ma0大于或等于慢速均线slow_ma10,实现了上穿的行为(即金叉)。均线死叉则是相反的情形。

当金叉或者死叉发生后,则需要执行相应的交易操作:

# 如果发生了金叉
if cross_over:
    # 为了保证成交,在K线收盘价上加5发出限价单
    price = bar.close_price + 5

    # 当前无仓位,则直接开多
    if self.pos == 0:
        self.buy(price, 1)
    # 当前持有空头仓位,则先平空,再开多
    elif self.pos < 0:
        self.cover(price, 1)
        self.buy(price, 1)

# 如果发生了死叉
elif cross_below:
    price = bar.close_price - 5

    # 当前无仓位,则直接开空
    if self.pos == 0:
        self.short(price, 1)
    # 当前持有空头仓位,则先平多,再开空
    elif self.pos > 0:
        self.sell(price, 1)
        self.short(price, 1)

对于简单双均线策略来说,用于处于持仓的状态中,金叉后拿多仓,死叉后拿空仓。

所以当金叉发生时,我们需要检查当前持仓的情况。如果没有持仓(self.pos == 0),说明此时策略刚开始交易,则应该直接执行多头开仓操作(buy)。如果此时已经持有空头仓位(self.pos , 0),则应该先执行空头平仓操作(cover)然后同时立即执行多头开仓操作(buy)。为了保证成交(简化策略),我们在下单时选择了加价的方式来实现(多头+5,空头-5)。

注意尽管这里我们选择使用双均线策略来做演示,但在实践经验中简单均线类的策略效果往往非常差,千万不要拿来跑实盘,也不建议在此基础上进行扩展开发。

REF

vn.py官方教程

 类似资料: