在股票投资中,我们会经常使用某种指标或者多种指标来对股票池进行筛选,这些用于选股的指标一般被称为因子。在米筐提供的因子系统中,目前仅支持日频率的因子。具体来说,一个因子在其定义的股票池中,对于池中的上市股票,每个交易日每只股票只会计算出一个值。
因子可分为基础因子和复合因子两种:
- 基础因子:不依赖于其他因子的因子。如基本的行情因子、财务因子,这些因子的值直接来源于财务报表或交易所行情数据;
- 复合因子:基础因子经过各种变换、组合之后得到的因子;
复合因子又可以分为两种:
- 横截面因子:典型的比如沪深 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, window, f)
这里我们定义了算子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
, kwargs
是 func
除 df
外的其他参数,计算引擎在调用 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)
.
# 横截面算子
对于横截面算子,我们提供了以下预定义的类:
CombinedCrossSectionalFactor(func, *factors)
: 定义在rqfactor.extension
中,其中func
的原型为func(*dfs)
.
API文档 →