TA-Lib 技术分析库高效地实现了 150 多种常用的技术指标,如 ADX、MACD 和 RSI 等,甚至还包括 K 线模式识别。基于其开源 API,在 Python 环境下,ta-lib 库对其进行了基于 Cython 的封装。

这里以大写的 TA 指代原生的开源技术分析库,小写的 ta 指代 Python 的封装实现。

本文将探究 Python 的 Pandas 数据分析库中的几个核心技术,以及它们在技术分析中的应用,其中也必然涉及到 NumPy 数值计算库。这里主要讨论以下几个方面:1)向量化运算(vectorization);2)(指数加权)滚动窗口;3)UFuncs 等。利用 Pandas 库,我们可以简单高效地实现一些没有出现在 TA-Lib 技术分析库中的指标,来进一步扩充 ta-lib 库。文章最后讨论了本地的数据结果和交易所端给出的指标数据存在偏差的原因。

import numpy as np
import pandas as pd
import talib as ta

测试环境:macOS 10.13.3;Python 3.6.5;Pandas 0.22.0;NumPy 1.14.2;TA-Lib 0.4.17。

Pandas 向量化计算

  • 像 Pandas、scikit-learnTensorFlow 这类高层工具集,其底层都是基于高性能科学计算和数据分析基础包 NumPy 实现的,后者提供了一个强大的多维数组的数据类型:ndarray,可以进行高效的向量化数值计算和广播操作,避免了使用循环来遍历数组中的每个元素。

NumPy 在向量化实现上的高效主要得益于其底层核心函数是用 C 语言实现的。这一性能优势在数据科学和机器学习中尤为重要。在 Pandas 中,对于大部分 NumPy 函数,都可以像使用原生 ndarray 一样将 Series 类型数据作为参数来进行向量化操作。

  • 针对 Pandas 的 DataFrame 数据类型,Sofia Heisler 在「A Beginner’s Guide to Optimizing Pandas Code for Speed」一文中通过一个案例研究,详细比较了以下五种计算方法的性能优劣:1)最简单的使用基于索引的循环;2)使用 iterrows() 方法遍历;3)使用 apply() 方法遍历;4)基于 Pandas 序列(Series)的向量化;5)基于 NumPy 数组的向量化。

尽管实验结果表明 NumPy 数组的向量化具有最佳的性能,但其本质是对 Pandas 序列使用了 values() 方法,从而将其转换为 NumPy 数组,因此它同样得益于 Pandas 序列的向量化,而后者相比使用 apply() 方法提升了近 56 倍的速度。尽管该结果并不适用于所有情况,但正如作者最后指出的,我们应当尽可能避免使用单纯的迭代遍历方法,Pandas 中绝大部分运算都是可以被向量化的,其性能要远胜于标量运算(scalar operations)。

Noq7qev6C5LqHyVi.png

Pandas 滚动窗口函数

  • 移动平均(moving average)可能是滚动统计(rolling statistics)中最常被使用的,它会从时间序列数据中取一个移动窗口(moving window)来计算其平均值。Pandas 提供了一组常用的滚动函数来计算时间序列数据的统计信息,这些函数都以 rolling_ 为前缀,比如 pd.rolling_mean() 对应的便是移动平均。

调用这些函数时都必须指定所观测的移动数据窗口 window 的大小。Pandas 在计算时,若当前窗口大小小于指定的窗口大小时,则会生成 NaN 数据。此外,除了使用 Pandas 提供标准的统计函数,也可以利用 rolling_apply() 将窗口数据应用于自定义函数上。

ddf = pd.Series([1.0, 2.0, 3.0, 4.0])
roll = pd.rolling_mean(ddf, 2)

Pandas 的 pd.rolling_mean(df, window=N) 和 TA-Lib 的 ta.MA(df, timeperiod=N) 两者是等价的。

  • Pandas 从 0.18.0 开始对窗口函数进行了重构,使其不再作为顶层函数,而是作用于 SeriesDataFrame 对象之上,并且返回的是一个 Rolling 对象。这样,就可以按照类似于 .groupby 这种 API 的调用形式来使用上述的窗口函数了。完整的文档可参考「#11603」和「#12373」。
# Before 0.18.0
>>> r = pd.rolling_mean(df, window=3)
>>> type(r)
<class 'pandas.core.frame.DataFrame'>
# After 0.18.0
>>> r = df.rolling(window=3)
>>> type(r)
<class 'pandas.core.window.Rolling'>
>>> r.<TAB>
>>> r.
r.a               r.corr(           r.is_freq_type    r.min(            r.sum(
r.agg(            r.count(          r.kurt(           r.ndim            r.validate(
r.aggregate(      r.cov(            r.max(            r.quantile(       r.var(
r.apply(          r.exclusions      r.mean(           r.skew(
r.b               r.is_datetimelike r.median(         r.std(

Pandas 指数加权移动窗口函数

  • 指数加权(exponentially weighted)过滤器通常用于对数据集进行降噪,或者平滑(smooth),其中权重通过指数函数计算得到,越近期的数据具有越小的衰减系数(decay factor),对应更大的权重。

pd.rolling_* 滚动窗口函数族类似,指数加权函数族 pd.ewm* 从 Pandas 0.18.0 开始也不再作为顶层函数了,而是统一使用 ewm() 方法进行调用,并且返回一个 EWM 对象。

Pandas UFuncs 函数

  • UFuncs(Universal Functions)是基于 Numpy 的一类特殊函数,它们都使用 C 语言实现,因此十分高效。在技术分析中,较为常用的是 shift(periods)diff(periods) 函数,前者将数据整体前移(periods 为负)或后移(periods 为正)period 个单位;后者计算数据序列中每相隔 periods 个单位数据之间的差值。

在技术分析中,对于收盘价数据序列,若要计算每一天的收盘价和前一天的收盘价的差值,只需使用 df['close'].diff(1) 即可,它相当于 df['close'] - df['close'].shift(1)。这两个方法适用于 SeriesDataFrame 类型的数据。另外一个十分常用的是 dt 属性访问器(accessor),可用于快速提取时间数据序列的信息,如:s.dt.hour

部分技术指标的 Pandas 实现

这里的示例均以日线周期为基准,函数参数 ddf 表示 pandas.Dataframe 类型的 OHLCV 数据,即包含 datetimeopenhighlowclosevolume 等数据列;PERIOD 为用于计算均线的周期值。尽管这些指标相对比较小众,但出现在了部分交易所的行情图表中。

  • EMV:简易波动指标(Ease of Movement Indicator),有时也称为 EOV。该指标同时考量了价格波动和成交量,具体介绍可参考「Ease Of Movement」。其计算公式为:
DM = ((High + Low) / 2 - (PriorHigh + PriorLow) / 2)
BR = (Volume / 100,000) / (High - Low)
EMV = DM / BR

注意到,当前价格与前一日价格之差可以直接利用 diff(1) 方法计算得到。以下是 EMV 指标 Python 版本的实现。

def EMV(ddf, PERIOD):
  """Ease of Movement"""
  EMV_L = (ddf['high'].diff(1) + ddf['low'].diff(1))
  EMV_R = (ddf['high'] - ddf['low']) / (2 * ddf['volume'])
  EMV = EMV_L * EMV_R
  EMV_MA = EMV.rolling(PERIOD).mean()
  return EMV, EMV_MA_N
  • BRAR:人气意愿指标,其中 AR(人气指标)注重开盘价,而 BR(意愿指标)则注重收盘价。以下是BRAR 指标的计算公式和 Python 版本的实现。
AR = Sum((High - Open), N-Day) / Sum((Open - Low), N-Day)
BR = Sum((High - PriorClose), N-Day) / Sum((PriorClose - Low), N-Day)
def BRAR(ddf, N):
    """Popular and Willingness Indicator"""
    AR_NT = (ddf['high'] - ddf['open']).rolling(N).sum()
    AR_DT = (ddf['open'] - ddf['low']).rolling(N).sum()
    AR = AR_NT / AR_DT * 100
    BR_NT = (ddf['high'] - ddf['close'].shift(1)).rolling(N).sum()
    BR_DT = (ddf['close'].shift(1) - ddf['low']).rolling(N).sum()
    BR = BR_NT / BR_DT * 100
    return BR, AR
  • PSY:心理线指标(Psychological Line)。该指标相对比较简单,即 N 日内价格上涨天数的比例,以下是 PSY 指标的 Python 版本实。这里使用了 apply() 方法来使用自定义的统计函数。
def PSY(ddf, N, PERIOD):
    """Psychological Line Indicator"""
    PSY = (ddf['close'] - ddf['open']).rolling(N).apply(
        lambda x: len(x[x>0]) / len(x) * 100)
    PSY_MA = PSY.rolling(PERIOD).mean()
    return PSY
  • 其他一些没有出现在 TA-Lib 库中的指标的 Python 实现,可以参考「Technical analysis Indicators without Talib」一文。对于已有的指标,作者也给出了单纯地基于 Pandas 的实现。

写在最后

在技术分析中,不管是使用基于 TA-Lib 的实现还是手动实现,很大概率下其结果多会与交易所给出的数据结果有偏差。造成这种偏差的原因有很多,而更为悲观的是,我们并无法完全百分之百确定这些原因究竟是什么。

事实上,不同交易所之间指标的实现可能也不尽相同。比如差别最大的 STOCHRSI 指标,尽管我们的实现匹配了 OKEx 给出的数据,但它并不是对 STOCHRSI 的一个标准实现。在计算 %K 时,它对分子和分母分别进行了移动平均运算:

K = ta.MA((RSI - LL_RSI), smoothK) / ta.MA((HH_RSI - LL_RSI), smooth_K)

crypto-signal 的作者指出了可能造成偏差的一些原因。其中最主要的一个原因在于交易所本身具有更多的历史数据。对某些指标而言,数据量的不同往往会对结果造成很大的不同,具体可参考 TA-Lib 对「不稳定周期(unstable period)的解释」。

庆幸的是,开源的技术分析库 TA-Lib 创建于 1999 年,而在过去近 20 年,它也并被广泛应用于了各种技术分析项目。因此,即使指标的结果会存在一定的偏差,TA-Lib 仍然是最值得我们信赖的技术分析库之一。

发表新评论

沪ICP备17018959号-3