一个完整的量化流程至少需要包括策略开发、历史回测与实盘交易三个步骤,下面就以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策略模块中,所有的策略逻辑都是由事件来驱动的。对于事件,举例来说:
对于最简单的双均线策略的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)。
注意尽管这里我们选择使用双均线策略来做演示,但在实践经验中简单均线类的策略效果往往非常差,千万不要拿来跑实盘,也不建议在此基础上进行扩展开发。