TA-Lib是一个Python库,封装了用C语言实现的金融交易技术分析的诸多常用指标。为了方便用户在DolphinDB中计算这些技术指标,我们使用DolphinDB脚本实现了TA-Lib中包含的指标函数,并封装在DolphinDB ta module (ta.dos)中。 使用ta模块需要DolphinDB Database Server 1.10.3 或以上版本。
1. 函数及参数的命名与用法规范
- 与TA-Lib中所有函数名大写以及所有参数名小写的规范不同,ta模块中,函数名及参数名均采用驼峰式命名法。
例如,TA-Lib中DEMA函数的语法为DEMA(close, timeperiod=30)
。在ta模块中相应的函数为dema(close, timePeriod)
。
- TA-Lib中某些函数有可选参数。ta模块中,所有参数皆为必选。
- 为得到有意义的结果,ta模块中函数的参数timePeriod要求至少是2。
2. 使用范例
2.1 脚本中直接使用指标函数
对一个向量直接使用ta模块中的wma
函数进行计算:
use ta
close = 7.2 6.97 7.08 6.74 6.49 5.9 6.26 5.9 5.35 5.63
x = wma(close, 5);
2.2 在SQL语句中分组使用
用户经常需要在数据表中对多组数据在每组内进行计算。在以下例子中,我们构造了一个包含2个股票的数据表:
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)
对其中每只股票使用ta模块中的wma
函数进行计算:
update t set wma = wma(close, 5) context by symbol
2.3 返回多个列的结果
某些函数会返回多个列的结果,例如函数bBands
。
直接使用的例子:
close = 7.2 6.97 7.08 6.74 6.49 5.9 6.26 5.9 5.35 5.63
low, mid, high = bBands(close, 5, 2, 2, 2);
在SQL语句中使用的例子:
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 *, bBands(close, 5, 2, 2, 2) as `high`mid`low from t context by symbol
symbol date close high mid low
------ ---------- ----- -------- -------- --------
F 2020.03.02 7.2
F 2020.03.03 6.97
F 2020.03.04 7.08
F 2020.03.05 6.74
F 2020.03.06 6.49 7.292691 6.786 6.279309
F 2020.03.09 5.9 7.294248 6.454 5.613752
F 2020.03.10 6.26 7.134406 6.328667 5.522927
F 2020.03.11 5.9 6.789441 6.130667 5.471892
F 2020.03.12 5.35 6.601667 5.828 5.054333
F 2020.03.13 5.63 6.319728 5.711333 5.102939
GPRO 2020.03.02 3.81
GPRO 2020.03.03 3.935
GPRO 2020.03.04 4.04
GPRO 2020.03.05 3.74
GPRO 2020.03.06 3.7 4.069365 3.817333 3.565302
GPRO 2020.03.09 3.33 4.133371 3.645667 3.157962
GPRO 2020.03.10 3.64 4.062941 3.609333 3.155726
GPRO 2020.03.11 3.31 3.854172 3.482667 3.111162
GPRO 2020.03.12 2.69 3.915172 3.198 2.480828
GPRO 2020.03.13 2.72 3.738386 2.993333 2.24828
3. 性能说明
ta模块中的函数与TA-Lib中对应函数相比,直接使用时的平均速度相似,但在分组计算时,ta模块中的函数性能远超TA-Lib中对应函数。本节的性能对比,我们以wma
函数为例。
3.1 直接使用性能对比
在DolphinDB中:
use ta
close = 7.2 6.97 7.08 6.74 6.49 5.9 6.26 5.9 5.35 5.63
close = take(close, 1000000)
timer x = wma(close, 5);
对一个长度为1,000,000的向量直接使用ta模块中的wma
函数,耗时为3毫秒。
与之对应的Python语句如下:
close = np.array([7.2,6.97,7.08,6.74,6.49,5.9,6.26,5.9,5.35,5.63,5.01,5.01,4.5,4.47,4.33])
close = np.tile(close,100000)
import time
start_time = time.time()
x = talib.WMA(close, 5)
print("--- %s seconds ---" % (time.time() - start_time))
TA-Lib中WMA
函数耗时为11毫秒,为DolphinDB ta module中wma
函数的3.7倍。
3.2 分组使用性能对比
在DolphinDB中,构造一个包含1000只股票,总长度为1,000,000的数据表:
n=1000000
close = rand(1.0, n)
date = take(2017.01.01 + 1..1000, n)
symbol = take(1..1000, n).sort!()
t = table(symbol, date, close)
timer update t set wma = wma(close, 5) context by symbol;
使用ta模块中的wma
函数对每只股票进行计算,耗时为17毫秒。
与之对应的Python语句如下:
close = np.random.uniform(size=1000000)
symbol = np.sort(np.tile(np.arange(1,1001),1000))
date = np.tile(pd.date_range('2017-01-02', '2019-09-28'),1000)
df = pd.DataFrame(data={'symbol': symbol, 'date': date, 'close': close})
import time
start_time = time.time()
df["wma"] = df.groupby("symbol").apply(lambda df: talib.WMA(df.close, 5)).to_numpy()
print("--- %s seconds ---" % (time.time() - start_time))
使用TA-Lib中WMA
函数对每只股票进行计算耗时为535毫秒,为ta模块中wma
函数的31.5倍。
4. 向量化实现
ta模块中的所有函数与TA-Lib一样,都是向量函数:输入为向量,输出的结果也是等长的向量。TA-Lib底层是用C语言实现的,效率非常高。ta模块虽然是用DolphinDB的脚本语言实现,但充分的利用了内置的向量化函数和高阶函数, 避免了循环,极为高效。已经实现的57个函数中,28个函数比TA-Lib运行的更快,最快的函数是TA-Lib性能的3倍左右;29个函数比TA-LIB慢,最慢的性能不低于TA-Lib的1/3。
ta模块中的函数实现亦极为简洁。ta.dos总共765行,平均每个函数约14行。扣除注释、空行、函数定义的起始结束行,以及为去除输入参数开始的空值的流水线代码,每个函数的核心代码约4行。用户可以通过浏览ta模块的函数代码,学习如何使用DolphinDB脚本进行高效的向量化编程。
4.1. 空值的处理
若TA-Lib的输入向量开始包含空值,则从第一个非空位置开始计算。ta模块采用了相同的策略。滚动/累积窗口函数的计算过程中,每组最初的尚未达到窗口长度的值,对应位置的结果为空。这一点TA-Lib与ta模块的结果一致。但之后,若再有空值,此位置以及所有以后位置在TA-Lib函数中的结果有可能均为空值。除非窗口中非空值数据的数量不足以计算指标(例如计算方差时只有一个非空值),否则空值的数量不影响ta模块函数结果的产生。
//use ta in dolphindb
close = [99.9, NULL, 84.69, 31.38, 60.9, 83.3, 97.26, 98.67]
ta::var(close, 5, 1)
[,,,,670.417819,467.420569,539.753584,644.748976]
//use talib in python
close = np.array([99.9, np.nan, 84.69, 31.38, 60.9, 83.3, 97.26, 98.67])
talib.VAR(close, 5, 1)
array([nan, nan, nan, nan, nan, nan, nan, nan])
上面的总体方差计算中,因为close的第二个值为空值,ta模块和TA-Lib的输出不同,TA-Lib输出全部为空值。如果替换空值为81.11,ta模块和TA-Lib得到相同的结果。在第一个元素99.9之前加一个空值,两者的结果仍然相同。简而言之,当输入参数只有开始的k个元素为空时,ta模块和TA-Lib的输出结果完全一致。
4.2 迭代处理
技术分析的很多指标计算会用到迭代,即当前的指标值取决于前一个指标值和当前输入: r[n] = coeff * r[n-1] + input[n]。对于这一类型的计算,DolphinDB引入了函数iterate
进行向量化处理,避免使用循环。
def ema(close, timePeriod) {
1 n = close.size()
2 b = ifirstNot(close)
3 start = b + timePeriod
4 if(b < 0 || start > n) return array(DOUBLE, n, n, NULL)
5 init = close.subarray(:start).avg()
6 coeff = 1 - 2.0/(timePeriod+1)
7 ret = iterate(init, coeff, close.subarray(start:)*(1 - coeff))
8 return array(DOUBLE, start - 1, n, NULL).append!(init).append!(ret)
}
以ema
函数实现为例,第5行代码计算第一个窗口的均值作为迭代序列的初始值。第6行代码定义了迭代参数。第7行代码使用iterate
函数计算ema序列。内置函数iterate
有非常高的运行效率,计算长度为1,000,000的向量的ema序列,窗口长度为10时,TA-Lib耗时7.4ms,ta模块仅耗时5.0ms,比TA-Lib更快。
4.3 滑动窗口函数的应用
大部分技术指标会指定一个滑动窗口,在每一个窗口中计算指标值。DolphinDB的内置函数中已经包括了一部分基本的滑动窗口指标的计算,包括mcount
, mavg
, msum
, mmax
, mmin
, mimax
, mimin
, mmed
, mpercentile
, mrank
, mmad
, mbeta
, mcorr
, mcovar
, mstd
和mvar
。这些基本的滑动窗口函数经过了充分的优化,大部分函数的复杂度达到了O(n),也即与窗口长度无关。更为复杂的滑动指标可以通过叠加或变换上述基本指标来实现。ta::var是总体方差,而DolphinDB内置的mvar是样本方差,所以需要经过调整。
def var(close, timePeriod, nddev){
1 n = close.size()
2 b = close.ifirstNot()
3 if(b < 0 || b + timePeriod > n) return array(DOUBLE, n, n, NULL)
4 mobs = mcount(close, timePeriod)
5 return (mvar(close, timePeriod) * (mobs - 1) \ mobs).fill!(timePeriod - 1 + 0:b, NULL)
}
下面我们给出一个更为复杂的例子,linearreg_slope指标的实现。linearreg_slope实际上是计算close相对于序列 0 .. (timePeriod - 1)的beta。这个指标看起来无法实现向量化,必须取出每个窗口的数据,循环进行beta计算。但事实上,这个例子中的自变量比较特殊,是一个固定的等差序列,计算后一个窗口的beta时,可以通过增量计算来优化。由于beta(A,B) = (sumAB - sumA*sumB/obs)/varB, varB和sumB是固定的,滑动窗口时,我们只需要优化sumAB和sumA的计算。通过公式化简,sumAB在两个窗口之间的变化可以通过向量化实现,具体参考代码第10行。代码第12行计算第一个窗口的sumAB。代码第13行中的sumABDelta.cumsum()向量化计算所有窗口的sumAB值。
def linearreg_slope(close, timePeriod){
1 n = close.size()
2 b = close.ifirstNot()
3 start = b + timePeriod
4 if(b < 0 || start > n) return array(DOUBLE, n, n, NULL)
5 x = 0 .. (timePeriod - 1)
6 sumB = sum(x).double()
7 varB = sum2(x) - sumB*sumB/timePeriod
8 obs = mcount(close, timePeriod)
9 msumA = msum(close, timePeriod)
10 sumABDelta = (timePeriod - 1) * close + close.move(timePeriod) - msumA.prev()
11 sumABDelta[timePeriod - 1 + 0:b] = NULL
12 sumABDelta[start - 1] = wsum(close.subarray(b:start), x)
13 return (sumABDelta.cumsum() - msumA * sumB/obs)/varB
}
计算长度为1,000,000的向量的linearreg_slope序列,窗口长度为10时,TA-Lib耗时13ms,ta模块耗时14ms,两者几乎相等。这对于用脚本实现的ta来说,已属不易。当窗口增加到20时,TA-Lib的耗时增加到22ms,而ta的耗时仍为14ms。这说明TA-Lib的实现采用了循环,对每一个窗口分别计算,而ta则实现了向量化计算,与窗口长度无关。
4.4 减少数据复制的技巧
对向量进行slice,join,append等操作时,很有可能发生大量数据的复制。通常数据复制会比很多简单的计算更耗时。这儿通过一些实际例子介绍如何减少数据复制的一些技巧。
4.4.1 使用向量视图subarray减少数据复制
如果直接slice某个向量的子窗口进行计算,会产生一个新的向量,并进行数据复制,不仅占用更多内存而且耗时。DolphinDB为此推出了一个新的数据结构subarray
。它实际上是原向量的一个视图,只是记录了原向量的指针,以及开始和结束位置,并没有分配大块内存来存储新的向量,所以实际上没有发生数据复制。所有向量的只读操作都可直接应用于subarray。ema和linearreg_slope的实现都大量使用了subarray。下面的例子中,我们对一个百万长度的向量进行100次slice操作,耗时62ms,每次操作耗时0.62ms。考虑到4.2中测试一个百万长度向量的ema操作耗时仅5ms,节约0.62ms是非常可观的。
close = rand(1.0, 1000000)
timer(100) close[10:]
Time elapsed: 62 ms
4.4.2 为向量指定容量(capacity)避免扩容
当我们往一个向量末尾追加数据时,如果容量不够,那么需要分配一个更大的内存空间,并把将旧的数据复制到新的内存空间,最后释放旧的内存空间。当向量比较大的时候,这个操作可能会比较耗时。如果明确知道一个向量最后的长度,那么事先指定这个长度为向量的容量可以避免向量扩容的发生。DolphinDB的内置函数array(dataType, [initialSize], [capacity], [defaultValue])可以在创建时指定capacity。譬如ema函数的第8行,先创建一个容量为n的向量,然后append计算结果。
5. DolphinDB ta 指标列表
Overlap Studies
Momentum Indicators
Volume Indicators
Volatility Indicators
Price Transform
Statistic Functions
Other Functions
- 对Ta-Lib中的 Math Transform 与 Math Operators 类函数,可使用相应的DolphinDB内置函数代替。例如,Ta-Lib中的 SQRT, LN, SUM 函数,可分别使用DolphinDB中的
sqrt
,log
,msum
函数代替。 - 下列 Ta-Lib 函数尚未在ta模块中实现:所有 Pattern Recognition 与 Cycle Indicators 类函数,以及HT_TRENDLINE(Hilbert Transform - Instantaneous Trendline), ADOSC(Chaikin A/D Oscillator), MAMA(MESA Adaptive Moving Average), SAR(Parabolic SAR), SAREXT(Parabolic SAR - Extended)函数。
6. 路线图(Roadmap)
- 尚未实现的指标函数,将在下一个版本中实现,预计在2020年4月完成。
- 目前DolphinDB的自定义函数不支持默认参数,也不支持函数调用时基于键值来输入参数。这两点将在DolphinDB Server 1.20.0中实现,届时ta模块将实现跟TA-Lib一致的默认参数。
- 使用ta模块前必须使用 use ta 以加载,这在交互式查询中不尽方便。DolphinDB Server将在1.20版本中允许在系统初始化时预加载模块,ta模块函数与DolphinDB内置函数将拥有同等地位,以省去加载模块这个步骤。