因子半衰期¶
对于一个时间序列,我们可以构建一个逐渐衰减的时间序列模型来估计其半衰期。本文介绍了两个模型,用于估计一个时间序列的半衰期。
在量化研究中,了解各个因子的衰减情况,可以更有效地动态分配因子权重,以适应市场变化。
模型一:自回归模型¶
定义弱平稳过程 \(X_t\) 为: $$ X_t = c + \lambda X_{t-1} + \epsilon_t, 0 < \lambda < 1 \tag{1}\label{1} $$
公式 \(\eqref{1}\) 中要求 \(0 < \lambda < 1\),因此 \(X_t\) 应是逐渐衰减的。
对于公式 \(\eqref{1}\),我们可以使用线性回归估计 \(c\) 和 \(\lambda\)。
弱平稳意味着过程 \(X_t\) 有一个固定的均值:\(E[X] = \mu\)。对 \(X_t\) 取期望得到:
重新排列 \(c\) 得到:\(c = \mu(1 - \lambda)\)
将此代入公式 \(\eqref{1}\) 得到:
如果我们设 \(Y_t\) 为均值距离 \(X_t - \mu\),则: $$ Y_t = \lambda Y_{t-1} + \epsilon_t \tag{2}\label{2} $$
半衰期定义为 \(X_t\) 衰减到均值一半所需的时间 \(h\)。换句话说,是 \(Y_t\) 衰减到 \(0\) 的一半所需的时间 \(h\)。这可以写为:
根据公式 \(\eqref{2}\) 我们得到:
这意味着:
解 \(h\) 得到: $$ h = -\frac{\log(2)}{\log(\lambda)} \tag{3}\label{3} $$
因此,我们只需要估计出公式 \(\eqref{1}\) 中的 \(\lambda\),再代入公式 \(\eqref{3}\) 中即可得到半衰期。
Tip
对于一个因子,如果我们认为它的预测能力随着滞后期的增大而最终会趋于 \(0\),即认为公式 \(\eqref{1}\) 中的 \(c = 0\),那么只需要将公式 \(\eqref{1}\) 改写为不带截距项的一元线性回归。
模型二:指数衰减模型¶
构建如下指数衰减模型: $$ X_t = \alpha \lambda^{t}, 0 < \lambda < 1 \tag{4}\label{4} $$
设 \(h\) 为半衰期,则有:
因此 $$ \alpha \lambda^{t+h} = \frac{1}{2} \alpha \lambda^{t}\ $$
解 \(h\) 得到: $$ h = -\frac{\log(2)}{\log(\lambda)} \tag{5}\label{5} $$
因此,我们只需要估计出公式 \(\eqref{4}\) 中的 \(\lambda\),再代入公式 \(\eqref{5}\) 中即可得到半衰期。
下面我们估计公式 \(\eqref{4}\) 中的 \(\lambda\)。为推导简便,我们考虑 \(\alpha > 0\),由公式 \(\eqref{4}\) 得: $$ \log(X_t) = \log(\alpha) + t\log(\lambda) \tag{6}\label{6} $$
因此,我们只需要将 \(\log(X_t)\) 序列作为因变量,将 \(t\) 作为自变量,进行带截距项的一元线性回归,估计出的自变量系数即为 \(\log(\lambda)\),再代入公式 \(\eqref{5}\) 中即可得到半衰期。
Note
相比于模型一中的自回归模型,模型二的指数衰减模型有些弊端:
- 模型二要求 \(X_t\) 为正数,否则无法对其取对数,来进行公式 \(\eqref{6}\) 的回归。而模型一并不要求 \(X_t\) 为正数。
代码实现¶
我们使用模型一,将时间序列进行滞后一期的自回归,估计回归系数 \(\lambda\)。核心代码如下:
def halflife(series):
lambda_ = np.linalg.lstsq(
series[:-1].values[:, np.newaxis],
series[1:].values[:, np.newaxis],
rcond=None,
)[0].item()
if 0 < lambda_ and lambda_ < 1:
return -np.log(2) / np.log(lambda_)
else:
return np.NaN
完整代码见 GitHub。
不同模拟数据的估计结果¶
完整代码模拟了“真实期望为零”、“真实期望不为零”和“时间序列不收敛”三种情况。
对于真实期望为零的情况,无论是否带截距项 \(c\),估计结果都较为接近。
对于真实期望不为零的情况,是否带截距项 \(c\) 的估计结果有较大差异。(注:图中的红线和橙线、蓝线和绿线实际上是重合的,为观察方便,我加上了随机扰动,因此它们看起来并不重合。)
- 对于带截距项的回归,意味着模型认为时间序列会收敛到一个不一定为 0 的值,因此估计出的半衰期的含义是:衰减到当前值距离均值的一半所需的时间。
- 对于不带截距项的回归,意味着模型认为时间序列会收敛到 0,因此估计出的半衰期的含义是:衰减到当前值的一半所需的时间。
对于时间序列不收敛的情况,我们的判断逻辑是:若估计出的 \(\lambda\) 不满足 \(0 < \lambda < 1\),则认为无法估计出半衰期。下图是一个时间序列不收敛而无法估计出半衰期的示例。
计算速度¶
完整代码还对比了不同实现方法进行一元线性回归的计算速度,结果如下。可以发现,使用纯 numpy
进行实现的计算速度最快,而使用 statsmodels
的 AutoReg
接口虽然代码更加简洁,但计算速度较慢。
def halflife(series, method="numpy_no_intercept"):
if method == "numpy_no_intercept":
lambda_ = np.linalg.lstsq(
series[:-1].values[:, np.newaxis],
series[1:].values[:, np.newaxis],
rcond=None,
)[0].item()
elif method == "statsmodels_no_intercept":
lambda_ = AutoReg(series, lags=1, trend="n").fit().params["y.L1"]
elif method == "numpy_intercept":
lambda_ = np.polynomial.Polynomial.fit(
series.shift(1).iloc[1:], series.iloc[1:], deg=1, domain=[]
).coef[1]
elif method == "statsmodels_intercept":
lambda_ = AutoReg(series, lags=1).fit().params["y.L1"]
if 0 < lambda_ and lambda_ < 1:
return -np.log(2) / np.log(lambda_)
else:
return np.NaN