1. 这节究竟解决什么实际问题?¶
1.2 风险与波动率 的具体任务是:用波动率、下行风险和收益分布解释“收益高”为什么不等于“风险低”。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
会使用百分比和基础代数;建议先完成环境配置。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
示例优先使用仓库固定的日频复权收盘价;它不是实时行情,也不代表完整市场。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 1.2 风险与波动率 的输入、计算步骤、输出和局限;尤其能说明:用波动率、下行风险和收益分布解释“收益高”为什么不等于“风险低”。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
价格表示资产在特定时点的水平,收益率表示两个时点之间的相对变化。多数跨资产比较使用收益率。
所有公式都先说明分子、分母、单位与时间尺度;年化前必须确认数据频率。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
NumPy/Pandas 帮你算数和对齐索引;统计库帮你估计参数,但不替你解释经济含义。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“用波动率、下行风险和收益分布解释“收益高”为什么不等于“风险低”。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
把百分比和小数混用、忘记 dropna、把日频波动率直接叫作年化波动率、把相关性当因果。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
概念演示也要避免事后选择样本或只展示支持结论的时间段。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/01_financial_concepts/02_risk_and_volatility_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/01_financial_concepts/02_risk_and_volatility_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:Investments(Bodie, Kane, Marcus)。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 03_market_basics。本节的概念会成为理解它的输入,而不是孤立的名词。
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
plt.rcParams['figure.figsize'] = (12, 5)
# 下载数据(AAPL=苹果,TSLA=特斯拉,SPY=标普500指数基金 作为基准)
tickers = ['AAPL', 'TSLA', 'SPY']
data = yf.download(tickers, start='2021-01-01', end='2024-01-01',
progress=False)['Close']
returns = data.pct_change().dropna()
print('数据下载完成')
returns.tail(3)
数据下载完成
| Ticker | AAPL | SPY | TSLA |
|---|---|---|---|
| Date | |||
| 2023-12-27 | 0.000518 | 0.001808 | 0.018822 |
| 2023-12-28 | 0.002226 | 0.000378 | -0.031594 |
| 2023-12-29 | -0.005424 | -0.002895 | -0.018564 |
# 默认使用固定样本,保证课程与 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. 波动率(Volatility)¶
为什么用「波动率」来衡量风险?¶
想象两个同事每个月工资变化:
- A:每月工资 10000 元,基本不变
- B:某月 3000,下月 20000,后月 -5000(亏损)……
B 的「平均工资」可能也是 10000,但你敢把 B 的工资当可靠收入来还房贷吗?当然不敢,因为波动太大了,不确定性太高。
金融里的「风险」 = 收益率的不确定性 = 收益率的波动程度
数学上就是收益率的标准差(Standard Deviation):
$$\sigma_{daily} = \text{std}(r_t) \quad \text{(日波动率)}$$
为什么要乘 $\sqrt{252}$ 年化?¶
一年有 252 个交易日,如果每天相互独立,方差会加 252 倍,标准差就要乘 $\sqrt{252}$:
$$\sigma_{annual} = \sigma_{daily} \times \sqrt{252} \quad \text{(年化波动率,方便跨期比较)}$$
# 计算年化波动率
daily_vol = returns.std()
annual_vol = daily_vol * np.sqrt(252)
print('年化波动率(越高代表越动荡,风险越大):')
for ticker in tickers:
print(f' {ticker:<6}: {annual_vol[ticker]:.2%}')
# 滚动波动率(20日)— 看波动率随时间的变化
rolling_vol = returns.rolling(20).std() * np.sqrt(252)
fig, ax = plt.subplots()
for ticker in tickers:
ax.plot(rolling_vol.index, rolling_vol[ticker], label=ticker, linewidth=1.2)
ax.set_title('20日滚动年化波动率 — 市场危机时(2022年),波动率会飙升', fontsize=13)
ax.set_ylabel('年化波动率')
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f'{y:.0%}'))
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
年化波动率(越高代表越动荡,风险越大): AAPL : 27.80% TSLA : 58.89% SPY : 17.60%
2. 夏普比率(Sharpe Ratio)¶
为什么不能只看收益率?¶
假设两个策略:
- 策略 A:年化收益 20%,年化波动 10%(收益稳定地涨)
- 策略 B:年化收益 20%,年化波动 60%(大起大落,心跳加速)
两个都赚了 20%,但你愿意用哪个?大多数人选 A,因为同样的收益率,风险小得多。
夏普比率解决的正是这个问题:单位风险能赚多少「超额」收益。
$$SR = \frac{\bar{r} - r_f}{\sigma}$$
- $\bar{r}$:策略年化收益率
- $r_f$:无风险利率(把钱存银行/买国债的收益,不承担任何风险也能赚到的)
- $\bar{r} - r_f$:超额收益(你比「不冒险」多赚的部分)
- $\sigma$:你为这个超额收益承担的风险(波动率)
夏普比率的解读(经验参考):
| 夏普比率 | 解读 |
|---|---|
| < 0 | 还不如存银行 |
| 0 ~ 0.5 | 勉强 |
| 0.5 ~ 1 | 可以接受 |
| 1 ~ 2 | 良好 |
| > 2 | 优秀(大型基金能做到 1 就已经不错了) |
risk_free_rate = 0.04 # 假设无风险利率 4%(美国国债水平)
annual_ret = returns.mean() * 252
sharpe = (annual_ret - risk_free_rate) / annual_vol
print(f'\n{"资产":<8} {"年化收益":<12} {"年化波动":<12} {"夏普比率"}')
print('-' * 45)
for ticker in tickers:
print(f'{ticker:<8} {annual_ret[ticker]:<12.2%} {annual_vol[ticker]:<12.2%} {sharpe[ticker]:.2f}')
# 可视化
fig, ax = plt.subplots(figsize=(8, 4))
colors = ['steelblue' if s > 0 else 'salmon' for s in sharpe.values]
bars = ax.bar(tickers, sharpe.values, color=colors, edgecolor='white', linewidth=1.5)
ax.axhline(0, color='black', linewidth=0.8)
ax.axhline(1, color='green', linestyle='--', alpha=0.5, label='夏普=1(良好标准线)')
ax.set_title('各资产夏普比率对比 — TSLA 高收益但风险高,SPY 相对稳健', fontsize=13)
ax.set_ylabel('Sharpe Ratio')
ax.legend()
for bar, val in zip(bars, sharpe.values):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.02,
f'{val:.2f}', ha='center', fontsize=11)
plt.tight_layout()
plt.show()
资产 年化收益 年化波动 夏普比率 --------------------------------------------- AAPL 17.76% 27.80% 0.49 TSLA 18.02% 58.89% 0.24 SPY 11.54% 17.60% 0.43
3. 最大回撤(Maximum Drawdown)¶
为什么需要最大回撤?¶
夏普比率衡量「平均」水平的风险收益比,但有时候平均数会骗人。
想象一个策略:前三年每年赚 30%,然后第四年突然亏了 80%。平均年化收益可能还是正的,但……问题是,在亏 80% 发生之前,你的账户会从高峰跌到谷底,跌了 80%。这个过程让你心理崩溃,你可能早就割肉离场了。
最大回撤就是衡量这个「最惨烈的连跌」有多深。
从历史某个高点出发,跌到最深的那个谷底,跌了多少百分比?
$$MDD = \max_{t} \left(\frac{\text{峰值} - \text{当前值}}{\text{峰值}}\right)$$
- 最大回撤 < 10%:非常稳健(大型对冲基金的目标)
- 最大回撤 20%~50%:个人可以接受,但要有心理准备
- 最大回撤 > 50%:除非你铁石心肠,否则很难拿住到反弹
def max_drawdown(returns_series):
"""计算回撤序列和最大回撤"""
cum_ret = (1 + returns_series).cumprod() # 累积净值 (连乘)
rolling_max = cum_ret.cummax() # 历史最高点(随时间更新)
drawdown = (cum_ret - rolling_max) / rolling_max # 当前离最高点跌了多少
return drawdown, drawdown.min()
fig, axes = plt.subplots(len(tickers), 1, figsize=(12, 10), sharex=True)
for ax, ticker in zip(axes, tickers):
dd, mdd = max_drawdown(returns[ticker])
ax.fill_between(dd.index, dd.values, 0, color='red', alpha=0.4)
ax.plot(dd.index, dd.values, color='darkred', linewidth=0.8)
ax.set_title(f'{ticker} | 最大回撤: {mdd:.2%} (从最高点最多跌过这么多)', fontsize=11)
ax.set_ylabel('回撤')
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f'{y:.0%}'))
ax.grid(alpha=0.3)
plt.suptitle('各资产回撤曲线对比 — 面积越大、越深,说明持有体验越痛苦', fontsize=13, y=1.01)
plt.tight_layout()
plt.show()
4. 汇总风险指标¶
把四个核心指标放在一张表里对比,这就是量化评估一个策略/资产时的标准格式:
summary = pd.DataFrame(index=tickers)
summary['年化收益率'] = annual_ret.map('{:.2%}'.format)
summary['年化波动率'] = annual_vol.map('{:.2%}'.format)
summary['夏普比率'] = sharpe.map('{:.2f}'.format)
summary['最大回撤'] = {t: f'{max_drawdown(returns[t])[1]:.2%}' for t in tickers}
summary
| 年化收益率 | 年化波动率 | 夏普比率 | 最大回撤 | |
|---|---|---|---|---|
| AAPL | 17.76% | 27.80% | 0.49 | -30.91% |
| TSLA | 18.02% | 58.89% | 0.24 | -73.63% |
| SPY | 11.54% | 17.60% | 0.43 | -24.50% |
练习¶
- 增加另一个资产(如债券 ETF
'TLT')并对比其风险特征。(债券和股票的风险特征有什么不同?) - 计算 2022 年熊市期间(2022-01-01 至 2022-12-31)各资产的最大回撤。
- 思考题:波动率高一定是坏事吗?结合夏普比率谈谈——如果波动率翻倍但收益率也翻倍,夏普比率会怎么变?
下一节 → 03_market_basics.ipynb