Python 量化环境 — pandas / numpy / yfinance 数据栈
Python 版本选型、虚拟环境对比、数据源生态地图、复权与时区等数据陷阱
日期: 2026-05-12 方向: 个人量化交易 / Python 数据基础设施 阶段: Phase 1: 基础与工具链 标签: #Python311 #pandas #numpy #yfinance #pyarrow #DataSource #SurvivorshipBias #Adjusted
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | Python 版本选型、虚拟环境对比、数据源生态地图、复权与时区等数据陷阱 |
| 实操 | 建 venv → 装核心包 → yfinance 下载 SPY 10 年日线 → parquet 落盘 → 算累计净值曲线 |
| 产出 | TR-DAY3 笔记 + tr_day3_data.py 可运行脚本 + 一份本地 parquet 数据 |
一、Python 版本选择:3.10 / 3.11 / 3.12 怎么选
很多人随手 brew install python 或 apt install python3 装到什么算什么。对量化场景这是一个有后果的决定——很多核心库(vectorbt、qlib、zipline-reloaded、numba、torch 某些组合)的兼容性曲线滞后于 CPython 主线半年到一年。
1.1 截至 2026 年中的兼容性现状
| 库 | 用途 | Python 3.10 | Python 3.11 | Python 3.12 | Python 3.13 |
|---|---|---|---|---|---|
| ib_insync | IBKR API 封装 | ✓ | ✓ | ✓(asyncio API 已稳) | 实测可跑但 wheel 有限 |
| pandas 2.x | 数据框 | ✓ | ✓ | ✓ | ✓ |
| numpy 1.26 / 2.x | 数值 | ✓ | ✓ | ✓ | ✓ |
| pyarrow 16+ | 列存 / parquet | ✓ | ✓ | ✓ | 部分版本 |
| yfinance 0.2.x | 行情下载 | ✓ | ✓ | ✓ | ✓ |
| vectorbt | 回测 | ✓ | ✓ | △ 部分需要 numba 升级 | ✗ numba 不支持 |
| numba | JIT | ✓ | ✓ | ✓(0.59+) | ✗ |
| qlib | 微软因子框架 | ✓ | ✓ | △ requirements 经常落后 | ✗ |
| zipline-reloaded | Quantopian 后裔 | ✓ | △ | ✗ | ✗ |
| pytorch 2.x | 深度学习 | ✓ | ✓ | ✓ | △ |
| backtrader | 老牌回测 | ✓ | ✓ | ✓(无人维护但能跑) | △ |
1.2 决策
强烈建议:Python 3.11。
理由:
- vectorbt + numba 完整可用:3.12 上 numba 0.59 才追上,但 qlib / vectorbt 的某些 transitive dep 会卡住。3.11 是当前「装啥都不报错」的最稳版本。
- 比 3.10 快 10–60%:CPython 3.11 的 specializing adaptive interpreter 对回测这种纯 Python 内层循环提速极明显。3.10 → 3.11 是过去 10 年 CPython 单次升级幅度最大的一次。
- 避开 3.13 的 free-threaded 早期坑:3.13 引入实验性 free-threaded 模式(PEP 703),但绝大多数 C 扩展还没适配 GIL-free,量化堆栈装不全。
- 3.12 也能用,但偶尔会撞到 qlib/zipline 这类生态库的 setup.py 检查。如果你不打算碰 qlib/zipline,3.12 是可以的次选。
反例:不要用 3.9 及更老。pandas 2.2+ 已经停止支持 3.9 wheel,明年会更糟。也不要用 conda 自带的某些古早版本——那些往往是 3.9 / 3.10 mix。
# macOS / Linux:用 pyenv 装独立的 3.11
pyenv install 3.11.9
pyenv local 3.11.9
# Windows:从 python.org 下载 3.11.x installer
# 或用 uv(见下节)一步搞定
二、虚拟环境:venv / poetry / uv 怎么选
| 方案 | 速度 | 锁文件 | 学习成本 | 量化场景适配 |
|---|---|---|---|---|
venv + pip | 慢 | requirements.txt(手维护) | 极低 | 入门够用,复现性差 |
conda / mamba | 中 | env.yml | 低 | 当年的标配,但和 pip 混用易 break |
poetry | 中 | poetry.lock 强制 | 中 | 生产 OK,初学有点重 |
uv (Astral) | 极快(10–100x pip) | uv.lock | 低 | 2026 年的最佳选择 |
pdm | 快 | pdm.lock | 中 | 小众但优雅 |
2.1 为什么是 uv
uv 是 Astral(ruff 团队)写的 Rust 实现,2024 年发布以来已经成为 Python 社区事实上的新标准:
- 解析 + 下载 + 安装一气呵成,比 pip 快一个数量级
- 自动管理 Python 版本(不用先装 pyenv)
- 兼容 pip / requirements.txt / pyproject.toml
- 锁文件
uv.lock跨平台可复现
对量化的价值:你 90 天内大概率会反复重建环境(换机器 / 加 GPU 节点 / 部署到 cloud VM)。uv 把「重建一次环境」从 5 分钟变成 5 秒,省下来的时间是真实的。
2.2 推荐建项目流程
# 1. 安装 uv(一次性)
# macOS / Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows PowerShell:
# powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
# 2. 进项目目录
cd ~/code/quant-lab
# 3. 让 uv 自己装 Python 3.11 + 建 venv
uv venv --python 3.11
# 4. 激活(uv 也支持 uv run 不激活直接跑)
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\Activate.ps1 # Windows PS
# 5. 装核心包(注意是 uv pip 不是 pip)
uv pip install ib_insync==0.9.86 pandas numpy yfinance pyarrow matplotlib
# 6. 冻结依赖(之后用 uv pip sync 复现)
uv pip freeze > requirements.txt
如果你坚持用 pip / venv,命令几乎一样,只是把 uv pip 换成 pip。本系列后续所有命令都默认 uv pip,但任何地方都可以无脑替换为 pip。
三、核心包:必装清单与各自用途
下面这份是「Day 3-30 量化基础阶段够用」的最小集,每个都有明确分工,不要装 anaconda 那套全家桶——那种装法会让你两个月后想清理时痛不欲生。
| 包 | 版本建议 | 用途 | 必装? |
|---|---|---|---|
| pandas | 2.2+ | DataFrame,时序所有操作的中枢 | ✓ 必装 |
| numpy | 1.26+ 或 2.x | 数值底层,pandas / scipy 的依赖 | ✓ 必装(被传递安装) |
| yfinance | 0.2.40+ | 免费雅虎行情下载 | ✓ 必装 |
| pyarrow | 16+ | parquet/feather 列存读写 | ✓ 必装(pandas 2.x 也越来越依赖它) |
| matplotlib | 3.8+ | 画图,简陋但够用 | ✓ 必装 |
| jupyterlab | 4.x | 探索式分析 | 推荐 |
| ib_insync | 0.9.86 | IBKR 数据/下单 | Day 1 已装 |
| scipy | 1.13+ | 统计、优化、信号处理 | Day 4 装 |
| statsmodels | 0.14+ | OLS、ADF 平稳性、ARCH | Day 12 因子分析时装 |
| vectorbt | 0.27+ | 向量化回测 | Day 30+ 装 |
| backtrader | 1.9.78 | 事件驱动回测(备选) | 可选 |
| seaborn | 0.13+ | 统计可视化 | 可选 |
| plotly | 5.20+ | 交互图 | 可选 |
3.1 一行装齐(推荐)
uv pip install \
"pandas>=2.2" "numpy>=1.26" \
"yfinance>=0.2.40" "pyarrow>=16" "matplotlib>=3.8" \
"jupyterlab>=4" "ib_insync==0.9.86"
3.2 关于 pyarrow:被低估的关键依赖
pyarrow 在量化项目里几乎是个「隐形主角」:
- pandas 读写 parquet 必须有它(
df.to_parquet()底层调它) - parquet 比 CSV 节省 5-10x 磁盘,比 pickle 跨语言安全
- 你 90 天后会有 50+ GB 的本地 bar 数据,CSV 会让你磁盘爆掉
坑:pandas 2.2+ 在某些操作上默认想用 pyarrow-backed string dtype,不装 pyarrow 会报 FutureWarning。装上就一了百了。
四、数据源生态地图
这是个人量化最容易低估的决定——数据源不只是「拿哪里的数据」,是你整个策略可信度的基石。
| 数据源 | 价格 | 覆盖 | 质量 | 限速 | 适合场景 |
|---|---|---|---|---|---|
| yfinance | 免费 | 美股 / 全球大部分主板 | 参差,有时缺 bar、复权偶尔出错 | 软限制(Yahoo 后端) | 入门、原型、批量下载历史日线 |
| polygon.io | $29-$199/月 | 美股 / 期权 / 加密 | 高,机构级 | 按 plan 限速 | 严肃个人量化的性价比之王 |
| IBKR Historical Data | 含在订阅里 | 全球,跟你能交易的一致 | 高,权威,与执行端口对账 | 每秒 ~50 ticks,limit hit 后封 10 分钟 | 实盘策略对账、最终生产数据 |
| Alpha Vantage | 免费有限 / 付费 | 美股 / 外汇 | 中 | 5 calls/min(免费) | 小批量补丁 |
| Tiingo | $10-$30/月 | 美股 EOD + 新闻 | 高 | 宽松 | 长周期回测的便宜替代 |
| Quandl / Nasdaq Data Link | 部分免费 | 宏观 / 另类数据 | 高 | 宽松 | 因子数据(CPI / 失业率 / 期限利差) |
| AKShare | 免费 | A 股 / 港股 / 美股部分 | 中,依赖东方财富等数据源稳定性 | 软限制 | A 股研究、中文文档友好 |
| Tushare | 部分免费 / 积分制 | A 股 / 港股 | 高(金融机构常用) | 按积分限速 | A 股严肃研究 |
| CryptoCompare / CCXT | 免费 | 加密 | 中 | 按交易所而定 | 加密行情聚合 |
4.1 我们 90 天的数据源策略
| 阶段 | 主用 | 备用 | 原因 |
|---|---|---|---|
| Day 1-30 | yfinance | - | 免费、量够、坑可学 |
| Day 31-60 | yfinance + IBKR Hist | polygon trial | 开始要复权对账、期权链 |
| Day 61-90 | IBKR Hist + polygon | yfinance(仅探索) | 实盘对账必须用执行端权威源 |
4.2 为什么不能一直用 yfinance
它的核心问题不是「不准」,是不稳定:
- 接口是非官方逆向 Yahoo Finance Web,Yahoo 一改前端就崩
- 没有 SLA,明天突然停服你没办法
- 复权逻辑是 Yahoo 的(详见后面),有时候和 CRSP / IBKR 的复权不一致
- 期权链残缺,完全不能用于期权策略
但对于学习阶段、批量下载日线、跑历史因子回测——yfinance 性价比无敌。我们的策略是:用 yfinance 学方法论、用付费源跑生产。
五、yfinance 第一段代码
5.1 装包
uv pip install yfinance pyarrow
5.2 下载 SPY 10 年日线
# tr_day3_data.py
import yfinance as yf
import pandas as pd
from pathlib import Path
DATA_DIR = Path("./data")
DATA_DIR.mkdir(exist_ok=True)
def download_daily(ticker: str, start: str, end: str | None = None) -> pd.DataFrame:
"""
下载日线 OHLCV + Adjusted Close。
auto_adjust=False 让我们同时拿到 Close 和 Adj Close,看清复权差异。
"""
df = yf.download(
ticker,
start=start,
end=end,
interval="1d",
auto_adjust=False, # 关键:自己控制复权
progress=False,
group_by="column",
)
if df.empty:
raise RuntimeError(f"empty data for {ticker}")
# yfinance 0.2.x 在多 ticker / 单 ticker 时返回 columns 结构不一样
# 单 ticker 时 columns 通常是 (col, ticker) 的 MultiIndex,扁平化它
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.index.name = "date"
return df
if __name__ == "__main__":
spy = download_daily("SPY", start="2015-01-01", end="2025-12-31")
print(spy.head())
print(spy.tail())
print(f"rows: {len(spy)}, span: {spy.index.min().date()} -> {spy.index.max().date()}")
# 落盘 parquet
out = DATA_DIR / "SPY_2015_2025_1d.parquet"
spy.to_parquet(out, engine="pyarrow", compression="zstd")
print(f"saved: {out} ({out.stat().st_size / 1024:.1f} KB)")
跑一下,应该看到:
Open High Low Close Adj Close Volume
date
2015-01-02 206.38 206.88 204.18 205.43 178.234... 121465900
...
rows: ~2770, span: 2015-01-02 -> 2025-12-30
saved: data/SPY_2015_2025_1d.parquet (~120 KB)
10 年 SPY 日线压缩成 ~120 KB parquet——同样数据存 CSV 大约 380 KB,5x 差距。这就是为什么 pyarrow 是必装。
六、数据清洗的 6 个常见坑
新手回测的「我策略 Sharpe 3.0!」绝大多数死在这一节。
6.1 复权(Adjusted Close vs Close)
问题:股票分红 + 拆股会让 Close 突然跳水/跳涨,但实际持有人没亏没赚。直接用 Close 算 return,每次分红都被你算成一次「下跌」。
对比示例(AAPL 在 2020-08-31 4-for-1 拆股):
| 日期 | Close(不复权) | Adj Close(复权) |
|---|---|---|
| 2020-08-28 | 499.23 | ~124.0 |
| 2020-08-31 | 129.04 | ~128.5 |
如果你用 Close 算 return,2020-08-31 的日收益率是 -74%——纯粹是数据 artifact。
结论:回测用 Adj Close,永远。yfinance 的 auto_adjust=True 会直接覆盖 Close 列;auto_adjust=False 同时返回两列让你看差异。生产用 auto_adjust=True 即可。
# 算 daily return — 用 Adj Close
df["ret"] = df["Adj Close"].pct_change()
坑中坑:复权只对「持有到分红日」的人是正确的。如果你的策略涉及分红前后买入卖出的具体行为(如 dividend capture),你需要原始 Close + dividend cash flow 分开建模,不能用 Adj Close 一刀切。
6.2 Survivorship Bias(幸存者偏差)
问题:yfinance 里你看到的「现在 S&P500 成分股」是今天的500只。如果你回测「2010 年买入 S&P500 全部成分股」,用今天的列表去拉历史数据——你已经天然剔除了所有这十几年间被踢出指数(因为表现差或破产)的公司,回测必然过分乐观。
真实案例:Lehman Brothers、Bear Stearns、Enron、WorldCom、AIG(2008 改组)、GM(2009 破产)、Sears、Toys R Us——这些公司在某个时段是 S&P500 / Russell 1000 成分股,今天 yfinance 上要么搜不到,要么数据残缺。
对策(按成本递增):
- 学习阶段先承认这个 bias,回测结果心里打 7-8 折
- 用 CRSP(学术标准,贵)或 Norgate Data(个人量化友好,~$70/月美股)拿 point-in-time 成分股历史
- 用 Wikipedia 的「S&P500 historical changes」表做手工 hack,时间线粗糙但能用
- polygon.io 的 reference API 也提供一定的退市数据
6.3 拆股 / 合股 / 股息再投资
yfinance 的 Adj Close 已经处理了拆股和股息,但有几个边界:
- 小数股拆分(1.5-for-1):yfinance 的复权偶尔有 1-2 个 bp 的精度问题
- 特殊分红:一次性大额分红(如 COST 偶尔 $10 special dividend)有时未完全反映
- 合股(reverse split):常见于濒退市股,yfinance 处理一般可靠,但配合 survivorship bias 几乎全军覆没
自检方法:拿你最熟悉的几只股票,看它在已知拆股日的 Adj Close 是否平滑。
6.4 时区
默认行为:
- yfinance 返回的 index 是 本地(exchange)时区 的 timezone-aware DatetimeIndex(如美股是 America/New_York)。早期版本是 naive,新版本改了。
- pandas 默认 print 时常常省略 tz 信息,让你以为是 UTC。
坑:你跨数据源 join 时(如 yfinance SPY × FRED 联邦基金利率 × IBKR ETH 永续合约),时区不一致会让 join 失败或错位 1 行。
统一规则(推荐):
# 全部统一到 UTC、并去 tz info(即假装都是 UTC naive)
df.index = df.index.tz_convert("UTC").tz_localize(None)
# 或者保留 tz,但所有 source 都 convert 到同一个 tz
df.index = df.index.tz_convert("America/New_York")
我倾向全部 UTC naive,简单,存盘后跨平台无歧义,画图时手动加 4/5 小时算 NY 即可。
6.5 节假日 / 半日交易日
美股每年有 9 个完整闭市日 + 3-4 个半日交易日(如感恩节后的 Black Friday、圣诞夜)。yfinance 不会给你节假日的 row(这是对的),但半日交易日的 Volume 看起来异常低——别误认为「流动性枯竭信号」。
# pandas_market_calendars 是处理交易日历的标配
# uv pip install pandas_market_calendars
import pandas_market_calendars as mcal
nyse = mcal.get_calendar("NYSE")
schedule = nyse.schedule(start_date="2024-01-01", end_date="2024-12-31")
print(schedule.head()) # 含每天的 market_open / market_close (UTC)
6.6 Look-ahead Bias(前瞻性偏差)
虽然不是「数据清洗」严格意义上的坑,但和数据预处理紧紧相关:
- 用「当天收盘价」算信号 + 「当天收盘价」执行 = 作弊(你 16:00:00 才知道收盘价,怎么 16:00:00 成交?)
- 正确做法:信号用 t 日收盘价 → 在 t+1 日开盘价执行,或更保守 t+1 日 VWAP
- 在 pandas 里这等价于一句
df["signal"].shift(1)—— 这就是后面shift操作为什么是量化里最危险也最重要的函数。
七、pandas 时序基础:量化最常用的 10 个操作
把 SPY 数据装入内存后,你 90% 的时间会在反复用这几招。把它们当成键盘上的肌肉记忆。
import pandas as pd
df = pd.read_parquet("data/SPY_2015_2025_1d.parquet")
7.1 DatetimeIndex 是一切的起点
# 确保 index 是 DatetimeIndex(来自 parquet 时通常已是)
df.index = pd.to_datetime(df.index)
# 用日期切片(原生 string 切片,不需要 .loc 的 datetime 对象)
df.loc["2020-03"] # 2020 年 3 月所有
df.loc["2020-03-09":"2020-03-23"] # COVID crash 那 2 周
df.loc["2024":] # 2024 至今
7.2 pct_change:日收益率
df["ret"] = df["Adj Close"].pct_change() # simple return
df["log_ret"] = (df["Adj Close"] / df["Adj Close"].shift(1)).apply("log") # log return
7.3 shift:把未来变成历史 / 把历史变成未来
df["ret_next"] = df["ret"].shift(-1) # 把 t+1 的 return 移到 t 行 — 用于「信号 → 未来收益」对齐
df["close_yest"] = df["Adj Close"].shift(1) # 用昨日收盘做 reference
血泪规则:你写信号的最后一步,闭着眼也要 signal.shift(1) 再去乘 ret,否则就是 look-ahead。
7.4 rolling:移动窗口
df["ma20"] = df["Adj Close"].rolling(20).mean()
df["vol20"] = df["ret"].rolling(20).std() * (252 ** 0.5) # 年化 20 日波动
df["high60"] = df["High"].rolling(60).max()
7.5 ewm:指数加权(半衰期更直观)
df["ema20"] = df["Adj Close"].ewm(span=20, adjust=False).mean()
df["vol_ewm"] = df["ret"].ewm(halflife=20).std() * (252 ** 0.5)
7.6 resample:日线 → 周线 / 月线
weekly = df["Adj Close"].resample("W-FRI").last() # 每周五收盘
monthly = df["Adj Close"].resample("ME").last() # month-end (pandas 2.x)
weekly_ret = df["ret"].resample("W-FRI").apply(lambda x: (1 + x).prod() - 1)
7.7 cumprod:累计净值曲线
df["nav"] = (1 + df["ret"]).cumprod()
df["nav"].iloc[-1] # 期末净值,1 = 初始 1.0
7.8 dropna / fillna:处理缺失
df = df.dropna(subset=["ret"]) # 第一行 pct_change 必为 NaN
df["dividend"] = df["dividend"].fillna(0)
7.9 groupby:跨资产并排
# 当你下载 ['SPY','QQQ','IWM'] 多个 ticker 时
multi = yf.download(["SPY","QQQ","IWM"], start="2020-01-01", auto_adjust=True)["Close"]
returns = multi.pct_change().dropna()
returns.corr() # 相关性矩阵
7.10 merge_asof:按时间最近匹配(跨频率拼接)
# 把日频价格和分钟频信号按 "as of" 拼接
result = pd.merge_asof(
df_minute_signal.sort_index(),
df_daily_price.sort_index(),
left_index=True, right_index=True,
direction="backward",
)
这十招覆盖你 80% 的日常操作。剩下 20% 是 pivot / melt / MultiIndex 这种偶尔用到的技巧,等碰到再学。
八、Mini 任务:完整可运行的累计净值曲线
把前面所有概念串起来,一段代码:下载 → 清洗 → 算 return → 画累计净值。
# tr_day3_mini_task.py
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
DATA_DIR = Path("./data")
DATA_DIR.mkdir(exist_ok=True)
PLOT_DIR = Path("./plots")
PLOT_DIR.mkdir(exist_ok=True)
def get_spy(start="2015-01-01", end=None) -> pd.DataFrame:
cache = DATA_DIR / "SPY_daily.parquet"
if cache.exists():
df = pd.read_parquet(cache)
else:
df = yf.download(
"SPY",
start=start,
end=end,
interval="1d",
auto_adjust=True, # 直接拿复权后 Close
progress=False,
)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.to_parquet(cache, engine="pyarrow", compression="zstd")
df.index = pd.to_datetime(df.index)
if df.index.tz is not None:
df.index = df.index.tz_convert("UTC").tz_localize(None)
return df
def build_metrics(df: pd.DataFrame) -> pd.DataFrame:
out = pd.DataFrame(index=df.index)
out["price"] = df["Close"]
out["ret"] = out["price"].pct_change()
out["nav"] = (1 + out["ret"].fillna(0)).cumprod()
out["dd"] = out["nav"] / out["nav"].cummax() - 1 # drawdown
out["ma200"] = out["price"].rolling(200).mean()
return out.dropna(subset=["ret"])
def annualized_stats(rets: pd.Series) -> dict:
n = len(rets)
ann_ret = (1 + rets).prod() ** (252 / n) - 1
ann_vol = rets.std() * (252 ** 0.5)
sharpe = ann_ret / ann_vol if ann_vol > 0 else float("nan")
return {"ann_ret": ann_ret, "ann_vol": ann_vol, "sharpe": sharpe}
def plot_nav(metrics: pd.DataFrame, out: Path):
fig, axes = plt.subplots(2, 1, figsize=(11, 7), sharex=True,
gridspec_kw={"height_ratios": [3, 1]})
axes[0].plot(metrics.index, metrics["nav"], label="SPY NAV (Adj)")
axes[0].set_ylabel("NAV (start = 1.0)")
axes[0].set_title("SPY Buy & Hold — Cumulative NAV with Drawdown")
axes[0].legend(loc="upper left")
axes[0].grid(alpha=0.3)
axes[1].fill_between(metrics.index, metrics["dd"], 0, color="crimson", alpha=0.4)
axes[1].set_ylabel("Drawdown")
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.savefig(out, dpi=140)
plt.close(fig)
if __name__ == "__main__":
df = get_spy(start="2015-01-01", end="2025-12-31")
m = build_metrics(df)
stats = annualized_stats(m["ret"])
print(f"period: {m.index.min().date()} -> {m.index.max().date()}")
print(f"ann_ret: {stats['ann_ret']:.2%}")
print(f"ann_vol: {stats['ann_vol']:.2%}")
print(f"sharpe: {stats['sharpe']:.2f}")
print(f"max_dd: {m['dd'].min():.2%}")
print(f"final_nav: {m['nav'].iloc[-1]:.3f}")
plot_nav(m, PLOT_DIR / "spy_nav.png")
print(f"saved plot -> {PLOT_DIR/'spy_nav.png'}")
跑完应该看到大致这样的输出(精确数字看你跑的时候 yfinance 给到哪天):
period: 2015-01-02 -> 2025-12-30
ann_ret: ~12%
ann_vol: ~18%
sharpe: ~0.65 (无风险利率未减)
max_dd: ~-34% (2020-03 COVID)
final_nav: ~3.4
8.1 这段代码里的 4 个值得注意的细节
- 本地缓存:parquet 一旦下载就重用。yfinance 接口不稳,缓存能让你免于「正在写代码 Yahoo 后端 504」的尴尬。
auto_adjust=True:上节讲了为什么。tz_localize(None):把 yfinance 给的 NY 时区 timestamp 统一为 UTC naive。否则 join FRED 数据时会撞到 tz mismatch。- drawdown = NAV / NAV.cummax() − 1:最便宜的风险指标,比 Sharpe 直观得多。后面所有策略评估都离不开它。
九、PM 视角:迁移性思考
9.1 数据源选择 = 信任成本 vs 价格的权衡
这条对于你 10 年金融背景的人来说应该非常熟悉:任何金融系统的设计都会遇到「权威数据源 vs 便宜数据源」的取舍。
| 维度 | 银行核心系统的「权威源」 | 量化系统的「权威源」 |
|---|---|---|
| 例子 | 总账(GL)、客户主数据 | IBKR Hist Data、CRSP |
| 特征 | 慢、贵、SLA 强、有审计追溯 | 慢、贵、和执行端对账 |
| 替代源 | 数据集市 / ODS / 各种缓存表 | yfinance / Tiingo / 第三方 EOD |
| 风险 | 数据漂移、对账失败 | 复权不一致、survivorship、停服 |
| 治理对策 | 主数据治理 + 落表对账 | 落 parquet + 跨源校验 + 审计 |
核心原则:学习/原型用便宜源,决策/执行用权威源,并且必须有跨源对账机制。这条原则在做支付系统时叫「双账户对账」,在量化系统里叫「shadow data check」——本质是同一件事。
9.2 数据陷阱 = 隐式的产品风险
你做支付产品时见过太多「业务逻辑没问题,数据 pipeline 出错导致对账差几分钱」的事故。量化里几乎一模一样:
- 复权错 → 信号反向
- 时区错 → 错日成交
- survivorship → 回测过分乐观
- 节假日 → resample 错位
这些都不是「策略好不好」的问题,是数据治理的问题。Day 3 学的所有东西,在你银行/零售背景里都有对应概念,只是换了名字:复权 ≈ 期初余额平移、survivorship ≈ 客户基数变更未追溯、时区错 ≈ 跨时区 cut-off 不一致。
9.3 为什么我们 90 天不直接上 polygon
便宜数据源的「坑」是教学材料的一部分。你需要踩过 yfinance 的复权坑、survivorship 坑、时区坑,将来才有 sense 去判断付费源给的数据是不是 OK——没有跟坏数据搏斗过的人,看到好数据也不知道好在哪。
这点和金融业务里「先在沙箱里跑过几次错账才能学会做对账」是一回事。
十、Day 3 实际执行 Checklist
- (0) 看完这篇笔记
- (1) 装 uv(5 分钟)
- (2) 项目目录建 venv:
uv venv --python 3.11 - (3) 装核心包:
uv pip install pandas numpy yfinance pyarrow matplotlib jupyterlab ib_insync - (4) 跑
tr_day3_data.py,确认data/SPY_2015_2025_1d.parquet落盘 - (5) 跑
tr_day3_mini_task.py,确认看到 NAV / Sharpe / DD 输出 + 图片 - (6) 用 jupyter 打开 parquet,玩一下 Section 7 的 10 个 pandas 操作
- (7) 故意试一次
auto_adjust=FalsevsTrue,对比 2020-08-31 AAPL 数据看复权差异 - (8) 把节假日列出来(用 pandas_market_calendars 或手查),对比 yfinance 的缺 row 情况
- (9) 更新
docs/daily/TR_PROGRESS.md,Day 3 标 ✅ - (10) 在本笔记最后加「实际执行记录」段
十一、明日预告
Day 4: 历史 bar 数据双线 — yfinance 批量下载 vs IBKR reqHistoricalData
明天我们做两件事:
- 用 yfinance 批量下载 S&P500 + Nasdaq100 全部成分股 10 年日线,建立本地数据湖(parquet 分区按 ticker)
- 学 IBKR 的
reqHistoricalData:限速、bar size、duration、whatToShow(TRADES / MIDPOINT / BID_ASK),以及如何避免被 IBKR 封 10 分钟 - 写一个简单的「双源对账」脚本:同样的 SPY,yfinance 和 IBKR 的 Adj Close 偏差超过多少 bp 算异常
并且会引出一个核心问题:当两个权威源给的复权价不一致,你信谁?
实际执行记录
启动一项填一项,时间戳 + 卡点。
- [hh:mm] uv 安装 — ...
- [hh:mm] venv + 包安装 — ...
- [hh:mm] yfinance SPY 下载成功 — ...
- [hh:mm] mini 任务 NAV 图生成 — ...
- [hh:mm] 复权对比实验(AAPL 2020-08-31)— ...
- 卡点 / 学到的:
总字数:约 6,200 字 今日完成度:理论 ✓ / 实操(你自己执行)/ 笔记 ✓