如何在 Pandas 中使用滚动窗口计算波动率(标准差)

21

我有一个时间序列“Ser”,想要使用滚动窗口计算波动率(标准差)。我的当前代码在这种形式下可以正确执行:

w = 10
for timestep in range(length):
    subSer = Ser[timestep:timestep + w]
    mean_i = np.mean(subSer)
    vol_i = (np.sum((subSer - mean_i)**2) / len(subSer))**0.5
    volList.append(w_i)

这对我来说似乎非常低效。Pandas是否具有内置功能来执行此类操作?


2
@Prune。根据我下面的回答,我认为这个问题符合SO的要求。OP实际上是在问是否有内置方法来执行滑动窗口。上面的代码只是为了展示努力。 - Mad Physicist
1
@Prune,这实际上最终与Pandas的使用有关。我添加了标签,也许回答的人可以清理一下标题。 - pvg
我已经修正了标题和措辞,使其在SO上非常明确地与主题相关。希望你喜欢它。 - Mad Physicist
我们确定OP正在使用pandas吗?代码中只提到了numpy。虽然我认为这绝对是正确的方法,但我认为编辑问题以询问一个库的使用,而据我所知OP并没有使用,有点不太对劲。即使猜测是正确的,我仍然会坚持这个看法。 - DSM
请问您是如何计算波动率的?您有一些计算波动率的最终代码示例吗? - kramer65
显示剩余4条评论
4个回答

27

通常,[金融类] 的人会以价格百分比年化波动率的形式引用。

假设您在一个数据框架 df 中有每日价格,并且一年有252个交易日,以下内容可能是您想要的:

df.pct_change().rolling(window_size).std()*(252**0.5)


1
为什么我们应该对交易日的数量进行平方根运算? - zipline86
@zipline86。可能是因为标准差是一个平方根。你正在通过总天数将平均值在平方根下归一化。 - Mad Physicist
平方根来自于预期的波动不随天数线性变化。我们有每天的预期波动(百分比变化)。你取了它的“std”。所以你得到的是一个“每天”的标准值。如果你想把它转换成一整年的预期波动,你需要将它乘以天数的平方根。为什么要用平方根,我无法解释。但很容易直观地看出,将其乘以天数会给你一个太大的数字。(这意味着股票每天都朝同一个方向移动。) - Kris
年化波动率的原因是什么?https://www.wallstreetmojo.com/volatility-formula/ - J.D
嗯,不完全正确。例如,波动率在期权定价中以年化形式报价。对于VaR(风险价值)计算,应该假定为每日。因此,本质上,[金融类型]的人知道每种工具都有其自己令人讨厌的特点。幸运的是,一旦你知道了这一点,转换就很简单:vol_year = vol_day * sqrt(252)一年被假定为252个交易日。约定俗成。 - jfaleiro
@aaron - 为什么你选择不使用ddof = 0来计算标准差,就像上面的答案一样?谢谢。 - Rocky the Owl

24

看起来你正在寻找 Series.rolling。你可以对结果对象应用std计算:

roller = Ser.rolling(w)
volList = roller.std(ddof=0)

如果您不打算再次使用rolling window对象,您可以编写一个一行代码的简短语句:

volList = Ser.rolling(w).std(ddof=0)

请记住,在这种情况下ddof=0是必要的,因为标准差的规范化是通过len(Ser)-ddof来计算的,并且在pandas中默认ddof1


哎呀,真希望在我花了四个小时找错误之前就知道 ddof 是什么了! :) - Jonas Byström
1
对我来说,通过阅读文档并不清楚ddof的作用;也许那些精通更高级数学的人知道“Delta自由度”这个术语。只有测试代码才能让我真正理解它。此外,如果你想完成任务,阅读所有文档通常是完全浪费时间。 - Jonas Byström
1
@RockytheOwl。这是基于w=10的10天波动性。 - Mad Physicist
@RockytheOwl。你是假定高斯随机游走吗? - Mad Physicist
@MadPhysicist - 我想是这样吧...? 我刚刚在处理一组股票收盘价值的数据框,并被告知要“计算波动率”。我想使用的是对数收益率标准差公式(所以我认为这假设了高斯随机漫步,但如果我错了,请纠正我)。在实现方面的搜索中,我找到了您的答案。我只是对将任意大小的窗口年化到一年感到困惑。也就是说,我们只需取任何(合理的)窗口大小并获得收益的标准差,但现在只是想知道如果我乘以sqrt(252)因子意味着什么? - Rocky the Owl
显示剩余7条评论

17

"波动性"即使在金融领域也是一个含义模糊的词。最常被提及的波动类型是实现波动率,它是实现方差的平方根。与收益标准差相比,其关键区别在于:

  • 使用对数收益率(而非简单收益率)
  • 数据以年化方式显示(通常假设一年有252至260个交易日)
  • 在方差交换中,对数收益率未进行去均值处理

有多种计算实现波动率的方法;然而,我下面实现的是两种最为常见的方法:

import numpy as np

window = 21  # trading days in rolling window
dpy = 252  # trading days per year
ann_factor = days_per_year / window

df['log_rtn'] = np.log(df['price']).diff()

# Var Swap (returns are not demeaned)
df['real_var'] = np.square(df['log_rtn']).rolling(window).sum() * ann_factor
df['real_vol'] = np.sqrt(df['real_var'])

# Classical (returns are demeaned, dof=1)
df['real_var'] = df['log_rtn'].rolling(window).var() * ann_factor
df['real_vol'] = np.sqrt(df['real_var'])

1
我认为你的意思是这对于负价格而言是不起作用的,而不是负回报。负价格(或利率)需要对基础过程进行不同的假设,具体来说是正常波动 https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2687742 - mcguip
不,我的意思是它不能处理负回报。log(x < 0)是未定义的。正如我发布的链接所描述的那样,您必须执行log(p1 / p0),即当r趋近于零时约为log(1 + r)。 - Carl
var() default is ddof=0, so you could probably just replace the first example with var() and second with var(ddof=1) - Baczek
1
被点赞了,感觉是这里最好/最正确和完整的答案。 - Kris
1
@carl 我认为这个公式对负回报也是有效的。该公式并不是取差值的对数,而是取价格的对数的差值。因此,如果价格为正数,则该公式可以很好地工作。如果价格可能为负数,那么使用对数收益率的直觉并不是一个好主意,因为使用它的背景假设价格不会变为负数,所以当你接近0时,收益率会变小。对于可能具有负价格的工具,这种直觉可能是错误的,因此您不应该使用对数收益率。 - Kris
显示剩余4条评论

7

这里是一种使用NumPy的方法-

# From https://dev59.com/IWYq5IYBdhLWcg3wpyNE#14314054 by @Jaime
def moving_average(a, n=3) :
    ret = np.cumsum(a, dtype=float)
    ret[n:] = ret[n:] - ret[:-n]
    return ret[n - 1:] / n

# From https://dev59.com/7FkS5IYBdhLWcg3wSk3b#40085052
def strided_app(a, L, S=1 ):  # Window len = L, Stride len/stepsize = S
    nrows = ((a.size-L)//S)+1
    n = a.strides[0]
    return np.lib.stride_tricks.as_strided(a, shape=(nrows,L), strides=(S*n,n))

def rolling_meansqdiff_numpy(a, w):
    A = strided_app(a, w)
    B = moving_average(a,w)
    subs = A-B[:,None]
    sums = np.einsum('ij,ij->i',subs,subs)
    return (sums/w)**0.5

示例运行 -

In [202]: Ser = pd.Series(np.random.randint(0,9,(20)))

In [203]: rolling_meansqdiff_loopy(Ser, w=10)
Out[203]: 
[2.6095976701399777,
 2.3000000000000003,
 2.118962010041709,
 2.022374841615669,
 1.746424919657298,
 1.7916472867168918,
 1.3000000000000003,
 1.7776388834631178,
 1.6852299546352716,
 1.6881943016134133,
 1.7578395831246945]

In [204]: rolling_meansqdiff_numpy(Ser.values, w=10)
Out[204]: 
array([ 2.60959767,  2.3       ,  2.11896201,  2.02237484,  1.74642492,
        1.79164729,  1.3       ,  1.77763888,  1.68522995,  1.6881943 ,
        1.75783958])

运行时测试

循环方法 -

def rolling_meansqdiff_loopy(Ser, w):
    length = Ser.shape[0]- w + 1
    volList= []
    for timestep in range(length):
        subSer=Ser[timestep:timestep+w]
        mean_i=np.mean(subSer)
        vol_i=(np.sum((subSer-mean_i)**2)/len(subSer))**0.5
        volList.append(vol_i)
    return volList

时间 -

In [223]: Ser = pd.Series(np.random.randint(0,9,(10000)))

In [224]: %timeit rolling_meansqdiff_loopy(Ser, w=10)
1 loops, best of 3: 2.63 s per loop

# @Mad Physicist's vectorized soln
In [225]: %timeit Ser.rolling(10).std(ddof=0)
1000 loops, best of 3: 380 µs per loop

In [226]: %timeit rolling_meansqdiff_numpy(Ser.values, w=10)
1000 loops, best of 3: 393 µs per loop

这两种向量化方法相对于循环方法的加速比接近7000倍!


我选择了Mad Physicists的解决方案。感谢您花费时间。 - Thegamer23
1
从技术上讲,它的速度快了约5%,但这实际上有点令人惊讶,因为我不希望Pandas中的任何东西能够超过类似的numpy解决方案。然而,事实就是这样。 - Mad Physicist
也许是Python循环做的。 - Mad Physicist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接