0%

非等间隔时间序列的指数加权移动平均

指数加权移动平均(EWMA或EWA)是量化交易中一种简单而强大的工具,特别适用于日内交易。它允许交易者快速轻松地跟踪指定时间段内证券的平均价格,并可用于识别趋势并进行交易决策。

通常来说EWMA的数学公式可以表示为如下:

EWMAt=αxt+(1α)EWMAt1\text{EWMA}_{t} = \alpha x_t +(1 - \alpha) \text{EWMA}_{t-1}

所以其关键在于α\alpha的计算,在pandas所提供的api中,提供了alphahalflifespancom这四个表示不同但是相互等价的参数,通常使用的多为alphaspan

其中com即为质心(Center of Mass),他的计算,可以认为是针对于每个时间点的权重的加权平均,所找到的位置,即:

CoM=t=0(1α)tαt=α(1α)t=0t(1α)t1=α(1α)t=0[(1α)t]=α(1α)[t=0(1α)t]=α(1α)[1α]=1αα\begin{aligned} \text{CoM} &= \sum_{t=0}^{\infty} (1 - \alpha)^t \alpha t \\ &= \alpha(1 - \alpha)\sum_{t=0}^{\infty} t(1-\alpha)^{t-1} \\ &= \alpha(1 - \alpha) \sum_{t=0}^{\infty} \left[-(1 - \alpha)^t\right]'\\ &= \alpha(1 - \alpha) \left[-\sum_{t=0}^{\infty} (1 - \alpha)^t\right]'\\ &= \alpha(1 - \alpha) \left[ - \frac{1}{\alpha}\right]' \\ &= \frac{1 - \alpha}{\alpha} \end{aligned}

化简上式,我们可以得到:

α=1/(1+CoM)\alpha = 1 / (1 + \text{CoM})

半衰期(Half-life)即为权重衰减到一半所需要的时间,所以我们可以得到:

(1α)H=0.5α=1exp(log2H)(1 - \alpha)^H = 0.5 \Rightarrow \alpha = 1 - \exp \left(-\frac{\log2}{H}\right)

以上均为时间间隔等长的情况,当面对不同间隔的时间序列的时候,我们可以使用index参数来指定时间序列的时间间隔,这样可以使得计算的结果更加准确。假设两个时间戳的间隔为dt,那么我们可以使用如下的公式来计算alpha

α=1exp(αdt)1(1αdt)=αdt\alpha' = 1 - \exp(-\alpha \text{d}t) \approx 1 - (1 - \alpha \text{d}t) = \alpha \text{d}t

当时间间隔总是为1的时候,实际上和最开始的公式基本等价。

考虑一个情形,依次有三个时间戳,分别为t0t1t2,那么dt1dt2分别为t1 - t0t2 - t1,那么我们可以使用如下的公式来计算alpha

EWMA2=α2x2+(1α2)EWMA1=α2x2+(1α2)(α1x1+(1α1)EWMA0)=α2x2+α1(1α2)x1+(1α1)(1α2)EWMA0\begin{aligned} \text{EWMA}_2 &= \alpha_2 x_2 + (1 - \alpha_2) \text{EWMA}_1 \\ &= \alpha_2 x_2 + (1 - \alpha_2) \left(\alpha_1 x_1 + (1 - \alpha_1) \text{EWMA}_0\right) \\ &= \alpha_2 x_2 + \alpha_1(1 - \alpha_2) x_1 + (1 - \alpha_1)(1 - \alpha_2) \text{EWMA}_0 \end{aligned}

其中t0时刻的权重为:

(1α1)(1α2)=exp(αdt1αdt2)=exp(α(t2t0))(1 - \alpha_1)(1 - \alpha_2) = \exp(-\alpha \text{d}t_1 - \alpha \text{d}t_2) = \exp(-\alpha (t_2 - t_0))

这样即使当中有多个时间戳到达,对于同样间隔的数据点,其权重仍然一致。对应的python代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from typing import Optional

import numpy as np


class EWMA(object):
def __init__(
self,
com: Optional[float] = None,
span: Optional[float] = None,
halflife: Optional[float] = None,
alpha: Optional[float] = None,
) -> None:
assert (
(com is None) + (span is None) + (halflife is None) + (alpha is None)
) == 3, "only one of com, span, halflife, alpha should be not None"
if com is not None:
self.alpha = 1 / (1 + com)
elif span is not None:
self.alpha = 2 / (span + 1)
elif halflife is not None:
self.alpha = 1 - np.exp(np.log(0.5) / halflife)
elif alpha is not None:
self.alpha = alpha

def __call__(self, x: np.ndarray, index: Optional[np.ndarray] = None) -> np.ndarray:
if index is not None:
alpha = 1 - np.exp(-np.diff(index, prepend=0) * self.alpha)
else:
alpha = np.ones_like(x) * self.alpha

ewma = np.zeros_like(x)
ewma[0] = x[0]
for i in range(1, len(x)):
ewma[i] = alpha[i] * x[i] + (1 - alpha[i]) * ewma[i - 1]
return ewma