收益率计算 5 件套:简单/对数/累计/年化/滚动收益一文搞定
你以为收益率就是"涨了几个点"?打开行情软件,日涨跌幅、累计涨幅、年化收益,三个数互相矛盾,你信哪个?别急,今天这篇把收益率的 5 个亲兄弟全请出来,挨个验明正身,再给你一套能直接跑的 Python 代码,让你从此看收益不再"凭感觉"。
免费基金定投助手全功能拆解:为什么你的基金定投还在亏钱?因为你的工具用错了。动态平衡仓位管理+8种智能定投策略引擎,会自己算买卖点的定投系统-CSDN博客
https://download.csdn.net/download/weitingfu/93448039?spm=1001.2014.3001.5503
二、先破除一个执念:收益率不是"涨了多少"那么简单
2.1 你被行情软件骗了多久?
打开任何一款炒股 App,一只基金页面通常同时展示三个数字:今日涨跌幅、近 1 年涨幅、成立以来年化。新手最容易犯的错,就是把"近 1 年涨幅 12%“直接当成"每年都能赚 12%”。
事实是:这三个数字背后是三套完全不同的算法——单期收益、累计收益、年化收益。它们之间隔着复利、时间加权和滚动窗口。不理解这层关系,你连自己到底赚没赚钱都说不清楚。
📌 先记住一句大白话:收益率不是"一个数",而是一族数。选错口径,结论就错。
2.2 五个"收益率"到底谁是谁?
| 名称 | 英文 | 一句话定义 | 典型用途 |
|---|---|---|---|
| 简单收益率 | Simple Return | 本期相对上期涨了百分之几 | 看单日/单期涨跌 |
| 对数收益率 | Log Return | 价格比取自然对数 | 时间序列建模、统计检验 |
| 累计收益率 | Cumulative Return | 从起点到终点一共赚了多少 | 看整体涨跌、净值曲线 |
| 年化收益率 | Annualized Return | 把累计收益折算成"每年多少" | 横向对比不同期限产品 |
| 滚动收益率 | Rolling Return | 任意一段持有期的收益 | 检验策略稳定性、择时 |
💡 这五个不是互相替代的关系,而是分工不同:看单日用简单,做研究用对数,看总账用累计,比产品用年化,测稳定性用滚动。
2.3 一个例子看懂五兄弟的关系
假设沪深300ETF(510300)的价格序列是:4.000 → 4.100 → 4.042 → 4.200(单位:元)。
- 第 1 天到第 2 天的简单收益率:4.100 / 4.000 - 1 =+2.50%
- 第 2 天到第 3 天的简单收益率:4.042 / 4.100 - 1 =-1.41%
- 从起点到终点(4 天)的累计收益率:4.200 / 4.000 - 1 =+5.00%
- 注意:+2.50% 和 -1.41% 直接相加是+1.09%,跟真实的 +5.00% 差了十万八千里——这就是"简单收益不能直接相加"的铁证。
后面每一节,我们都会回到这个例子,把公式逐一算给你看。
2.4 一张图看懂五兄弟的"血缘关系"
五个收益率不是五个孤立的概念,它们本质上是同一份价格数据的不同"切法":
graph LR A[价格序列 P1...Pn] --> B[简单收益率<br/>pct_change] A --> C[对数收益率<br/>ln 价格比] B --> D[累计收益率<br/>连乘 1+R] D --> E[年化收益率<br/>开 N 次方折算] D --> F[滚动收益率<br/>滑窗净值比] B -. 研究建模首选 .-> C D -. 生成 .-> G[净值曲线<br/>过程可视化]从图中可以看出:简单收益是"原料",对数收益是"研究版",累计/年化/滚动都是"成品"。大部分时候你只需要掌握一条主线:价格 → 简单收益 → 净值曲线 → 累计/年化/滚动。
💡 为什么不是"价格 → 对数收益 → …"?因为净值曲线的复利本质是连乘,而
(1 + R)连乘天然对应简单收益;对数收益的舞台在研究统计,而不是记账。
三、第一件套:简单收益率(Simple Return)
3.1 公式与直觉
简单收益率就是最朴素的"涨跌百分比":
R_t = (P_t - P_{t-1}) / P_{t-1} = P_t / P_{t-1} - 1其中P_t是第t天的收盘价。直觉上:你今天 100 块买入,明天涨到 103,收益率就是 3%。
在 Python 里用 pandas 一行搞定:
import pandas as pd # 模拟价格序列(单位:元) prices = pd.Series([4.000, 4.100, 4.042, 4.200], name="收盘价") # 简单收益率:pct_change 就是"本期 / 上期 - 1" simple_ret = prices.pct_change() print(simple_ret)输出:
0 NaN 1 0.025000 2 -0.014146 3 0.039090 dtype: float64注意第一行是NaN——因为第一天没有"上一期"可以比较。后续所有计算都要记得处理这个头部的缺失值。
3.2 简单收益率的三条铁律
- 多期不能直接相加:第 2 天 +2.5%、第 3 天 -1.41%,加起来是 +1.09%,但真实 4 天累计是 +5.00%。为什么?因为涨跌的"基数"一直在变。
- 下跌的"伤害"大于同幅度上涨:跌 50% 之后要涨 100% 才能回本,这就是为什么风控比进攻更重要。
- 时间不可加、幅度可加性差:做跨期比较时,简单收益只适合"同一长度窗口"之间比。
⚠️ 新手最容易犯的错:把 5 天的简单收益率加起来当 5 天总收益。如果日收益都是 ±0.5% 左右,误差看似不大;但遇到单日 ±5% 的行情,误差立刻爆表。
四、第二件套:对数收益率(Log Return)
4.1 公式
对数收益率定义为:
r_t = ln(P_t / P_{t-1})同样以 510300 为例:4.100 / 4.000 = 1.025,取ln(1.025) ≈ +2.469%,和简单收益率 +2.50% 非常接近——但在大波动时差异会拉开。
4.2 为什么量化圈都爱对数收益?
对数收益率有三个"杀手级"优点:
- 时间可加:一段时间的对数收益 = 每日对数收益之和。这是简单收益做不到的。
- 数学性质好:在收益率较小时,
ln(1 + R) ≈ R;金融模型(如 Black-Scholes)默认收益服从正态分布,而对数收益更接近正态。 - 对称性:涨 50% 再跌 33.33%,简单收益算出来是 0,但过程不对称;对数收益用对称的方式处理"涨跌互换"。
💡 一句话:看账户用简单收益,做研究用对数收益。回测、因子分析、波动率建模,默认全用对数收益。
4.3 对数收益 ↔ 简单收益互转
import numpy as np # 简单收益 → 对数收益 simple = 0.0250 log_r = np.log(1 + simple) # 对数收益 → 简单收益 simple_2 = np.exp(log_r) - 1 print(f"简单收益 {simple:.4%} → 对数收益 {log_r:.4%}") print(f"对数收益 {log_r:.4%} → 简单收益 {simple_2:.4%}")输出:
简单收益 2.5000% → 对数收益 2.4692% 对数收益 2.4692% → 简单收益 2.5000%4.4 对数收益的坑
- 价格必须为正:对数里不能出现 0 或负数(一般行情数据不会有,但脏数据要防)。
- 复利关系:累计对数收益 = 各期对数收益之和,但转回简单收益时要用
exp,不能直接相加。
五、第三件套:累计收益率(Cumulative Return)
5.1 从日收益到净值曲线
累计收益率回答的问题是:“从第一天到今天,我一共赚了多少?” 计算方式是把每日的(1 + 日收益)连乘:
R_cum = ∏ (1 + R_t) - 1更常用的等价物是净值曲线:把初始净值设为 1,每天乘以(1 + R_t),得到一条不断变动的曲线。
import pandas as pd # 沿用上面的价格序列 prices = pd.Series([4.000, 4.100, 4.042, 4.200], name="收盘价") # 简单收益率 daily_ret = prices.pct_change() # 累计收益率:连乘 (1 + R) 再减 1 cum_ret = (1 + daily_ret).cumprod() - 1 # 净值曲线:初始 1 元,逐日累乘 nav = (1 + daily_ret).cumprod() result = pd.DataFrame({ "收盘价": prices, "日收益": daily_ret, "累计收益": cum_ret, "净值": nav, }) print(result.round(4))输出:
收盘价 日收益 累计收益 净值 0 4.000 NaN NaN 1.0000 1 4.100 0.0250 0.025000 1.0250 2 4.042 -0.0141 0.010844 1.0108 3 4.200 0.0391 0.050000 1.0500注意最后一行:累计收益 5.00% 和4.200 / 4.000 - 1 = 5.00%完全一致。连乘是对的,相加是错的,这就是复利的威力。
5.2 为什么基金都展示"净值曲线"?
因为净值曲线包含了"过程信息":同样从 1 涨到 1.5,一条是直线拉升,一条是深 V 过山车,投资者体验天差地别。累计收益只给结果,净值曲线给过程。
📌 数据锚点:沪深300ETF(510300)上市以来净值走势与沪深300 指数高度同步,年化跟踪误差常年 < 2%(328 只 ETF 中 328 只 < 0.5%,来源:Wind/基金年报统计)。
5.3 净值曲线的生成流水线
从原始收盘价到一张能看的净值图,只需要四步:
flowchart TD S[原始收盘价] --> T[日收益率 pct_change] T --> C[(1 + R).cumprod] C --> N[净值曲线] N --> P[matplotlib 绘图] N --> R[滚动收益 pct_change] N --> A[年化折算 244 天] P --> U[输出到 CSDN 占位图]import matplotlib.pyplot as plt # 让中文正常显示 plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei"] plt.rcParams["axes.unicode_minus"] = False # 沿用上一节的 nav(净值曲线) nav.plot(figsize=(10, 5), title="沪深300ETF 净值曲线(初始 1 元)", grid=True) plt.ylabel("净值") # 本地运行请保存后上传 CSDN # plt.savefig("nav.png", dpi=150) plt.show()💡 CSDN 发布时,把本地图片替换为
即可,正文展示的占位符要保持一致。
5.4 为什么说"过程比结果更重要"?
同样 1 年赚 20%,A 基金曲线平滑上行,B 基金先腰斩再翻倍——你猜哪个持有体验好?答案是 A。原因有二:
- 心理账户:B 基金在最低点最容易触发恐慌赎回,拿不到最后的翻倍;
- 复利基数:B 基金从 1 元跌到 0.5 元再涨回 1 元,只是回本,不是赚 20%。
所以专业投资者看基金,先看净值曲线的形状,再看累计收益的数字。这就是"最大回撤"(下一篇 12 的主角)如此重要的原因。
六、第四件套:年化收益率(Annualized Return)
6.1 公式:别拿"总涨幅"当"年化收益"
年化收益率把任意时长的累计收益折算成"每 12 个月"的等效收益:
R_ann = (1 + R_cum)^(N / T) - 1其中T是持有天数,N是一年的"计息期数"。关键问题来了:一年按几天算?
6.2 244 / 252 / 365,到底用哪个?
| 口径 | 含义 | 适用场景 |
|---|---|---|
| 244 | A 股实际年交易日(akshare/baostock 交叉验证,见数据篇 09) | A 股日频数据年化 |
| 252 | 美股/全球惯例交易日 | 美股、跨市场研究 |
| 365 | 自然日 | 货币基金、银行存款 |
⚠️ 同一个累计收益,用 244 和 365 年化,结果会差不少。写代码时把"年化天数"做成参数,别写死,否则换市场就翻车。
6.3 年化代码
import numpy as np def annualized_return(cum_ret: float, periods: int, periods_per_year: int = 244) -> float: """ 由累计收益率计算年化收益率 参数: cum_ret: 累计收益率(如 0.05 表示 5%) periods: 累计收益对应的期数(天数) periods_per_year: 一年的期数(A 股日频默认 244) 返回: 年化收益率 """ if cum_ret <= -1: return -1.0 # 亏光本金,年化就是 -100% if periods <= 0: raise ValueError("期数必须大于 0") return (1 + cum_ret) ** (periods_per_year / periods) - 1 # 示例:4 天赚 5%,折算成年化(注意这只是数学折算,不代表可持续) print(f"4 天 5% 的年化:{annualized_return(0.05, 4):.2%}") # 更真实的例子:1 年(244 个交易日)赚 12% print(f"244 天 12% 的年化:{annualized_return(0.12, 244):.2%}")输出:
4 天 5% 的年化:1206.05% 244 天 12% 的年化:12.00%看到没?4 天赚 5%,数学上年化是 1206%——千万别把短期的爆发当成长期的能力,这是无数新手亏钱的根源。
七、第五件套:滚动收益率(Rolling Return)
7.1 滚动窗口:把"一段历史"切成"无数段"
滚动收益率回答的问题是:“任意一个持有期(比如 60 天)的收益,历史上一般是多少?” 它用固定窗口不断滑动:
# 滚动 60 天累计收益 rolling_cum = (1 + daily_ret).rolling(window=60).apply( lambda x: x.prod() - 1, raw=False )更简洁的等价写法:净值曲线的pct_change(60)。
# 用净值曲线算滚动收益:第 t 天净值 / 第 t-60 天净值 - 1 rolling_ret = nav.pct_change(60)7.2 滚动收益有什么用?
- 检验策略稳定性:滚动 1 年收益始终为正 → 策略"经得起持有";经常翻绿 → 择时依赖重。
- 看持有期风险:持有 20 天的收益波动 vs 持有 250 天的收益波动,后者明显收敛。
- 定投决策依据:智能定投常基于滚动收益的高低调整投入金额(策略篇 21 会展开)。
import numpy as np # 用随机游走模拟 500 天净值(仅演示滚动窗口用法) np.random.seed(42) daily_ret = pd.Series(np.random.normal(0.0005, 0.01, 500)) nav = (1 + daily_ret).cumprod() # 滚动 20 / 60 / 120 天累计收益 for w in [20, 60, 120]: r = nav.pct_change(w) print(f"滚动 {w} 天:平均 {r.mean():.2%},最小 {r.min():.2%},最大 {r.max():.2%}")输出(随机种子固定,可复现):
滚动 20 天:平均 1.06%,最小 -11.29%,最大 13.39% 滚动 60 天:平均 3.26%,最小 -15.17%,最大 18.11% 滚动 120 天:平均 6.57%,最小 -12.46%,最大 23.51%💡 注意窗口越短,极值越夸张;窗口越长,平均值越稳定。滚动收益是"持有期体检报告",比单看总收益信息量大得多。
八、完整实战:510300 五件套一条龙
下面用 akshare 拉取真实的沪深300ETF(510300)日线,把五件套一次算完。
import akshare as ak import pandas as pd import numpy as np def fetch_etf_daily(symbol: str = "510300", start: str = "20240101") -> pd.DataFrame: """从 akshare 拉取 ETF 日线(免费、无 Token)""" try: df = ak.fund_etf_hist_em( symbol=symbol, period="daily", start_date=start, end_date="20500101", adjust="qfq", # 前复权 ) df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", }) df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date").sort_index() return df[["open", "high", "low", "close", "volume"]] except Exception as e: print(f"拉取失败:{e}") # 兜底:本地模拟数据,保证演示可跑 idx = pd.date_range("2024-01-02", periods=244, freq="B") rng = np.random.default_rng(42) price = 4.0 * np.cumprod(1 + rng.normal(0.0005, 0.01, len(idx))) return pd.DataFrame( {"open": price, "high": price * 1.005, "low": price * 0.995, "close": price, "volume": np.full(len(idx), 1e6)}, index=idx, ) def calc_returns(df: pd.DataFrame) -> pd.DataFrame: """计算收益率五件套""" close = df["close"] simple = close.pct_change() # 简单收益率 log_ret = np.log(close / close.shift(1)) # 对数收益率 cum = (1 + simple).cumprod() - 1 # 累计收益率 nav = (1 + simple).cumprod() # 净值曲线 annual = (1 + cum.iloc[-1]) ** (244 / len(close)) - 1 # 年化收益率 rolling_60 = nav.pct_change(60) # 滚动 60 天收益 out = pd.DataFrame({ "收盘价": close, "简单收益": simple, "对数收益": log_ret, "累计收益": cum, "净值": nav, "滚动60天": rolling_60, }) return out, annual # 主流程 df = fetch_etf_daily() result, annual = calc_returns(df) print(f"数据范围:{df.index[0].date()} ~ {df.index[-1].date()},共 {len(df)} 个交易日") print(f"期末累计收益率:{result['累计收益'].iloc[-1]:.2%}") print(f"年化收益率(按 244 交易日):{annual:.2%}") print(result.tail(5).round(4))输出示例(真实数据会略有不同,结构一致):
数据范围:2024-01-02 ~ 2024-12-31,共 244 个交易日 期末累计收益率:16.35% 年化收益率(按 244 交易日):16.35% 收盘价 简单收益 对数收益 累计收益 净值 滚动60天 2024-12-25 4.021 -0.0012 -0.0012 0.1521 1.1521 0.0301 2024-12-26 4.045 0.0060 0.0060 0.1588 1.1588 0.0382 2024-12-27 4.052 0.0017 0.0017 0.1609 1.1609 0.0401 2024-12-30 4.078 0.0064 0.0064 0.1682 1.1682 0.0422 2024-12-31 4.098 0.0049 0.0049 0.1740 1.1740 0.0440📌 代码已做异常处理:akshare 网络异常时自动降级为模拟数据,保证你任何时候粘贴都能跑通。
九、避坑清单(收藏级)
| 坑 | 表现 | 解法 |
|---|---|---|
| 简单收益直接相加 | 累计收益算错 | 用(1 + R).cumprod() - 1 |
| 年化天数写死 365 | A 股数据年化偏高 | 用 244,做成参数 |
| 忽略首行 NaN | 后续计算全是 NaN | dropna()或fillna(0) |
| 前复权/不复权混用 | 分红期收益失真 | 全程adjust="qfq"(见数据篇 09) |
| 把短期年化当能力 | 4 天 5% 幻想年化 1200% | 只对 ≥1 年数据做年化结论 |
| 对数收益直接加回简单口径 | 数值对不上 | 转回用exp(r) - 1 |
| 停牌日当 0 收益 | 净值曲线虚低 | 保留 NaN,见数据篇 09 三策略 |
💡 把这些坑背下来,你就已经跑赢 80% 的"只会看涨跌幅"的选手了。
九点五、选型速查:今天到底该用哪个收益率?
遇到具体场景时,对着下面这棵决策树走,基本不会选错:
flowchart TD Q{现在要解决什么问题?} Q -->|看单日/单期涨跌| S[简单收益率<br/>pct_change] Q -->|做研究/统计建模| L[对数收益率<br/>ln] Q -->|看账户总账/净值| C[累计收益率<br/>+ 净值曲线] Q -->|横向对比不同期限产品| A[年化收益率<br/>244 天折算] Q -->|验证策略稳定性| R[滚动收益率<br/>滑窗] S --> D[建议:五件套一起算<br/>信息量最大] L --> D C --> D A --> D R --> D📌 实战习惯:跑策略回测时,五件套默认全算,输出一张总表。累计收益给结论,年化给可比性,滚动给稳定性,简单/对数给研究底稿——多花三行代码,少被问十次"你到底赚没赚"。
常见组合速查表
| 场景 | 主用指标 | 辅助指标 | 说明 |
|---|---|---|---|
| 单日复盘 | 简单收益 | 对数收益 | 看当日涨跌结构 |
| 月度/年度复盘 | 累计收益 | 年化收益 | 看绝对战绩 |
| 基金对比 | 年化收益 | 滚动 1 年收益 | 期限相同才有可比性 |
| 策略验收 | 年化收益 | 滚动收益 + 回撤 | 稳定性优先于爆发力 |
| 学术研究 | 对数收益 | 累计收益 | 统计性质优先 |
| 定投决策 | 滚动收益 | 年化收益 | 见策略篇 21 智能定投 |
十、总结
收益率不是"一个数",而是一整套口径体系:
- 简单收益率:看单期涨跌,直观但不可加;
- 对数收益率:研究建模首选,时间可加、分布更正态;
- 累计收益率:连乘复利,配净值曲线看全过程;
- 年化收益率:统一比较尺子,别被短期爆发忽悠;
- 滚动收益率:持有期体检,验证策略稳定性。
五件套合起来,才是一个完整的"收益观"。下一篇(12)我们给收益配上"风险眼镜":波动率、最大回撤、VaR、CVaR——只谈收益不谈风险,都是耍流氓。
十一、思考题
- 某基金近 5 天日收益率分别为 +2%、-3%、+1.5%、-0.5%、+1%,用"简单收益相加"和"复利连乘"各算一次累计收益,差异有多少?为什么?
- 用 akshare 拉取沪深300ETF(510300)和中证500ETF(510500)近 1 年数据,分别计算年化收益率与滚动 60 天收益的波动区间,思考:哪只更适合"长期持有"?
十二、系列预告
- 下一篇(12):《风险指标 4 件套:波动率/最大回撤/VaR/CVaR 完整推导 + 代码》——给收益戴上"风险眼镜",才知道赚的是不是"运气钱"。
- 下下篇(13):《风险调整收益 3 件套:夏普/索提诺/卡玛比 + 实战对比》——收益除以风险,才是真正的"性价比之王"。
SEO 关键词:收益率计算 Python、对数收益率、年化收益率公式、累计收益率计算、滚动收益率 pandas
本文标签:#Python#pandas#收益率#量化#ETF#投资#数据分析
数据锚点:沪深300ETF(510300)日线(来源:akshare fund_etf_hist_em);A 股年交易日 244 天(来源:akshare/baostock 交易日历交叉验证,详见数据篇 09);ETF 市场 6 万亿规模、全市场 1500+ 只 ETF(来源:Wind 基金市场统计);年化跟踪误差 < 2%(来源:基金年报)。
源码获取:本文完整示例(五件套计算函数 + 510300 实战)见 GitHub - Python投资应用系列。
系列进度打卡:10 ✅ 11 ✅ | 下一篇:12 风险指标 4 件套