Week 13:时间序列基础、收益率与滚动统计
本周目标不是马上预测股价,而是先学会把价格序列变成可以分析的时间序列数据:收益率、滚动均值、滚动波动率、自相关、平稳性直觉,以及最基础的画图检查。
本文命令默认使用 fish shell。如果你正在 CachyOS 的终端里跟做,虚拟环境激活命令必须使用 fish 版本:
source .venv/bin/activate.fish
不要把 Bash / Zsh 的激活脚本直接拿到 fish 里执行。
0. 本周详细教学:语法、规范、验收
本节不是追加在尾部的复习,而是本周正文的入口。先读这里,再做后面的命令和项目。
0.1 本周真正要学会什么
| 维度 | 要求 |
|---|---|
| 知识点 | return、rolling、autocorrelation、时间索引 |
| 代码语法 | 能从空文件写出本周核心脚本,而不是只复制运行 |
| 程序规范 | 函数拆分、路径清楚、输入输出明确、错误能解释 |
| 交付物 | notebooks/week13_time_series_intro.ipynb |
| 验收方式 | 从 fish 终端运行命令,得到可复查的文件或指标 |
0.2 代码语法精讲
下面的代码不是最终答案,而是本周必须理解的最小骨架:
df = df.sort_values("date")
df["ret"] = df["close"].pct_change()
df["log_ret"] = np.log(df["close"]).diff()
df["vol_20"] = df["ret"].rolling(20).std()
df["mom_20"] = df["close"].pct_change(20)
print(df.tail())
读代码时按四步检查:输入从哪里来;中间变量的类型和 shape 是什么;函数或脚本输出什么;哪些错误应该显式报出来。
0.3 本周程序规范
- 所有路径用相对路径或 `pathlib.Path`,不要写死 `/home/miku/...`。
- 核心逻辑进 `src/`,notebook 只做探索和解释。
- 每个脚本能从 fish 终端运行,并在 README 写出命令。
- 输出必须落盘到 `reports/`、`figures/` 或 `outputs/`,不能只在屏幕上看。
0.4 本周练习分层
| 层级 | 任务 | 不合格表现 | 合格验收 |
|---|---|---|---|
| 最小练习 | 手写上面的最小骨架 | 只在 notebook 里运行 | 终端运行成功 |
| 标准练习 | 把逻辑拆成函数/模块 | 一个大脚本从头写到尾 | 至少 2 个函数,职责清楚 |
| 项目练习 | 生成本周交付物 notebooks/week13_time_series_intro.ipynb | 只有屏幕输出 | 文件落盘,可复查 |
| 复盘练习 | 写 3 个错误和修复 | 只写“已解决” | 写清报错、原因、修复、预防 |
0.5 本周和主线的连接
- 回到总计划:USTC AI / Quant 练习手册
- 查详细练习索引:技术练习详解
- 查质量评分:最终质量门槛
1. 本周学习目标
完成本周后,你应该能回答这些问题:
- 为什么量化里通常分析收益率,而不是直接分析价格?
- 简单收益率和对数收益率有什么区别?
- rolling mean / rolling volatility 在看什么?
- 什么叫“时间序列不平稳”,为什么金融价格经常不适合直接建模?
- 如何用图和几个简单统计量检查一条序列?
本周交付物建议为:
week13-time-series-basics/
├── data/
│ └── prices.csv
├── figures/
│ ├── price_and_returns.png
│ ├── rolling_stats.png
│ └── autocorrelation.png
├── notebooks/
│ └── week13_time_series_intro.ipynb
├── reports/
│ └── week13_time_series_notes.md
├── src/
│ └── week13_time_series_basics.py
├── pyproject.toml
└── README.md
如果你还不熟悉 notebook,可以先用 src/week13_time_series_basics.py 写脚本;等脚本跑通后,再把关键图和解释整理到 notebook 或报告里。
2. 前置条件
你需要已经具备:
- CachyOS / Arch 系 Linux 的基本终端操作。
- VS Code 能打开项目目录。
uv已安装。- Python 虚拟环境能正常创建和激活。
- 已经学过
pandas、numpy、matplotlib的基础用法。 - 能理解均值、方差、标准差、相关系数这些统计概念。
先进入统一学习目录:
mkdir -p ~/Code/ustc-stat-ai-quant
cd ~/Code/ustc-stat-ai-quant
解释:
| 命令 | 含义 |
|---|---|
mkdir -p |
创建目录;目录已存在时不报错 |
~/Code/ustc-stat-ai-quant |
建议保存 20 周项目的总目录 |
cd |
进入该目录,后续命令都在这里执行 |
3. 创建 Week 13 项目
mkdir week13-time-series-basics
cd week13-time-series-basics
uv init
uv venv
source .venv/bin/activate.fish
uv add pandas numpy matplotlib statsmodels jupyter ipykernel
mkdir -p data src notebooks reports figures
code .
逐句解释:
| 命令 | 作用 |
|---|---|
mkdir week13-time-series-basics |
为本周单独建项目,不和其它周混在一起 |
uv init |
生成 pyproject.toml、README.md 等项目文件 |
uv venv |
创建当前项目专用的 .venv |
source .venv/bin/activate.fish |
在 fish shell 中激活虚拟环境 |
uv add ... |
安装数据分析、画图、时间序列相关依赖 |
mkdir -p ... |
创建数据、代码、报告、图片目录 |
code . |
用 VS Code 打开当前项目 |
检查 Python 是否来自当前项目:
which python
python --version
python -c "import pandas as pd; import numpy as np; import matplotlib; print('ok')"
如果 which python 的输出包含 .venv/bin/python,说明环境正确。
4. 准备一份练习价格数据
真实市场数据可以以后再接入。本周先生成一份模拟价格数据,重点练概念和代码流程。
在 VS Code 中创建 src/make_sample_prices.py,写入下面代码:
from pathlib import Pathimport numpy as np import pandas as pd
rng = np.random.default_rng(42) dates = pd.bdate_range(“2023-01-02”, periods=260)
模拟日对数收益:小正漂移 + 随机波动 + 少数冲击日
log_returns = rng.normal(loc=0.0004, scale=0.012, size=len(dates)) shock_idx = rng.choice(len(dates), size=6, replace=False) log_returns[shock_idx] += rng.normal(loc=-0.025, scale=0.02, size=len(shock_idx))
price = 100 * np.exp(np.cumsum(log_returns))
out = pd.DataFrame({“date”: dates, “close”: price}) Path(“data”).mkdir(exist_ok=True) out.to_csv(“data/prices.csv”, index=False) print(out.head()) print(“saved to data/prices.csv”)
运行:
python src/make_sample_prices.py
检查文件:
python -c "import pandas as pd; df = pd.read_csv('data/prices.csv'); print(df.head()); print(df.tail())"
这里使用的是模拟数据,所以不能拿结论解释真实市场。它的作用是让你安全地练习时间序列处理。
5. 读取价格并设置时间索引
创建 src/week13_time_series_basics.py,先写入读取部分:
from pathlib import Pathimport matplotlib.pyplot as plt import numpy as np import pandas as pd from statsmodels.graphics.tsaplots import plot_acf
FIGURE_DIR = Path(“figures”) FIGURE_DIR.mkdir(exist_ok=True)
prices = pd.read_csv(“data/prices.csv”, parse_dates=[“date”]) prices = prices.sort_values(“date”).set_index(“date”)
print(prices.head()) print(prices.info())
运行:
python src/week13_time_series_basics.py
解释:
parse_dates=["date"]:把日期列解析成真正的时间类型。sort_values("date"):时间序列必须按时间排序。set_index("date"):把日期设为索引,后面 rolling 和画图更自然。
时间序列第一条规则:先确认时间顺序,再做任何统计。
6. 价格、简单收益率、对数收益率
在脚本中继续加入:
prices["simple_return"] = prices["close"].pct_change() prices["log_return"] = np.log(prices["close"]).diff()
print(prices[[“close”, “simple_return”, “log_return”]].head(10)) print(prices[[“simple_return”, “log_return”]].describe())
运行:
python src/week13_time_series_basics.py
概念解释:
| 名称 | 公式直觉 | 常见用途 |
|---|---|---|
| 简单收益率 | 今天价格相对昨天涨跌百分比 | 回测中计算资产组合收益很直观 |
| 对数收益率 | 价格取对数后做差 | 数学上可加,时间序列建模时常用 |
为什么不用价格直接建模?
- 价格经常有趋势,均值随时间变化。
- 价格尺度随时间变大,波动范围也可能变化。
- 很多模型默认数据围绕稳定均值波动,价格往往不满足。
- 收益率更接近“每天的相对变化”,通常比价格更适合做统计分析。
注意:第一天没有前一天价格,所以收益率是缺失值。这是正常现象,不要强行填 0。
7. 画价格和收益率
继续加入画图代码:
fig, axes = plt.subplots(2, 1, figsize=(10, 7), sharex=True)prices[“close”].plot(ax=axes[0], title=“Simulated Close Price”) axes[0].set_ylabel(“price”)
prices[“simple_return”].plot(ax=axes[1], title=“Daily Simple Return”) axes[1].axhline(0, color=“black”, linewidth=1) axes[1].set_ylabel(“return”)
fig.tight_layout() fig.savefig(FIGURE_DIR / “price_and_returns.png”, dpi=150) plt.close(fig)
运行并查看图片:
python src/week13_time_series_basics.py
xdg-open figures/price_and_returns.png
看图时不要只问“涨了吗”,要问:
- 价格是否有明显趋势?
- 收益率是否围绕 0 上下波动?
- 是否有异常大的正收益或负收益?
- 波动是否集中在某些时间段?
金融数据经常不是“均匀随机噪声”,而是会出现波动聚集:一段时间很平静,另一段时间连续剧烈波动。
8. rolling mean 与 rolling volatility
继续加入:
window = 20 prices["rolling_mean_20"] = prices["simple_return"].rolling(window).mean() prices["rolling_vol_20"] = prices["simple_return"].rolling(window).std()fig, axes = plt.subplots(2, 1, figsize=(10, 7), sharex=True)
prices[“rolling_mean_20”].plot(ax=axes[0], title=“20-Day Rolling Mean of Returns”) axes[0].axhline(0, color=“black”, linewidth=1) axes[0].set_ylabel(“mean”)
prices[“rolling_vol_20”].plot(ax=axes[1], title=“20-Day Rolling Volatility of Returns”) axes[1].set_ylabel(“volatility”)
fig.tight_layout() fig.savefig(FIGURE_DIR / “rolling_stats.png”, dpi=150) plt.close(fig)
print(prices[[“simple_return”, “rolling_mean_20”, “rolling_vol_20”]].tail())
运行:
python src/week13_time_series_basics.py
xdg-open figures/rolling_stats.png
解释:
rolling(20).mean():每一天都看过去 20 个交易日的平均收益。rolling(20).std():每一天都看过去 20 个交易日的波动率。- 前 19 天没有足够窗口,所以结果是缺失值,这是合理的。
量化里 rolling 指标非常重要,因为策略在某一天做决策时,只能使用这一天之前已经发生的数据。以后做回测时,要特别检查 rolling 特征有没有偷看未来。
9. 平稳性直觉
你不需要本周就精通严谨的单位根检验,但必须先建立直觉。
粗略地说,一条平稳序列应该满足:
均值大致稳定
波动范围大致稳定
自相关结构大致稳定
没有明显长期趋势
价格通常不像平稳序列:
价格可能长期上升或下降
价格水平改变后,均值不再稳定
价格的绝对尺度会影响波动大小
收益率通常更接近平稳,但也不完美:
收益率均值更接近稳定
收益率仍可能有波动聚集
极端事件会破坏简单模型假设
在脚本里加入一个简单比较:
first_half = prices.iloc[: len(prices) // 2] second_half = prices.iloc[len(prices) // 2 :]
summary = pd.DataFrame( { “price_first_half”: first_half[“close”].describe(), “price_second_half”: second_half[“close”].describe(), “return_first_half”: first_half[“simple_return”].describe(), “return_second_half”: second_half[“simple_return”].describe(), } ) print(summary)
运行:
python src/week13_time_series_basics.py
观察重点:价格两段的均值可能差很多;收益率两段的均值通常更接近,但标准差仍可能变化。
10. 自相关:今天和过去有关吗
自相关回答的是:今天的收益率是否和前几天的收益率有关系。
继续加入:
returns = prices["simple_return"].dropna()print(“lag 1 autocorrelation:”, returns.autocorr(lag=1)) print(“lag 5 autocorrelation:”, returns.autocorr(lag=5)) print(“lag 20 autocorrelation:”, returns.autocorr(lag=20))
fig = plot_acf(returns, lags=30) fig.set_size_inches(10, 5) fig.tight_layout() fig.savefig(FIGURE_DIR / “autocorrelation.png”, dpi=150) plt.close(fig)
运行:
python src/week13_time_series_basics.py
xdg-open figures/autocorrelation.png
解释:
- 如果 lag 1 自相关明显为正,可能表示短期动量。
- 如果 lag 1 自相关明显为负,可能表示短期反转。
- 如果大部分自相关接近 0,说明简单线性关系很弱。
不要看到一点点相关就兴奋。金融时间序列噪声很大,样本外稳定性比样本内漂亮图更重要。
11. 整理报告
创建 reports/week13_time_series_notes.md,建议包含:
# Week 13 Time Series Notes
Data
- 数据来源:模拟价格数据
- 时间范围:2023 年 260 个工作日
What I computed
- close price
- simple return
- log return
- 20-day rolling mean
- 20-day rolling volatility
- autocorrelation
Observations
- 价格是否有趋势?
- 收益率是否围绕 0 波动?
- rolling volatility 是否有明显变化?
- 自相关是否明显?
Stationarity intuition
- 为什么价格更不平稳?
- 为什么收益率更适合建模?
Questions
如果换成真实股票或指数,结果会怎样?
用 VS Code 创建和编辑报告:
code reports/week13_time_series_notes.md
12. 文件布局检查
运行:
pwd
ls
ls data src reports figures
理想结果类似:
week13-time-series-basics/
data figures notebooks pyproject.toml README.md reports src
你最终至少应有:
data/prices.csv
src/make_sample_prices.py
src/week13_time_series_basics.py
figures/price_and_returns.png
figures/rolling_stats.png
figures/autocorrelation.png
reports/week13_time_series_notes.md
13. 练习
- 把 rolling window 从 20 改成 5、60,比较图像差异。
- 分别计算简单收益率和对数收益率的均值、标准差、最小值、最大值。
- 找出收益率绝对值最大的 5 天,检查这些天在价格图上是否明显。
- 把模拟数据的随机种子从 42 改成 7,再生成一次,观察结论是否稳定。
- 用
pandas.Series.skew()和pandas.Series.kurt()查看收益率的偏度和峰度。 - 试着下载一个真实指数或 ETF 的日线数据,再重复同样分析,并在报告中说明数据来源。
14. 验收检查
在项目根目录执行:
source .venv/bin/activate.fish
python src/make_sample_prices.py
python src/week13_time_series_basics.py
python -c "from pathlib import Path; required = ['data/prices.csv', 'figures/price_and_returns.png', 'figures/rolling_stats.png', 'figures/autocorrelation.png', 'reports/week13_time_series_notes.md']; missing = [p for p in required if not Path(p).exists()]; print('missing:', missing); raise SystemExit(1 if missing else 0)"
通过标准:
- 命令能顺利运行。
figures/下有三张图片。- 报告能解释价格和收益率的差异。
- 你能口头说明为什么时间序列不能随便随机打乱。
15. 常见错误
错误 1:fish 中激活环境失败
错误原因通常是用了 Bash / Zsh 的激活脚本。
fish 应使用:
source .venv/bin/activate.fish
错误 2:收益率第一行是空值
这是正常的,因为第一天没有前一天价格。不要为了“看起来整齐”把它随便改成 0。
错误 3:rolling 前几行是空值
rolling(20) 需要至少 20 个观测值。前 19 行没有完整窗口,所以是缺失值。
错误 4:图保存失败
先确认 figures/ 存在:
mkdir -p figures
脚本里也建议写:
Path("figures").mkdir(exist_ok=True)
错误 5:把价格图看成策略收益
价格上涨不代表策略有效。策略必须明确什么时候买、什么时候卖、交易成本多少、样本外是否有效。这些将在 Week 15 和 Week 16 继续做。
16. 下一步
进入 Week 14:时间序列验证、rolling window 与数据泄露检查。下一周重点不是模型多高级,而是避免把未来信息泄露给过去。
plain
喜欢的话,留下你的评论吧~