因子投资-实验01-从价格到收益率

QUANT RESEARCH NOTE LAB 01

给定一列按交易日排序的价格,怎样得到简单收益、对数收益、累计净值和年化波动率?这个实验只用 6 个确定性价格,不依赖网络或数据库,目的是把第 01、02 讲的公式变成一条最小可审计的数据管道。

类型动手实验
模块研究实验
阅读约 8 分钟
材料9 节 · 4 段代码

问题

给定一列按交易日排序的价格,怎样得到简单收益、对数收益、累计净值和年化波动率?这个实验只用 6 个确定性价格,不依赖网络或数据库,目的是把第 01、02 讲的公式变成一条最小可审计的数据管道。

完成后你应当能解释每一行为什么出现缺失值、为什么最终净值是 1.06、为什么简单收益与对数收益不能逐项相等,以及为什么 5 个日收益估出的年化波动率几乎没有决策价值。

输入数据

假设价格已经按日期升序、口径一致并代表总回报价格:

输入数据 6 行 × 2 列
date price
2026-01-05 100.0
2026-01-06 102.0
2026-01-07 101.0
2026-01-08 104.0
2026-01-09 103.0
2026-01-12 106.0

第一行没有前一期价格,所以一期收益必须是缺失,而不是 0。只有在计算“从 1 开始的净值”时,才把这一行的增长因子设为 1。

逐步实现

第一步:建立有日期索引的表

PYTHON 可复现实验
1
2
3
4
5
6
# kb-skip
dates = pd.to_datetime([
"2026-01-05", "2026-01-06", "2026-01-07",
"2026-01-08", "2026-01-09", "2026-01-12",
])
data = pd.DataFrame({"price": prices.to_numpy()}, index=dates)

日期不是装饰。真实研究必须先排序、去重、校验交易日,再计算相邻变化;否则 pct_change 会忠实地计算错误顺序。

第二步:计算两类收益

简单收益直接用当前价除以前价减一;对数收益用价格对数的一阶差分:

PYTHON 可复现实验
1
2
3
# kb-skip
data["simple"] = data["price"].pct_change(fill_method=None)
data["log_return"] = np.log(data["price"]).diff()

对每个非缺失观测,都应满足 exp(log_return)-1=simple。这个恒等式是检查收益实现是否一致的好断言。

第三步:累计净值与年化波动

PYTHON 可复现实验
1
2
3
# kb-skip
data["wealth"] = (1 + data["simple"].fillna(0)).cumprod()
annual_volatility = data["simple"].dropna().std(ddof=1) * np.sqrt(252)

净值使用简单收益连乘。日波动乘根号 252 只是在日收益独立、分布相对稳定时的尺度近似;本实验只有 5 个收益观测,数值只用于验证公式。

完整代码

复制下面代码即可独立运行。完整程序不读取或写入本地文件。

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
import numpy as np
import pandas as pd

dates = pd.to_datetime([
"2026-01-05", "2026-01-06", "2026-01-07",
"2026-01-08", "2026-01-09", "2026-01-12",
])
prices = pd.Series([100.0, 102.0, 101.0, 104.0, 103.0, 106.0])

data = pd.DataFrame({"price": prices.to_numpy()}, index=dates)
simple = prices.pct_change(fill_method=None)
log_return = np.log(prices).diff()
wealth = (1 + simple.fillna(0)).cumprod()
annual_volatility = simple.dropna().std(ddof=1) * np.sqrt(252)

data["simple"] = simple.to_numpy()
data["log_return"] = log_return.to_numpy()
data["wealth"] = wealth.to_numpy()

assert np.isclose(wealth.iloc[-1], 1.06)
assert np.allclose(
np.exp(log_return.dropna()).values - 1,
simple.dropna().values,
)
assert data.index.is_monotonic_increasing
assert data.index.is_unique

print(data.round(6).to_string())
print(f"annual_volatility={annual_volatility:.6f}")

预期输出

核心数值应为:

预期输出 6 行 × 4 列
date simple log_return wealth
2026-01-05 缺失 缺失 1.00
2026-01-06 0.020000 0.019803 1.02
2026-01-07 -0.009804 -0.009852 1.01
2026-01-08 0.029703 0.029270 1.04
2026-01-09 -0.009615 -0.009662 1.03
2026-01-12 0.029126 0.028710 1.06

年化波动率约为 0.318804,即 31.88%。它很高的一部分原因是样本只有 5 个日收益;不要把教学输出写成对未来波动的估计。

断言

实验用四类断言保护研究口径:

  1. 财富恒等式: 最终价格 106 相对初始 100,因此最终净值必须为 1.06。
  2. 收益变换恒等式: 每一期 exp(r)-1 必须等于简单收益 R。
  3. 时间顺序: 日期索引必须单调递增。
  4. 唯一性: 每个日期只能出现一次;真实个股面板还应断言 date、asset 联合唯一。

断言不是为了证明研究结论,而是尽早阻止数据管道静默地产生错误结果。真实项目还应检查价格为正、复权因子可用、异常跳变对应公司行动,以及股票代码没有被错误复用。

结果解释

价格从 100 到 106,所以累计总回报是 6%。中间有两次下跌,净值路径不是直线。简单收益与对数收益在幅度小时接近,但正收益时对数值略小,负收益时对数值更负;经过 exp(r)-1 后两者完全一致。

第一行收益保留缺失,是因为“没有上期数据”和“收益为零”具有不同含义。计算净值时将第一行缺失填 0,只是人为规定初始增长因子为 1,不应反向污染用于估计均值、波动或回归的收益样本。

年化波动使用样本标准差和根号 252。它没有考虑自相关、周末跨期、涨跌停或条件波动,也没有置信区间。正确的结论是“代码按这个近似得到 31.88%”,而不是“资产真实年风险为 31.88%”。

常见故障

  • pct_change 警告或隐式填充: 显式指定 fill_method=None,避免缺失价格被自动前向填充后制造零收益。
  • 日期未排序: 先排序再计算;排序后检查是否有重复日期,而不是让后出现的行覆盖前一行。
  • 复权与分红重复: 若 price 已是总回报复权价,不再单独加入分红。
  • 对数价格非正: 正常股价应大于零;出现零或负数通常是缺失编码、单位或数据错误,应停止而非强行取对数。
  • 首行填零过早: 只在净值连乘时填零;统计估计必须丢弃没有定义的第一期收益。
  • 根号年化滥用: 交易日数量、收益自相关和样本长度需要单独报告。
  • 浮点精度: 不用 simple == exact 做严格相等判断,应使用 np.isclose 或 np.allclose。

练习

  1. 把最后价格改为 95,重新计算累计收益、最大回撤和年化波动。
  2. 在输入中复制一个日期,编写断言让程序立刻失败。
  3. 插入一个缺失价格,比较 fill_method=None 与先前向填充的结果,并解释哪一步可以用于持仓估值、哪一步不能代表成交。
  4. 新增一列每股现金分红,使用未复权价格手工构造总回报;再与供应商复权价收益交叉核对。
  5. 将频率改为月度,把年化因子从 252 改为 12;解释为什么这只是单位变换而非信息增加。

完成练习后,把数据表扩展为 date、asset、price 三列的长表。后续实验都将沿用“日期与资产联合唯一、信号先于未来收益”的结构。


因子投资-实验01-从价格到收益率
https://www.polarishaw.top/2026/07/17/因子投资-实验01-从价格到收益率/
作者
Polaris
发布于
2026年7月17日
许可协议