在股票投资中,我们会经常使用某种指标或者多种指标来对股票池进行筛选,这些用于选股的指标一般被称为因子。在米筐提供的因子系统中,目前仅支持日频率的因子。具体来说,一个因子在其定义的股票池中,对于池中的上市股票,每个交易日每只股票只会计算出一个值。

因子可分为基础因子和复合因子两种:

  • 基础因子:不依赖于其他因子的因子。如基本的行情因子、财务因子,这些因子的值直接来源于财务报表或交易所行情数据;
  • 复合因子:基础因子经过各种变换、组合之后得到的因子;

复合因子又可以分为两种:

  • 横截面因子:典型的比如沪深 300 成分股根据当前的 pe_ratio 的值大小进行排序,序号作为因子值。在这种情况下,一个股票的因子值不再只取决于股票本身,而是与整个股票池中所有其他股票有关;对于横截面因子,一个给定的股票在不同的股票池中计算得到的因子值一般是不同的;
  • 非横截面因子

在实现上,基础因子是 rqfactor.interface.LeafFactor 的一个实例。米筐提供的公共因子可以用 Factor(factor_name) 来引用,如 Factor('open') 表示开盘价这个因子。

# 一个简单的因子

In[]:
from rqfactor import *
f = (Factor('close') - Factor('open')) / (Factor('high') - Factor('low'))
f
Out[]:
<rqfactor.interface.BinaryCombinedFactor at 0x126916b00>

在上面的代码中,我们定义了一个简单的因子f,它表示股票当天的收盘价与开盘价的差和最高价与最低价差的比值;可以看到这个定义是非常直观的。显然f是一个非横截面类型的复合因子。我们来看看这个因子的依赖:

In[]:
f.dependencies
Out[]:
[Factor('close'), Factor('open'), Factor('high'), Factor('low')]

这个因子具体应该如何计算?

In[]:
f.expr
Out[]:
(<ufunc 'true_divide'>,
 ((<ufunc 'subtract'>, (Factor('close'), Factor('open'))),
  (<ufunc 'subtract'>, (Factor('high'), Factor('low')))))

expr属性返回了一个前缀表达式树;因子计算引擎正是根据这棵树来计算因子值的。

# 算子

在前面的因子定义中,Factor('close') - Factor('open')中减法是怎么回事呢?从业务层面看,非常简单,两个因子相减,生成了一个新的因子;从实现层面看,两个LeafFactor相减?我们来检验一下:

In[]:
Factor('close') - Factor('open')
Out[]:
<rqfactor.interface.BinaryCombinedFactor at 0x10a7d5c88>

与业务层面看起来一样,两个因子相减,确实生成了一个新的因子。对一个或多个 因子进行组合、变换,生成一个新的因子,这样的函数我们称为算子。在上面的例子中,-(减号) 正是我们预先定义的一个算子。一个算子封装一个对输入的因子进行变换的函数,- 这个算子对应的是numpy.ufunc.subtract;这个函数由因子计算引擎在计算因子值时调用。

在本系统中,算子除 +, -, *, /, **, //, <, >, >=, <=, &, |, ~, !=这些内置的操作符外,都以全大写命名,如MIN, MA, STD

与复合因子类似,算子可以分为两类,横截面算子和非横截面算子。一个因子,如果在表达式中使用了横截面算子,就成为了一个横截面因子。一般情况下,横截面因子命名以 CS_ (cross sectional)为前缀,如 CS_ZSCORE;非横截面算子一般不带前缀,或以 TS_ (time series)为前缀,以和类似功能的横截面因子区分。

非横截面算子封装的函数,其输入是一个或多个一维的numpy.ndarray; 横截面算子封装的函数,其输入则是一个或多个pandas.DataFrame

系统提供的算子可以参考RQFactor API 手册中关于算子的描述

# 数据处理的细节

# 复权

我们来看一个简单的因子:

f2 = Factor('close') / REF(Factor('close'), 1) - 1

在这个因子定义中,REF用来对因子在时间上进行调整,REF(Factor('close'), 1)表示上一个交易日的收盘价。f2这个因子表示相对于上一个交易日的涨幅,也就是当日收益率。

不过,如果股票在当天进行了分红或者拆分,其收盘价与上一个交易日的收盘价是不可以直接比较的:需要对价格序列进行复权处理。在本系统中,Factor('open'), Factor('close') 等价格序列是后复权价格,另外提供了Factor('open_unadjusted'), Factor('close_unadjusted') 等带有后缀_unadjusted的不复权价格数据。

我们建议,所有使用价格数据的因子,其最终输出应该是一个无量纲的数字,避免使用价格的绝对数值。

# 停牌处理

对于很多使用了均线的技术指标来说,在计算时需要过滤掉停牌期间的数据,否则结果会不符合预期。

因此,因子计算引擎在计算因子值时,会过滤掉停牌期间的数据;在计算完成后,将停牌日期的因子值填充为 NaN。

# NaN 及 Inf 处理

在系统提供的横截面算子中,Inf 与 NaN 处理方式相同,参考pandas mode.use_inf_as_na=True (opens new window)时的行为。

# 自定义算子和因子

使用系统提供的基础因子和算子,已经可以写出很多因子了,比如著名的alpha101 (opens new window)。不过,有时候系统内置的算子不能满足需求,比如需要一种不一样的均线计算方式。这时候你就需要用到自定义算子

# 自定义算子

我们以一个对时间序列进行指数加权的算子为例,说明如何定义一个算子。这个算子实现如下功能:

  • 半衰期为 22 个交易日;
  • 时间窗口长度可设置;
  • 输出值为加权平均值;

我们先看一下这个自定义算子的代码:

import numpy as np
from rqfactor.extension import rolling_window
from rqfactor.extension import RollingWindowFactor
def my_ema(series, window):
    # series: np.ndarray, 一维数组
    # window: int, 窗口大小
    q = 0.5 ** (1 / 22)
    weight = np.array(list(reversed([q ** i for i in range(window)])))
    r = rolling_window(series, window)
    return np.dot(r, weight) / window
def MY_EMA(f, window):
    return RollingWindowFactor(my_ema, window, f)

我们来逐行看一下这个代码:

import numpy as np

这一行引入了numpy这个包。

from rqfactor.extension import rolling_window

rolling_window是定义在rqfactor.extension中的一个辅助函数,它实现了一个一维数组的滑动窗口算法,具体演示如下(其中第一个参数是一个一维数组,第二个参数代表滑动窗口的大小):

In[]:
a = np.arange(100)
rolling_window(a, 20)
Out[]:
array([[ 0,  1,  2, ..., 17, 18, 19],
       [ 1,  2,  3, ..., 18, 19, 20],
       [ 2,  3,  4, ..., 19, 20, 21],
       ...,
       [78, 79, 80, ..., 95, 96, 97],
       [79, 80, 81, ..., 96, 97, 98],
       [80, 81, 82, ..., 97, 98, 99]])

上述代码从一个长度为 100 的数组生成了 81 个长度为 20 的数组,每一个数组的长度都是 20,起止索引都比前一个数组多 1。

from rqfactor.extension import RollingWindowFactor

我们实现这个算子是一个滑动窗口算子,RollingWindowFactor 中封装了相应的细节。

def my_ema(series, window):
    # series: np.ndarray, 一维数组
    # window: int, 窗口大小
    q = 0.5 ** (1 / 22)
    weight = np.array(list(reversed([q ** i for i in range(window)])))
    r = rolling_window(series, window)
    return np.dot(r, weight) / window

这是实际的运算逻辑,weight是对应的权重。

def MY_EMA(f, window):
    return RollingWindowFactor(my_ema, f, window)

这里我们定义了算子MY_EMA,它有两个参数,f 是输入因子,window 是窗口大小。这个函数返回一个 RollingWindowFactor 对象。RollingWindowFactor 类接受三个参数,第一个是实际执行变换的函数,在这个例子里是 my_ema,第二个参数是窗口大小,第三个参数是待变换的因子。

我们来试试这个刚定义的算子:

In[]:
f3 = MY_EMA(Factor('close'), 60)
execute_factor(f3, ['000001.XSHE', '600000.XSHG'], '20180101', '20180201')
Out[]:
	000001.XSHE	600000.XSHG
2018-01-02	595.519641	71.160893
2018-01-03	596.415462	71.127573
2018-01-04	597.134786	71.096515
2018-01-05	597.910246	71.072833
2018-01-08	598.141833	71.051230
2018-01-09	598.508616	71.031295
2018-01-10	599.535419	71.078636
2018-01-11	600.368100	71.105770
2018-01-12	601.440555	71.124114
2018-01-15	603.602941	71.167961
2018-01-16	605.771385	71.191253
2018-01-17	607.872235	71.253908
2018-01-18	610.756388	71.341881
2018-01-19	613.707366	71.429583
2018-01-22	615.869869	71.417998
2018-01-23	618.315959	71.437837
2018-01-24	620.674527	71.596174
2018-01-25	622.260534	71.768031
2018-01-26	623.511577	71.886025
2018-01-29	624.244004	72.008997
2018-01-30	624.831171	72.060310
2018-01-31	625.906664	72.120089
2018-02-01	626.862451	72.203242

execute_factor 会调用因子计算引擎来计算因子值。

# 自定义横截面算子

上面我们定义了一个非横截面类型的算子,下面我们看看如何定义一个横截面算子。系统提供了一个行业中性化的算子,INDUSTRY_NEUTRALIZE,这个算子采用的是申万一级行业分类;现在希望使用中信行业分类,为此我们需要定义一个算子:

import pandas as pd
import rqdatac
from rqfactor.extension import UnaryCrossSectionalFactor
def zx_industry_neutralize(df):
    # 横截面算子在计算时,输入是一个 pd.DataFrame,其 index 为 trading date,columns 为 order_book_id
    latest_day = df.index[-1]
    # 事实上我们需要每个交易日获取行业分类,这样是最准确的。不过这里我们简化处理,直接用最后一个交易日的行业分类
    # 无需担心 rqdatac 的初始化问题,在因子计算引擎中已经有适当的初始化,因此这里可以直接调用
    industry_tag = rqdatac.zx_instrument_industry(df.columns, date=latest_day)['first_industry_name']
    # 在处理时,对 inf 当做 null 处理,避免一个 inf 的影响扩大
    with pd.option_context('mode.use_inf_as_null', True):
        # 每个股票的因子值减去行业均值
        result = df.T.groupby(industry_tag).apply(lambda g: g - g.mean()).T
        # reindex 确保输出的 DataFrame 含有输入的所有股票
        return result.reindex(columns=df.columns)
def ZX_INDUSTRY_NEUTRAILIZE(f):
    return UnaryCrossSectionalFactor(zx_industry_neutralize, f)

UnaryCrossSectionalFactor 封装了横截面算子的一些细节,其原型如下:

UnaryCrossSectionalFactor(func, factor, *args, **kwargs)

其中 args, kwargsfuncdf 外的其他参数,计算引擎在调用 func 时,会一并传入。

我们来试试这个新的算子:

In[]:
f4 = ZX_INDUSTRY_NEUTRAILIZE(Factor('pb_ratio'))
execute_factor(f4, index_components('000300.XSHG', '20180201'), '20180101', '20180201')
Out[]:
002508.XSHE	601727.XSHG	600362.XSHG
2018-01-02	4.772367	-1.187943	-2.622838
2018-01-02	4.772367	-1.187943	-2.622838
......

自定义算子方面我们就介绍到这里。更详细的信息可以参考附录,详细列出了rqfactor提供的相关工具函数。

# 自定义基础因子

自定义算子解决的是自定义转换方法的问题,自定义基础因子解决的则是材料问题。我们来看一个实际的例子:股票的日内波动率,也就是计算每个交易日分钟线的收盘价的波动率。我们来看一下代码:

import numpy as np
import pandas as pd
import rqdatac
# 所有自定义基础因子都是 UserDefinedLeafFactor 的实例
from rqfactor.extension import UserDefinedLeafFactor
# 计算因子值
def get_factor_value(order_book_ids, start_date, end_date):
    """
    @param order_book_ids: 股票/指数代码列表,如 000001.XSHE
    @param start_date: 开始日期,pd.Timestamp 类型
    @param end_date: 结束日期,pd.Timestamp 类型
    @return pd.DataFrame, index 为 pd.DatatimeIndex 类型,可通过 pd.to_datetime(rqdatac.get_trading_dates(start_date, end_date)) 生成;column 为 order_book_id;注意,仅包含交易日
    """
    data = rqdatac.get_price(order_book_ids, start_date, end_date, fields='close', frequency='1m', adjust_type='none')
    if data is None or data.empty:
        return pd.DataFrame(
            index=pd.to_datetime(rqdatac.get_trading_dates(start_date, end_date)),
            columns=order_book_ids)
    result = data.groupby(lambda d: d.date()).apply(lambda g: g.pct_change().std())
    # index 转换为 pd.DatetimeIndex
    result.index = pd.to_datetime(result.index)
    return result
f5 = UserDefinedLeafFactor('day_volatility', get_factor_value)

UserDefinedLeafFactor的原型如下:

UserDefiendLeafFactor(name, func)

其中,参数name是因子名称,func则是因子值的计算方法,其原型如上面代码中注释所示。

我们来使用一下这个因子:

In[]:
execute_factor(f5, ['000001.XSHE', '600000.XSHG'], '20180101', '20180201')
Out[]:
	000001.XSHE	600000.XSHG
2018-01-02	0.001672	0.000872
2018-01-03	0.001680	0.000772
2018-01-04	0.001232	0.000767
2018-01-05	0.000830	0.000639
2018-01-08	0.000943	0.000619
2018-01-09	0.000999	0.000585
2018-01-10	0.001251	0.001110
2018-01-11	0.001203	0.000852
2018-01-12	0.001065	0.000694
2018-01-15	0.001562	0.000817
2018-01-16	0.001791	0.000909
2018-01-17	0.002437	0.001630
2018-01-18	0.001841	0.001025
2018-01-19	0.001785	0.001460
2018-01-22	0.001730	0.001036
2018-01-23	0.001777	0.001054
2018-01-24	0.002149	0.002010
2018-01-25	0.001493	0.001465
2018-01-26	0.001483	0.001591
2018-01-29	0.001488	0.001163
2018-01-30	0.001303	0.001158
2018-01-31	0.001268	0.001162
2018-02-01	0.002066	0.001315

这时候,f5作为一个自定义因子,已经可以如其他基础因子一样使用了:

In[]:
execute_factor(f5 * Factor('pb_ratio'), ['000001.XSHE', '600000.XSHG'], '20180101', '20180201')
Out[]:
	000001.XSHE	600000.XSHG
2018-01-02	0.001946	0.000823
2018-01-03	0.001903	0.000725
2018-01-04	0.001387	0.000723
2018-01-05	0.000938	0.000602
2018-01-08	0.001038	0.000583
2018-01-09	0.001110	0.000551
2018-01-10	0.001432	0.001073
2018-01-11	0.001370	0.000818
2018-01-12	0.001227	0.000665
2018-01-15	0.001885	0.000790
2018-01-16	0.002160	0.000870
2018-01-17	0.002947	0.001585
2018-01-18	0.002303	0.001008
2018-01-19	0.002245	0.001434
2018-01-22	0.002123	0.000982
2018-01-23	0.002212	0.001009
2018-01-24	0.002673	0.002024
2018-01-25	0.001801	0.001484
2018-01-26	0.001770	0.001584
2018-01-29	0.001737	0.001161
2018-01-30	0.001511	0.001126
2018-01-31	0.001513	0.001136
2018-02-01	0.002463	0.001298

# 附录 自定义算子参考

# 非横截面算子

非横截面算子又可以分为两种,一种算子计算的结果只与输入因子的当期值有关,这种算子输出的因子值长度与输入因子值相同,这种我们称为简单算子,如LOG, +;另一种则是根据输入因子的一个时间序列进行计算,如最近 20 个交易日的均值,这种因子我们称为滑动窗口算子。

对于上面两种算子,我们提供了一些预定义的类:

  • 简单算子
    • CombinedFactor(func, *factors): 定义在rqfactor.extension中;其接受的func原型为func(*series);
  • 滑动窗口算子
    • RollingWindowFactor(func, window, factor): 定义在rqfactor.extension中;func函数原型为def func(series, window);
    • CombinedRollingWindowFactor(func, window, *factors): 定义在rqfactor.extension中,接受多个因子作为输入,func函数原型为def func(window, *series).

滑动窗口算子中有一种特殊情况:封装talib中的函数形成的算子。talib函数返回的数组长度与输入一致,数组前几项为nan:

In[]:
import talib
talib.MA(np.full(100, 1.0), 10)
Out[]:
array([nan, nan, nan, nan, nan, nan, nan, nan, nan,  1.,  1.,  1.,  1.,
        1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,
        1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,
        1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,
        1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,
        1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,
        1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,
        1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.])

我们需要将开头的nan除去。为此我们提供了专门针对talib的类:

TALibFactor(func, factor, window): functalib中函数,window为窗口大小。

# 横截面算子

对于横截面算子,我们提供了以下预定义的类:

  • CombinedCrossSectionalFactor(func, *factors): 定义在 rqfactor.extension 中,其中 func 的原型为 func(*dfs).
Last Updated: 10/29/2020, 6:14:56 PM