返回交易笔记
TR Day 3

Python 量化环境 — pandas / numpy / yfinance 数据栈

Python 版本选型、虚拟环境对比、数据源生态地图、复权与时区等数据陷阱

2026-05-12
Phase 1: 基础与工具链
Python311pandasnumpyyfinancepyarrowDataSourceSurvivorshipBiasAdjusted

日期: 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 pythonapt install python3 装到什么算什么。对量化场景这是一个有后果的决定——很多核心库(vectorbt、qlib、zipline-reloaded、numba、torch 某些组合)的兼容性曲线滞后于 CPython 主线半年到一年。

1.1 截至 2026 年中的兼容性现状

用途Python 3.10Python 3.11Python 3.12Python 3.13
ib_insyncIBKR API 封装✓(asyncio API 已稳)实测可跑但 wheel 有限
pandas 2.x数据框
numpy 1.26 / 2.x数值
pyarrow 16+列存 / parquet部分版本
yfinance 0.2.x行情下载
vectorbt回测△ 部分需要 numba 升级✗ numba 不支持
numbaJIT✓(0.59+)
qlib微软因子框架△ requirements 经常落后
zipline-reloadedQuantopian 后裔
pytorch 2.x深度学习
backtrader老牌回测✓(无人维护但能跑)

1.2 决策

强烈建议:Python 3.11

理由:

  1. vectorbt + numba 完整可用:3.12 上 numba 0.59 才追上,但 qlib / vectorbt 的某些 transitive dep 会卡住。3.11 是当前「装啥都不报错」的最稳版本。
  2. 比 3.10 快 10–60%:CPython 3.11 的 specializing adaptive interpreter 对回测这种纯 Python 内层循环提速极明显。3.10 → 3.11 是过去 10 年 CPython 单次升级幅度最大的一次。
  3. 避开 3.13 的 free-threaded 早期坑:3.13 引入实验性 free-threaded 模式(PEP 703),但绝大多数 C 扩展还没适配 GIL-free,量化堆栈装不全。
  4. 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 + piprequirements.txt(手维护)极低入门够用,复现性差
conda / mambaenv.yml当年的标配,但和 pip 混用易 break
poetrypoetry.lock 强制生产 OK,初学有点重
uv (Astral)极快(10–100x pip)uv.lock2026 年的最佳选择
pdmpdm.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 那套全家桶——那种装法会让你两个月后想清理时痛不欲生。

版本建议用途必装?
pandas2.2+DataFrame,时序所有操作的中枢✓ 必装
numpy1.26+ 或 2.x数值底层,pandas / scipy 的依赖✓ 必装(被传递安装)
yfinance0.2.40+免费雅虎行情下载✓ 必装
pyarrow16+parquet/feather 列存读写✓ 必装(pandas 2.x 也越来越依赖它)
matplotlib3.8+画图,简陋但够用✓ 必装
jupyterlab4.x探索式分析推荐
ib_insync0.9.86IBKR 数据/下单Day 1 已装
scipy1.13+统计、优化、信号处理Day 4 装
statsmodels0.14+OLS、ADF 平稳性、ARCHDay 12 因子分析时装
vectorbt0.27+向量化回测Day 30+ 装
backtrader1.9.78事件驱动回测(备选)可选
seaborn0.13+统计可视化可选
plotly5.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-30yfinance-免费、量够、坑可学
Day 31-60yfinance + IBKR Histpolygon trial开始要复权对账、期权链
Day 61-90IBKR Hist + polygonyfinance(仅探索)实盘对账必须用执行端权威源

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-28499.23~124.0
2020-08-31129.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 上要么搜不到,要么数据残缺。

对策(按成本递增)

  1. 学习阶段先承认这个 bias,回测结果心里打 7-8 折
  2. CRSP(学术标准,贵)或 Norgate Data(个人量化友好,~$70/月美股)拿 point-in-time 成分股历史
  3. 用 Wikipedia 的「S&P500 historical changes」表做手工 hack,时间线粗糙但能用
  4. 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 个值得注意的细节

  1. 本地缓存:parquet 一旦下载就重用。yfinance 接口不稳,缓存能让你免于「正在写代码 Yahoo 后端 504」的尴尬。
  2. auto_adjust=True:上节讲了为什么。
  3. tz_localize(None):把 yfinance 给的 NY 时区 timestamp 统一为 UTC naive。否则 join FRED 数据时会撞到 tz mismatch。
  4. 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=False vs True,对比 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

明天我们做两件事:

  1. 用 yfinance 批量下载 S&P500 + Nasdaq100 全部成分股 10 年日线,建立本地数据湖(parquet 分区按 ticker)
  2. 学 IBKR 的 reqHistoricalData:限速、bar size、duration、whatToShow(TRADES / MIDPOINT / BID_ASK),以及如何避免被 IBKR 封 10 分钟
  3. 写一个简单的「双源对账」脚本:同样的 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 字 今日完成度:理论 ✓ / 实操(你自己执行)/ 笔记 ✓