文章目录

  • 双均线策略(Double MA)
  • DoubeMA源码分析
    • 1、策略类初始化
    • 2、策略初始化
    • 3、策略启动
    • 4、接收Tick数据
    • 5、处理Bar数据
    • 6、订单以及交易的回调
    • 7、策略结束
  • 流程框图

双均线策略(Double MA)

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

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

DoubeMA源码分析

1、策略类初始化

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

class DoubleMaStrategy(CtaTemplate):author = "用Python的交易员"fast_window = 10slow_window = 20fast_ma0 = 0.0fast_ma1 = 0.0slow_ma0 = 0.0slow_ma1 = 0.0parameters = ["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 priceif not tick.last_price:returnif not self.bar:new_minute = Trueelif 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 = Trueif 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_priceself.bar.open_interest = tick.open_interestself.bar.datetime = tick.datetimeif self.last_tick:volume_change = tick.volume - self.last_tick.volumeself.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.amam.update_bar(bar)if not am.inited:returnfast_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_ma1cross_below = self.fast_ma0 < self.slow_ma0 and self.fast_ma1 > self.slow_ma1if 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.amam.update_bar(bar)if not am.inited:returnfast_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_ma1cross_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."""passdef 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对于策略实现以及执行的流程:

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

  1. vn.py源码解读(一、环境配置与回测初试)

    近来忙于毕业找工作,也不知道能不能继续在量化界混了.周末比较闲,抽空研究了一下vn.py.有人说,为什么学那么多的回测平台呀.其实我个人觉得,做cta的话,两个回测平台还是要的,这样,当你的策略出现和 ...

  2. vn.py源码解读(六、主引擎代码分析---策略模块)

    之前在讲MainEngine的时候,有这样一个代码: me.addApp(ctaStrategy) 这里,我们来看一下MainEngine里面这个addApp函数的代码: def addApp(sel ...

  3. vn.py源码解读(五、主引擎代码分析----CTP模块)

    上一篇文章讲了MainEngine中的初始化函数,重点是DataEngine的讲解.有了对行情数据的处理,还需要有行情数据的来源.在MainEngine的初始化函数后面的一个函数就是addGatewa ...

  4. JDK源码解析之 Java.lang.Double

    Double类是原始类型double的包装类,它包含若干有效处理double值的方法,如将其转换为字符串表示形式,反之亦然.Double类的对象可以包含一个double值. Double类包装原始类型 ...

  5. 熔断器 Hystrix 源码解析 —— 命令执行(三)之执行超时

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: 原创出处 http://www.iocoder.cn/Hystrix/command-execute-third-ti ...

  6. [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (5) 嵌入式hash表

    [源码解析] NVIDIA HugeCTR,GPU版本参数服务器- (5) 嵌入式hash表 文章目录 [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (5) 嵌入式hash表 ...

  7. 0. DRF之软件开发模式CBV源码解析

    文章目录 1. Web应用模式 1.1 动/静态页面 1.2 前后端不分离 1. 3前后端分离 1.4 JSON/XML数据格式 1. json格式 2. xml格式 1.5 服务器页面后缀 2. A ...

  8. Python数据爬取之0基础小白实战(三)源码解析

    前两篇(一)软件安装.(二)初窥门槛我解决了软件版本不匹配的问题并学习关键技术.找到重要源码,完成了程序思路总体设计,本篇废话不多说,我们直接上源码. 任务描述 获取2015-2020年通过申请的国家 ...

  9. [源码解析] 深度学习流水线并行Gpipe(1)---流水线基本实现

    [源码解析] 深度学习流水线并行Gpipe(1)-流水线基本实现 文章目录 [源码解析] 深度学习流水线并行Gpipe(1)---流水线基本实现 0x00 摘要 0x01 概述 1.1 什么是GPip ...

最新文章

  1. GNU Radio的hello world(转)
  2. hdu-1029 Ignatius and the Princess IV
  3. java的图形界面上传附件_Java图形界面(GUI) 动态获取上传或下载文件的路径问题...
  4. D3引擎用正则运算的方式,实现智能设备APP消息推送
  5. 在NumericStepper控件中使用嵌入字体显示数字.
  6. 第一周 从C走进C++ 008 函数缺省参数
  7. 长能耐了?想造反了?你老婆没了.......
  8. python刷题技巧_【python刷题】分治法
  9. 小米:近期发现5件恶意抢注批量申请Redmi商标事件
  10. maven常见问题处理(3-3)Gradle编译时下载依赖失败解决方法
  11. 淘宝TFS文件系统配置
  12. 什么是servlet?servlet有什么用?
  13. 【07月19日】指数估值排名
  14. 手机APP测试注意点
  15. AQS队列到底是什么?
  16. 我对软件应聘学生的建议
  17. MATLAB2018a 64安装
  18. 华硕笔记本k555拆机图解_「华硕k401n」华硕K401笔记本电脑拆机清灰步骤详解 - seo实验室...
  19. 计算机内部线有,电脑主机内部有几根线?分别叫什么?
  20. 绝了!Spring事务是如何传播的?快来收藏!

热门文章

  1. 如何设计一个落地页,才算得上是一个好的落地页?
  2. java jms clust,activeMQ使用总结 (集群方案)
  3. JAVA命名规范(数据库 and 后端)
  4. python卡路里程序_通过步数和体重计算消耗的卡路里的公式是什么?
  5. 我的网站:攻壳机动队,再生侠,吸血莱恩(fan site)
  6. 深海迷航创造模式火箭怎么飞_深海迷航火箭怎么发射说什么没关 | 手游网游页游攻略大全...
  7. WMSYS.WM_CONCAT标识符无效问题
  8. C语言检测数独是否合法,会数独的大佬请进。这是个判断九宫格数独是否正确的程序。...
  9. 液相色谱仪计算机化用户需求,液相色谱仪满足用户的技术要求
  10. AnyBurn 免费专业的 CD/DVD/蓝光刻录和ISO编辑软件