1. 这节究竟解决什么实际问题?¶
1.1 价格与收益率 的具体任务是:区分价格变化、简单收益率和对数收益率,并知道何时可以相互近似。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
会使用百分比和基础代数;建议先完成环境配置。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
示例优先使用仓库固定的日频复权收盘价;它不是实时行情,也不代表完整市场。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 1.1 价格与收益率 的输入、计算步骤、输出和局限;尤其能说明:区分价格变化、简单收益率和对数收益率,并知道何时可以相互近似。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
价格表示资产在特定时点的水平,收益率表示两个时点之间的相对变化。多数跨资产比较使用收益率。
所有公式都先说明分子、分母、单位与时间尺度;年化前必须确认数据频率。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
NumPy/Pandas 帮你算数和对齐索引;统计库帮你估计参数,但不替你解释经济含义。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“区分价格变化、简单收益率和对数收益率,并知道何时可以相互近似。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
把百分比和小数混用、忘记 dropna、把日频波动率直接叫作年化波动率、把相关性当因果。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
概念演示也要避免事后选择样本或只展示支持结论的时间段。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/01_financial_concepts/01_price_and_return_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/01_financial_concepts/01_price_and_return_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:Investments(Bodie, Kane, Marcus)。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 02_risk_and_volatility。本节的概念会成为理解它的输入,而不是孤立的名词。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
print('导入完成 ')
导入完成
# 默认使用固定样本,保证课程与 CI 不依赖实时接口。
USE_OFFLINE_DATA = True
if USE_OFFLINE_DATA:
from pathlib import Path
import sys
docs_root = next(parent for parent in [Path.cwd(), *Path.cwd().parents]
if (parent / 'utils' / 'offline_data.py').exists())
sys.path.insert(0, str(docs_root))
from utils.offline_data import configure_yfinance_offline
configure_yfinance_offline(yf)
else:
print('在线下载为扩展步骤;请核对数据字段、时区和下载日期。')
1. 下载真实数据¶
我们用苹果公司(AAPL)的历史数据来练手。下载下来的每行数据代表一天,包含:
- Open:开盘价(当天第一笔交易的价格)
- High / Low:当天最高 / 最低价
- Close:收盘价(当天最后一笔交易的价格)—— 我们最常用这个
- Volume:当天成交量(交易了多少股)
# 下载苹果公司 (AAPL) 近两年日线数据
ticker = 'AAPL'
data = yf.download(ticker, start='2022-01-01', end='2024-01-01', progress=False)
price = data['Close'].squeeze() # 收盘价
print(f'数据形状: {data.shape}')
print(f'时间范围: {price.index[0].date()} → {price.index[-1].date()}')
data[['Open', 'High', 'Low', 'Close', 'Volume']].tail()
数据形状: (501, 5) 时间范围: 2022-01-03 → 2023-12-29
| Price | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| Ticker | AAPL | AAPL | AAPL | AAPL | AAPL |
| Date | |||||
| 2023-12-22 | 193.173224 | 193.400870 | 190.985954 | 191.609482 | 37149600 |
| 2023-12-26 | 191.619364 | 191.896484 | 190.847385 | 191.065125 | 28919300 |
| 2023-12-27 | 190.510860 | 191.510470 | 189.125245 | 191.164062 | 48087700 |
| 2023-12-28 | 192.143916 | 192.658574 | 191.183888 | 191.589676 | 34049900 |
| 2023-12-29 | 191.906370 | 192.401229 | 189.758683 | 190.550461 | 42672100 |
# 绘制收盘价走势
fig, ax = plt.subplots()
ax.plot(price.index, price.values, color='steelblue', linewidth=1.5)
ax.fill_between(price.index, price.values, alpha=0.1, color='steelblue')
ax.set_title(f'{ticker} 收盘价 (这就是你在财经软件里看到的那条线)', fontsize=14)
ax.set_ylabel('价格 (USD)')
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
2. 简单收益率 (Simple Return)¶
为什么不直接用价格变化?¶
假设你买了股票 A,昨天 100 元,今天 105 元,赚了 5 元。 同时你朋友买了股票 B,昨天 1000 元,今天 1005 元,也赚了 5 元。
按收益率比较,第一项投资为 5%,第二项投资为 0.5%;第一项投资的相对回报更高。
这就是为什么金融里我们用百分比收益率而不是绝对价格变化:
$$r_t = \frac{P_t - P_{t-1}}{P_{t-1}} = \frac{P_t}{P_{t-1}} - 1$$
简单收益率的特点¶
- 直观易懂:-5% 就是亏了 5%,就是这么简单
- 多资产可以加权平均:如果你同时持有 A 和 B 各 50%,组合收益率 = 0.5×A + 0.5×B
- 跨期计算要相乘:2 天累积收益 = (1+r₁)(1+r₂) - 1,不能直接加
# 计算简单日收益率(pct_change 就是 (今天-昨天)/昨天)
simple_ret = price.pct_change().dropna()
print('简单收益率统计:')
print(simple_ret.describe().round(4))
print(f'\n最大单日收益: {simple_ret.max():.2%} (赚最多的那天)')
print(f'最大单日亏损: {simple_ret.min():.2%} (亏最多的那天)')
简单收益率统计: count 500.0000 mean 0.0003 std 0.0183 min -0.0587 25% -0.0097 50% 0.0006 75% 0.0101 max 0.0890 Name: AAPL, dtype: float64 最大单日收益: 8.90% (赚最多的那天) 最大单日亏损: -5.87% (亏最多的那天)
3. 对数收益率 (Log Return)¶
为什么会有「对数」收益率这东西?¶
对数收益率是量化分析师和研究者更常用的方式,原因有两个:
① 跨期直接相加(不用乘法了)
简单收益率跨期要相乘(很麻烦):$(1+r_1)(1+r_2)\cdots$
对数收益率跨期直接相加:$r^{log}_1 + r^{log}_2 + \cdots$
这在数学推导中方便很多。
② 更接近正态分布,便于统计建模
很多量化模型假设收益率是正态分布的,对数收益率更满足这个假设。
③ 当涨跌幅很小时,两者几乎相等
(日线数据大多数时候两者差不超过 0.01%,可以互换)
$$r_t^{\log} = \ln\frac{P_t}{P_{t-1}} = \ln P_t - \ln P_{t-1}$$
结论: 日常分析用简单收益率(直观),数学建模用对数收益率(方便)。
# 计算对数收益率
log_ret = np.log(price / price.shift(1)).dropna()
# 对比两种收益率的分布形状
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
axes[0].hist(simple_ret, bins=60, color='steelblue', alpha=0.7, edgecolor='white')
axes[0].set_title('简单收益率分布')
axes[0].set_xlabel('收益率')
axes[0].axvline(simple_ret.mean(), color='red', linestyle='--', label=f'均值={simple_ret.mean():.4f}')
axes[0].legend()
axes[1].hist(log_ret, bins=60, color='darkorange', alpha=0.7, edgecolor='white')
axes[1].set_title('对数收益率分布(形状非常相似,因为日收益率都很小)')
axes[1].set_xlabel('收益率')
axes[1].axvline(log_ret.mean(), color='red', linestyle='--', label=f'均值={log_ret.mean():.4f}')
axes[1].legend()
plt.suptitle(f'{ticker} 日收益率分布对比(注意两个分布形状几乎一样)', fontsize=13)
plt.tight_layout()
plt.show()
# 计算累积收益率(假设初始投入 10000 元)
initial_capital = 10000
cumulative_ret = (1 + simple_ret).cumprod()
portfolio_value = cumulative_ret * initial_capital
fig, ax = plt.subplots()
ax.plot(portfolio_value.index, portfolio_value.values, color='green', linewidth=1.5)
ax.axhline(initial_capital, color='gray', linestyle='--', alpha=0.7, label='初始资金 (10000 元)')
ax.fill_between(portfolio_value.index, initial_capital, portfolio_value.values,
where=(portfolio_value.values >= initial_capital),
alpha=0.2, color='green', label='盈利阶段')
ax.fill_between(portfolio_value.index, initial_capital, portfolio_value.values,
where=(portfolio_value.values < initial_capital),
alpha=0.2, color='red', label='亏损阶段')
ax.set_title(f'初始投入 {initial_capital} 元购买 {ticker} 的资产变化', fontsize=13)
ax.set_ylabel('资产价值 (元)')
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
total_return = cumulative_ret.iloc[-1] - 1
print(f'总收益率: {total_return:.2%}')
print(f'最终资产: {portfolio_value.iloc[-1]:.2f} 元')
总收益率: 6.99% 最终资产: 10698.85 元
T = len(simple_ret) # 交易日数
total_ret = cumulative_ret.iloc[-1] - 1
annual_ret = (1 + total_ret) ** (252 / T) - 1
print(f'持有交易日数: {T} 天(约 {T/252:.1f} 年)')
print(f'总收益率: {total_ret:.2%}')
print(f'年化收益率: {annual_ret:.2%} ← 每年平均赚这么多')
持有交易日数: 500 天(约 2.0 年) 总收益率: 6.99% 年化收益率: 3.46% ← 每年平均赚这么多
6. 练习¶
- 将
ticker = 'AAPL'改为你感兴趣的其他股票(如'TSLA','MSFT','GOOGL'),重新运行所有单元格,观察结果差异。 - 计算 2023 年全年的年化收益率(提示:修改下载的起止时间)。
- 对比同一时期多支股票的累积收益率曲线(在同一张图上绘制多条线)。
下一节 → 02_risk_and_volatility.ipynb