直接用np.cumprod(1 + returns)是最简方案,因其精确对应复利连乘本质:(1+r₁)×(1+r₂)×…×(1+rₙ),输出逐日累积增值因子,减1即得累计收益率。

为什么直接用 np.cumprod(1 + returns) 是最简方案
金融资产的累积收益率本质是复利连乘:每天收益叠加在前一日本金上,数学形式就是 (1+r₁)×(1+r₂)×…×(1+rₙ)。NumPy 的 np.cumprod 正好计算累计乘积,所以对日收益率数组 returns,np.cumprod(1 + returns) 直接产出从起点开始的逐日累积增值因子(即每单位初始资金在各时点的价值)。
常见错误是误用 np.cumsum——那是算简单累加,不反映复利;或者漏加 1,把收益率当增长因子用,导致结果整体偏小一个数量级。
- 输入
returns必须是浮点数数组,不能含NaN或无穷值,否则np.cumprod会传播nan - 若需从 1 开始(即首日为基准=1.0),确保
returns[0]是第 1 天相对于第 0 天的收益率,而非第 0 天自身 - 输出是增值因子序列,要转成收益率需减 1:
np.cumprod(1 + returns) - 1
处理缺失值和非交易日的三种典型场景
真实金融数据常有停牌、休市或数据缺失,np.cumprod 对 NaN 零容忍:一旦遇到就让后续全变 nan。必须提前干预。
- 用
pd.Series(returns).fillna(0).values将缺失日视作零收益(最常用,符合“无交易则无变动”逻辑) - 用前向填充:
pd.Series(returns).ffill().values,适用于短期停牌但你假设价格不变 - 严格剔除:
returns = returns[~np.isnan(returns)],但会破坏时间对齐,仅适用于纯计算、不要求日期索引的场景
注意:别用 np.nan_to_num(returns, nan=0) 直接替换——它会把 inf 也强行转 0,而极端收益率(如涨跌停)可能产生 inf,应先检查分布。
立即学习“Python免费学习笔记(深入)”;
np.cumprod 和手动循环/累计乘积的性能差异有多大
在万级长度以上,np.cumprod 比 Python 循环快 50–100 倍,且内存连续、无解释器开销。但它不是“绝对最优”:
- 若只关心最终累积值(而非全过程),用
np.prod(1 + returns)更快,少存中间数组 - 若需滚动窗口累积(如近 250 天复利),
np.cumprod不适用,得用scipy.ndimage.uniform_filter1d配合对数变换,或改用pandas.Series.rolling().apply(lambda x: np.prod(1+x)) - 对超长序列(千万级),
np.cumprod可能因浮点误差累积导致末尾精度漂移,此时应分段计算并拼接
和 pandas 的 cumprod 方法有什么关键区别
NumPy 版本返回纯 ndarray,pandas 版本(Series.cumprod())保留索引、自动对齐、支持 skipna 参数,默认行为更“金融友好”。但底层都调用相同 C 实现,性能几乎一致。
- 用 NumPy:适合已清洗好、无索引依赖的批处理,比如回测引擎内核
- 用 pandas:适合带日期索引的分析流程,
df['ret'].cumprod()直接继承df.index,省去对齐麻烦 - 混用风险:
np.cumprod(df['ret'].values)丢弃索引;反过来pd.Series(np_array).cumprod()又多一次包装开销
真正容易被忽略的是起始点语义:np.cumprod([1.01, 1.02]) 输出 [1.01, 1.0302],代表第 1 天末和第 2 天末的值;但很多人期望首元素是 1.0(初始状态)。这需要手动 prepend:np.concatenate([[1.0], np.cumprod(1 + returns)])——这个细节在画净值曲线时错一位,整条线就偏了。


















