最后
不知道你们用的什么环境,我一般都是用的Python3.6环境和pycharm解释器,没有软件,或者没有资料,没人解答问题,都可以免费领取(包括今天的代码),过几天我还会做个视频教程出来,有需要也可以领取~
给大家准备的学习资料包括但不限于:
Python 环境、pycharm编辑器/永久激活/翻译插件
python 零基础视频教程
Python 界面开发实战教程
Python 爬虫实战教程
Python 数据分析实战教程
python 游戏开发实战教程
Python 电子书100本
Python 学习路线规划
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
在每次测试中,我们都通过遍历测试用数据的方式来模拟实盘中策略不断收到新数据推送的情况(同样适用于事件驱动的回测模式),将计算出的移动平均值不断保存到一个列表list中作为最终结果返回。
测试用电脑的配置情况:Core i7-6700K 4.0G/16G/Windows 7。
第一步我们以最简单、最原始的方式来计算移动平均值:
# 计算500期的移动均线,并将结果保存到一个列表里返回
def ma_basic(data, ma_length):
# 用于保存均线输出结果的列表
ma = []
# 计算均线用的数据窗口
data_window = data[:ma_length]
# 测试用数据(去除了之前初始化用的部分)
test_data = data[ma_length:]
# 模拟实盘不断收到新数据推送的情景,遍历历史数据计算均线
for new_tick in test_data:
# 移除最老的数据点并增加最新的数据点
data_window.pop(0)
data_window.append(new_tick)
# 遍历求均线
sum_tick = 0
for tick in data_window:
sum_tick += tick
ma.append(sum_tick/ma_length)
# 返回数据
return ma
# 运行测试
start = time.time()
for i in range(test_times):
result = ma_basic(data, ma_length)
time_per_test = (time.time()-start)/test_times
time_per_point = time_per_test/(data_length - ma_length)
print u'单次耗时:%s秒' %time_per_test
print u'单个数据点耗时:%s微秒' %(time_per_point*1000000)
print u'最后10个移动平均值:', result[-10:]
单次耗时指的是遍历完整个测试数据计算移动平均值所需的时间,单个数据点耗时指的是遍历过程中每个数据点的平均计算耗时,最后10个移动平均值用于和后续的算法进行比对,保证计算结果的正确性。
ma_basic测试结果
- 单次耗时:1.15699999332秒
- 单个数据点耗时:11.6281406364微秒
大约10万个数据点(说大约因为有500个用于初始化了),这个测试结果不能说很好但也还过得去。考虑到一个简单的双均线CTA策略(Double SMA Strategy),每个数据点来了后会进行两次均线计算,通常均线窗口不会超过500,且比较两根均线交叉情况的算法开销更低,估计策略单纯在信号计算方面的耗时会在30微秒以内,对于一个通常跑在1分钟线甚至更高时间周期上的策略而言已经是绰绰有余。
有了起点,下面来试着一步步提升性能。
试试NumPy?
用Python做数值运算性能不够的时候,很多人的第一反应就是上NumPy:之前的ma_basic里,比较慢的地方应该在每一个新的数据点加入到data_window中后遍历求平均值的代码,那么改用numpy.array数组来求和应该性能就会有所提升了吧?
# 改用numpy(首先是一种常见的错误用法)
import numpy as np
def ma_numpy_wrong(data, ma_length):
ma = []
data_window = data[:ma_length]
test_data = data[ma_length:]
for new_tick in test_data:
data_window.pop(0)
data_window.append(new_tick)
# 使用numpy求均线,注意这里本质上每次循环
# 都在创建一个新的numpy数组对象,开销很大
data_array = np.array(data_window)
ma.append(data_array.mean())
return ma
ma_numpy_wrong测试结果
- 单次耗时:2.11879999638秒
- 单个数据点耗时:21.2944723254微秒
WTF?!用NumPy后居然反而速度降低了一半(耗时增加到了快2倍)!
这里的写法是一个非常常见的NumPy错误用法,问题就出在:
data_array = np.array(data_window)
由于NumPy中的对象大多实现得比较复杂(提供了丰富的功能),所以其对象创建和销毁的开销都非常大。上面的这句代码意味着在计算每一个新数据点时,都要创建一个新的array对象,并且仅使用一次后就会销毁,使用array.mean方法求均值带来的性能提升还比不上array对象创建和销毁带来的额外开销。
正确的用法是把np.array作为data_window时间序列的容器,每计算一个新的数据点时,使用底层数据偏移来实现数据更新:
# numpy的正确用法
def ma_numpy_right(data, ma_length):
ma = []
# 用numpy数组来缓存计算窗口内的数据
data_window = np.array(data[:ma_length])
test_data = data[ma_length:]
for new_tick in test_data:
# 使用numpy数组的底层数据偏移来实现数据更新
data_window[0:ma_length-1] = data_window[1:ma_length]
data_window[-1] = new_tick
ma.append(data_window.mean())
return ma
ma_numpy_right测试结果
- 单次耗时:0.614300012589秒
- 单个数据点耗时:6.17386947325微秒
速度比ma_basic提高了大约2倍,看来NumPy也就这么回事了。
JIT神器:Numba
关心过Python性能的朋友应该都听过PyPy的大名,通过重新设计的Python解释器,PyPy内建的JIT技术号称可以将Python程序的速度提高几十倍(相比于CPython),可惜由于兼容性的问题并不适合于量化策略开发这一领域。
幸运的是,我们还有Anaconda公司推出的Numba。Numba允许用户使用基于LLVM的JIT技术,对程序内想要提高性能的部分(函数)进行局部优化。同时Numba在设计理念上更加务实:可以直接在CPython中使用,和其他常用的Python模块的兼容性良好,并且最爽的是使用方法傻瓜到了极点:
# 使用numba加速,ma_numba函数和ma_basic完全一样
import numba
@numba.jit
def ma_numba(data, ma_length):
ma = []
data_window = data[:ma_length]
test_data = data[ma_length:]
for new_tick in test_data:
data_window.pop(0)
data_window.append(new_tick)
sum_tick = 0
for tick in data_window:
sum_tick += tick
ma.append(sum_tick/ma_length)
return ma
ma_numba测试结果
- 单次耗时:0.043700003624秒
- 单个数据点耗时:0.439196016321微秒
OMG!就加了一行@numba.jit,性能竟然提高了26倍!这估计是按照代码修改行数算,性价比最高的优化方案了。
改写算法
从编程哲学的角度来看,想提高计算机程序的速度,一个最基本的原则就是降低算法复杂度。看到这里估计早就有量化老手ma_basic不爽了,弄个复杂度O(N)的算法来算平均值,就不能缓存下求和的结果,把复杂度降低到O(1)么?
# 将均线计算改写为高速算法
def ma_online(data, ma_length):
ma = []
data_window = data[:ma_length]
test_data = data[ma_length:]
# 缓存的窗口内数据求和结果
sum_buffer = 0
for new_tick in test_data:
old_tick = data_window.pop(0)
data_window.append(new_tick)
# 如果缓存结果为空,则先通过遍历求第一次结果
if not sum_buffer:
sum_tick = 0
for tick in data_window:
sum_tick += tick
ma.append(sum_tick/ma_length)
# 将求和结果缓存下来
sum_buffer = sum_tick
else:
# 这里的算法将计算复杂度从O(n)降低到了O(1)
sum_buffer = sum_buffer - old_tick + new_tick
ma.append(sum_buffer/ma_length)
return ma
ma_online测试结果
- 单次耗时:0.0348000049591秒
- 单个数据点耗时:0.349748793559微秒
哲学果然才是最强大的力量!!!
(索罗斯:其实我是个哲学家。)
改写算法后的ma_online无需JIT就超越了ma_numba,将性能提高到了33倍(对比ma_basic),如果再把numba加上会如何?
# 高速算法和numba结合,ma_online_numba函数和ma_online完全一样
@numba.jit
def ma_online_numba(data, ma_length):
ma = []
data_window = data[:ma_length]
test_data = data[ma_length:]
sum_buffer = 0
for new_tick in test_data:
old_tick = data_window.pop(0)
data_window.append(new_tick)
if not sum_buffer:
sum_tick = 0
for tick in data_window:
sum_tick += tick
ma.append(sum_tick/ma_length)
sum_buffer = sum_tick
else:
sum_buffer = sum_buffer - old_tick + new_tick
ma.append(sum_buffer/ma_length)
return ma
ma_online_numba测试结果
- 单次耗时:0.0290000200272秒
- 单个数据点耗时:0.29145748771微秒
尽管性能进一步提升了到了40倍,不过相比较于ma_numba对比ma_basic的提升没有那么明显,果然哲学的力量还是太强大了。
终极武器:Cython
到目前为止使用纯Python环境下的优化方法我们已经接近了极限,想要再进一步就得发挥Python胶水语言的特性了:使用其他扩展语言。由于CPython虚拟机的开发语言是C,因此在性能提升方面的扩展语言主要选择就是C/C++,相关的工具包括ctypes、cffi、Swig、Boost.Python等,尽管功能十分强大,不过以上工具都无一例外的需要用户拥有C/C++语言相关的编程能力,对于很多Python用户而言是个比较麻烦的事。
好在Python社区对于偷懒的追求是永无止境的,Cython这一终极武器应运而生。关于Cython的详细介绍可以去官网看,简单来它的主要作用就是允许用户以非常接近Python的语法来实现非常接近C的性能。
先来试试最简单的方法:完全不修改任何代码,只是把函数放到.pyx文件里,调用Cython编译成.pyd扩展模块。
# 基础的cython加速
def ma_cython(data, ma_length):
ma = []
data_window = data[:ma_length]
test_data = data[ma_length:]
for new_tick in test_data:
data_window.pop(0)
data_window.append(new_tick)
sum_tick = 0
for tick in data_window:
sum_tick += tick
ma.append(sum_tick/ma_length)
return ma
最后
🍅 硬核资料:关注即可领取PPT模板、简历模板、行业经典书籍PDF。
🍅 技术互助:技术群大佬指点迷津,你的问题可能不是问题,求资源在群里喊一声。
🍅 面试题库:由技术群里的小伙伴们共同投稿,热乎的大厂面试真题,持续更新中。
🍅 知识体系:含编程语言、算法、大数据生态圈组件(Mysql、Hive、Spark、Flink)、数据仓库、Python、前端等等。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!