性能提升30倍丨基于 DolphinDB 的 mytt 指标库实现

983 阅读8分钟

MyTT 是一个简单易用的 Python 库,它将通达信、同花顺、文华麦语言等指标公式最简化移植到了 Python 中,实现的常见指标包括 MACD、RSI、BOLL、ATR、KDJ、CCI、PSY 等。MyTT 全部基于 numpy 和 pandas 的函数进行封装。

为了方便用户在 DolphinDB 中计算这些技术指标,我们使用 DolphinDB 脚本实现了 MyTT 中包含的指标函数,并封装在 DolphinDB mytt module (mytt.dos)中。 相比于 Python 中的 MyTT 库,DolphinDB mytt module 中的计算函数不仅在批处理中性能有大幅提升,而且支持 DolphinDB 的流式增量计算引擎,可以直接用于实时流计算场景。

因为 DolphinDB mytt module 是基于 DolphinDB V1.30.18 和 DolphinDB V2.00.6 开发的,所以建议用户使用 DolphinDB V1.30.18 和 DolphinDB V2.00.6 及以上版本运行 mytt 指标库中的函数。

1. 函数及参数的命名与用法规范

  • Python MyTT 库中所有函数名大写,所有参数名大写,为适应使用者的使用习惯,DolphinDB mytt module 中的函数名、参数、参数默认值均与 MyTT 保持一致。
  • 为得到有意义的计算结果,mytt 中函数的参数表示时间跨度的参数均要求至少是 2。
  • 由于 LAST 函数与 DolphinDB 中内置关键字冲突,mytt 中将此函数命名为 LAST_。

2. 环境配置

把附件的 mytt.dos 放在节点的 [home]/modules 目录下,[home] 目录由系统配置参数 home 决定,可以通过 getHomeDir() 函数查看。初次使用模块文件时,[home]目录没有 modules 目录,手动创建 modules 目录,然后把 mytt.dos 模块文件放入该目录即可。

DolphinDB 模块使用的教程文档:DolphinDB 教程:模块

3. 使用范例

3.1 脚本中直接使用指标函数

对一个向量直接使用 mytt 模块中的 EMA 函数(指数平滑法)进行计算:

// 如果未设置自动加载 mytt module,新会话需要手动加载一次 mytt module
use mytt

close = 7.2 6.97 7.08 6.74 6.49 5.9 6.26 5.9 5.35 5.63
x = EMA(close, 5)

3.2 在 SQL 语句中分组使用

用户经常需要在数据表中对多组数据在每组内进行计算。在以下例子中,先构造了一个包含 2 个股票的数据表:

use mytt

close = 7.2 6.97 7.08 6.74 6.49 5.9 6.26 5.9 5.35 5.63 3.81 3.935 4.04 3.74 3.7 3.33 3.64 3.31 2.69 2.72
date = (2020.03.02 + 0..4 join 7..11).take(20)
symbol = take(`F,10) join take(`GPRO,10)
t = table(symbol, date, close)

对其中每只股票使用 mytt 模块中的 EMA 函数进行计算:

update t set EMA = EMA(close, 5) context by symbol

3.3 返回多个列的结果

某些函数会返回多个列的结果,例如函数 BIAS(乘离率指标)。

直接使用的例子:

use mytt

close = 7.2 6.97 7.08 6.74 6.49 5.9 6.26 5.9 5.35 5.63
bias1, bias2, bias3 = BIAS(close, L1 = 2, L2 = 4, L3 = 6)

在 SQL 语句中使用的例子:

use mytt

close = 7.2 6.97 7.08 6.74 6.49 5.9 6.26 5.9 5.35 5.63 3.81 3.935 4.04 3.74 3.7 3.33 3.64 3.31 2.69 2.72
date = (2020.03.02 + 0..4 join 7..11).take(20)
symbol = take(`F,10) join take(`GPRO,10)
t = table(symbol, date, close)
select *, BIAS(close, L1 = 2, L2 = 4, L3 = 6) as `bias1`bias2`bias3 from t context by symbol

symbol date       close  bias1    bias2    bias3
------ ---------- ----- -------- -------- --------
F      2020.03.02 7.2
F      2020.03.03 6.97 -1.623
F      2020.03.04 7.08  0.783
F      2020.03.05 6.74 -2.46     -3.68
F      2020.03.06 6.49 -1.89     -4.839
F      2020.03.09 5.9  -4.762    -9.958   -12.333
F      2020.03.10 6.26  2.961    -1.378   -4.767
F      2020.03.11 5.9  -2.961    -3.87    -7.74
F      2020.03.12 5.35 -4.889    -8.586   -12.391
F      2020.03.13 5.63  2.55     -2.679   -4.925
GPRO   2020.03.02 3.81
GPRO   2020.03.03 3.935 1.614
GPRO   2020.03.04 4.04  1.317
GPRO   2020.03.05 3.74 -3.856    -3.639
GPRO   2020.03.06 3.7  -0.538    -3.99
GPRO   2020.03.09 3.33 -5.263    -10.061 -11.417
GPRO   2020.03.10 3.64  4.448     1.041  -2.435
GPRO   2020.03.11 3.31 -4.748    -5.293  -8.732
GPRO   2020.03.12 2.69 -10.333   -17.039 -20.921
GPRO   2020.03.13 2.72  0.555    -11.974 -15.833

4. 函数计算性能

本节将以 AVEDEV 函数为例做直接使用的性能对比,同时使用真实股票日频数据对所有函数进行分组使用性能对比。

4.1 直接使用性能对比

在 DolphinDB 中:

use mytt

close = 7.2 6.97 7.08 6.74 6.49 5.9 6.26 5.9 5.35 5.63
close = take(close, 100000)
timer x = mytt::AVEDEV(close, 100)

对一个长度为 100000 的向量直接使用 mytt 模块中的 AVEDEV 函数,耗时为 25ms。

与之对应的 Python 代码如下:

import numpy as np
from MyTT import *
import time

close = np.array([7.2,6.97,7.08,6.74,6.49,5.9,6.26,5.9,5.35,5.63])
close = np.tile(close,10000)
start_time = time.time()
x = AVEDEV(close, 100)
print("--- %s seconds ---" % (time.time() - start_time))

Python MyTT 库中的 AVEDEV 函数耗时为 25000ms,是 DolphinDB mytt module 中的 AVEDEV 函数的 1000 倍。测试数据量越大,性能差异越显著。

4.2 分组使用性能对比

  • 测试数据为上海证券交易所 2020 年,全年 2919 个证券(筛选交易日大于 120)日频交易数据,总记录数为 686,104 条。
  • 计算逻辑为按照股票代码进行分组计算各指标。
  • 为了测试函数计算性能,DolphinDB 和 Python 测试代码都是单线程运行。
  • DolphinDB 测试代码
  • Python 测试代码
  • 测试数据
  • Python MyTT 库

测试结果如下表所示:

从测试结果分析可知:

  • DolphinDB mytt module 中的函数计算性能远远超过 Python MyTT 库,最大的性能差距达到 5520 倍,普遍性能差距在 30 倍左右。

**Python pandas 测试核心代码 **

data.groupby("symbol").apply(lambda x: RSI(np.array(x.close), N = 24))

DolphinDB 测试核心代码

RSI = select symbol, tradedate, mytt::RSI(close, N=24) as `RSI from data context by symbol

5. 正确性验证

基于 4.2 分组使用性能对比中的测试数据和代码,验证 DolphinDB mytt module 中函数的计算结果是否和 Python MyTT 库一致。

5.1 浮点数精度问题

结果有差异的函数

  • CROSS, LONGCROSS

原因

  • 浮点数精度问题
  • 对于 CROSS 和 LONGCROSS 函数,在浮点数比较上,DolphinDB mytt module 中的处理比 Python MyTT 库更加严谨。DolphinDB mytt module 中首先会对浮点数 round 保留小数点后 6 位,然后再进行大小判断,而 MyTT 中并没有类似处理,因此对于相同大小的浮点数,Python 的判别可能会出错,如下图所示:

5.2 NULL 值的处理

结果有差异的函数

  • SUM, DMI, EMV, MASS, MFI, ASI

原因

  • 若输入向量开始包含空值,则从第一个非空位置开始计算。DolphinDB mytt module 与 Python MyTT 库的计算规则一致。
  • 对一个滚动 / 累积窗口长度为 k 的函数,每组最初的 (k-1) 个位置的结果均为空。DolphinDB mytt module 与 Python MyTT 库的计算规则一致。
  • 对一个滚动 / 累积窗口长度为 k 的函数,若一组中第一个非空值之后再有空值,Python MyTT 库会对包含 nan 的窗口计算结果都处理为 nan。DolphinDB mytt module 会对窗口内非 NULL 的元素按计算规则计算,得到一个非 NULL 的计算结果。

DolphinDB 代码与结果:

close = [99.9, NULL, 84.69, 31.38, 60.9, 83.3, 97.26, 98.67]
mytt::SUM(close, 5);

[,,,,276.87, 260.27, 357.53, 371.51]

Python 代码与结果:

close = np.array([99.9, np.nan, 84.69, 31.38, 60.9, 83.3, 97.26, 98.67])
MyTT.SUM(close,5)

array([nan, nan, nan, nan, nan,  nan, 357.53, 371.51])

以滑动窗口求和为例,close 向量的第 2 个元素为空值,DolphinDB mytt module 在计算第 5 个元素(60.9)时,回看过去 5 个窗口内的数据 [99.9, NULL, 84.69, 31.38, 60.9],对非 NULL 的元素求和,所以结果向量的第 5 个元素为 276.87。

Python MyTT 库会对包含 nan 的窗口计算结果都处理为 nan,所以结果向量的前 6 个元素都为 nan。

除上述因为浮点数精度问题和 NULL 值的处理问题导致计算结果存在差异外,其余函数计算结果的百分比误差均小于 1e-10。

6. 实时流计算案例

在 DolphinDB V1.30.3 中发布的响应式状态引擎(Reactive State Engine)是许多金融场景流批统一计算中的重要构件,DolphinDB mytt module 在开发时就对其做了适配,使得 mytt 模块中的大部分函数可以在响应式状态引擎中实现增量计算。

  • 当前无法在响应式状态引擎中使用的指标函数:RET, CONST, BARSLAST, BARSLASTCOUNT,已经规划开发。
  • 所有 mytt 中的 技术指标函数 均支持增量计算。

示例代码如下:

def cleanEnvironment(){
	try{unsubscribeTable(tableName="snapshotStream",actionName="aggr1min") } catch(ex){ print(ex) }
	try{dropStreamEngine("myttReactiveStateEngine") } catch(ex){ print(ex) }
	try{dropStreamEngine("aggr1min") } catch(ex){ print(ex) }
	try{dropStreamTable(`snapshotStream) } catch(ex){ print(ex) }
	try{dropStreamTable(`outputTable) } catch(ex){ print(ex) }
	undef all
}
cleanEnvironment()
go

//load modules
use mytt

//define stream table
name = `tradetime`SecurityID`high`low`open`close`vol
type = `TIMESTAMP`SYMBOL`DOUBLE`DOUBLE`DOUBLE`DOUBLE`INT
share streamTable(100:0, name, type) as snapshotStream
name = `SecurityID`tradetime`K`D`J`DIF`DEA`MACD`UPPER`MID`LOWER`ROC`MAROC
type = `SYMBOL`TIMESTAMP`DOUBLE`DOUBLE`DOUBLE`DOUBLE`DOUBLE`DOUBLE`DOUBLE`DOUBLE`DOUBLE`DOUBLE`DOUBLE
share streamTable(1000000:0, name, type) as outputTable

//register stream computing engine
reactiveStateMetrics=<[    tradetime,    mytt::KDJ(close, high, low, N=9, M1=3, M2=3) as `K`D`J,    mytt::MACD(close, SHORT_=12, LONG_=26, M=9) as `DIF`DEA`MACD,    mytt::KTN(close, high, low, N=20, M=10) as `UPPER`MID`LOWER,    mytt::ROC(close, N=12, M=6) as `ROC`MAROC]>

createReactiveStateEngine("myttReactiveStateEngine", metrics=reactiveStateMetrics, dummyTable=snapshotStream, outputTable=outputTable, keyColumn=`SecurityID, keepOrder=true)

createTimeSeriesEngine(name="aggr1min", windowSize=60000, step=60000, metrics=<[first(open),max(high),min(low),last(close),sum(vol)]>, dummyTable=snapshotStream, outputTable=getStreamEngine("myttReactiveStateEngine"), timeColumn=`tradetime, useWindowStartTime=true, keyColumn=`SecurityID)

subscribeTable(tableName="snapshotStream", actionName="aggr1min", offset=-1, handler=getStreamEngine("aggr1min"), msgAsTable=true, batchSize=2000, throttle=1, hash=0, reconnect=true)

7. DolphinDB mytt 指标列表

7.1 核心工具函数

7.2 应用层函数 (通过核心工具函数实现)

7.3 技术指标函数 (全部通过核心工具和应用函数实现)

8. 路线图(Road Map)

  • 优化当前无法在响应式状态引擎中使用的指标函数,包括 RET, CONST, BARSLAST, BARSLASTCOUNT,预计在 DolphinDB V1.30.19 和 DolphinDB V2.00.7 支持上述函数的增量计算和在响应式状态引擎中的使用。
  • 长期保持对 Python MyTT 包的同步更新。

附件

计算性能测试环境

  • CPU 类型:Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz 3.60 GHz
  • 逻辑 CPU 总数:8
  • 内存:32GB
  • OS:Windows 10