加载价格数据:CSV 文件与免费 API、复权与未复权价格、缺失值与时区
把价格文件和 API 下载的数据读入一个干净的 DataFrame,理解拆股和股息复权的原理,并正确处理数据缺口和时区。
你将学到什么
- 如何把 CSV 文件读入带有正确日期的 DataFrame,以及能修复常见导入问题的参数。
- 免费数据 API 能提供什么,它们的许可条款和默认设置意味着什么,以及如何缓存下载的数据。
- 拆股和股息复权因子如何计算,以及为什么回测需要复权价格,而实盘下单需要未复权价格。
- 如何发现并处理缺失值,以及如何在不移动 K 线的前提下设置和转换时区。
讲解
CSV 文件。CSV(comma-separated values,逗号分隔值)文件是纯文本文件,每根 K 线占一行。关键调用是:
df = pd.read_csv("data/raw/xyz.csv", parse_dates=["Date"], index_col="Date")
parse_dates 把文本列转换为时间戳,index_col 把它设为索引。常见修复:德国格式的导出文件使用 sep=";" 和 decimal=",",像 03.04.2025 这样的日期使用 dayfirst=True,解析出错时使用 date_format="%d.%m.%Y"。加载后,运行 df = df.sort_index()(有些导出文件按从新到旧排序),检查 df.index.is_unique,并用 df.columns = df.columns.str.lower() 把列名改为小写。
免费 API。API(application programming interface,应用程序编程接口)让代码可以向服务器请求数据。对于第一个项目,yfinance 库(Yahoo Finance 数据)是常见选择:yf.download("SAP.DE", start="2015-01-01");它面向个人研究,准确性不受保证。有一个默认设置很重要:截至 2026 年,auto_adjust=True,因此开盘价、最高价、最低价和收盘价都已经针对拆股和股息进行了复权,也没有“Adj Close”列;使用 auto_adjust=False 时,你会得到经拆股复权的 OHLC,外加一个同时包含股息调整的“Adj Close”列。提供免费额度的替代方案包括 Stooq(CSV 下载)、Alpha Vantage 和 Tiingo(需要注册和 API 密钥,请求次数限制严格);条款会变化,请查看当前条件,第 12 课会介绍密钥管理和速率限制。无论数据来源如何,都请把原始下载结果保存一次到 data/raw/(CSV 或 Parquet),然后基于该文件工作;数据供应商会修订历史数据,而请求额度十分有限。
复权与未复权。课程《系统化交易与回测》解释了拆股和股息为什么会扭曲回测;这里讲的是修正如何进行。对于 n 拆 1 的拆股,之前的每个价格都除以 n(成交量乘以 n)。对于在第 t 天除息的现金股息 D,标准方法是把第 t 天之前的所有价格乘以因子 1 − D ÷ C,其中 C 是除息日前最后一天的收盘价。复权会向过去逐次累积。由此产生的后果:每出现一次新的股息,复权后的历史数据都会改变;百分比收益率是正确的,而以欧元计的价格水平则不是。规则:用复权数据做回测,用未复权价格下真实订单,并且永远不要在同一个计算中混用两者。
缺失值。pandas 用 NaN 标记缺口。用 df.isna().sum() 找出它们。用 df["close"].ffill() 向前填充少量价格缺口是可以接受的,但永远不要向前填充成交量,也永远不要对价格进行插值,因为插值使用了未来的值,会植入前视偏差(第 7 课)。其他情况请用 df.dropna(subset=["close"]) 删除这些行,并记录删除了多少行。注意,旧的 fillna(method="ffill") 写法自 pandas 2.1 起已被弃用,不应再用于新代码;请写成 .ffill()。
时区。日线数据很少需要时区;日内数据则总是需要。tz_localize("Europe/Berlin") 给一个不含时区的索引附加时区,但不改变钟点时间;tz_convert("America/New_York") 则会改变钟点时间。如果错误地把 UTC 数据本地化为柏林时间,每根 K 线都会被悄悄移动一到两个小时,因此请查阅数据供应商的文档。欧洲和美国的夏令时切换日期不同,正是在这些日子里,处理不当的时间戳会让不同品种的数据错位。
实例演算
假设示例:一只股票星期一收盘价为 €50.00。一笔 €1.00 的股息在星期二除息,当天该股收于 €49.50。不计手续费和税费。
复权因子 = 1 − 1.00 ÷ 50.00 = 0.98。星期二之前的所有价格都乘以 0.98:星期一的收盘价变为 50 × 0.98 = €49.00;前一周 €48.00 的收盘价变为 48 × 0.98 = €47.04。
跨越除息日的收益率:未复权为 (49.50 − 50.00) ÷ 50.00 = −1.0%,看起来像亏损,但这是错误的。复权后为 (49.50 − 49.00) ÷ 49.00 = +1.02%,正确反映了收到的 €1.00 现金与 €0.50 的价格损失相抵(在 €50 上净得 +€0.50;之所以存在细微差别,是因为复权后的基数是 €49)。
再看一次 2 拆 1 的拆股:拆股前原始收盘价为 100、102、104,拆股后为 52、53。把拆股前的价格除以 2,得到 50、51、52、52、53,这是一个连续的序列。如果不这样做,pct_change() 会在拆股当天报告 −50% 的收益率,即一次从未发生过的暴跌。
常见错误
info()、head()、tail()、isna().sum(),并画一张收盘价折线图。练习活动
在纸上完成:一只股票连续两天收盘价为 €90.00 和 €93.00,随后进行 3 拆 1 拆股,之后收盘价为 €31.00 和 €30.38(假设数据)。在收盘价为 €30.38 的那一天,一笔 €0.60 的股息除息,而其前一天的收盘价为 €31.00。(a) 写出拆股前两个收盘价经拆股复权后的值。(b) 计算股息复权因子,并将其应用于除息日之前的全部三个收盘价。(c) 说明从 €31.00 收盘价到 €30.38 收盘价的原始收益率和完全复权后的收益率。
显示答案
(a) 除以 3:€90.00 → €30.00,€93.00 → €31.00。
(b) 因子 = 1 − 0.60 ÷ 31.00 = 0.98065(四舍五入)。应用后:30.00 × 0.98065 = €29.42;31.00 × 0.98065 = €30.40;除息日前 €31.00 的收盘价同样变为 €30.40。€30.38 的收盘价保持不变。
(c) 原始:(30.38 − 31.00) ÷ 31.00 = −2.00%。复权后:(30.38 − 30.40) ÷ 30.40 = −0.07%,接近持平:价格下跌的幅度大致等于股息金额,因此持有人的总回报几乎没有损失。
局限性:不同数据供应商对因子的四舍五入方式不同,有些还使用不同的参考价格;请始终如一地使用同一家供应商的复权序列。
要点回顾
- 用
parse_dates和index_col加载数据,然后排序、检查唯一性并统一列名。 - 只下载一次,把原始数据保存在本地,并阅读 API 的许可条款及其复权默认设置(yfinance:截至 2026 年为
auto_adjust=True)。 - 拆股因子为 1 ÷ n,股息因子为 1 − D ÷ C,均向过去应用;用复权价格做回测,用未复权价格进行交易。
- 最多只向前填充少量价格缺口,永远不要插值,永远不要向前填充成交量,并按照数据供应商实际使用的时区只做一次时区本地化。
下一步
第 5 课把价格转换为收益率:简单收益率与对数收益率、累计收益率、年化收益率和波动率,以及滚动窗口。
自测
回答问题后核对答案。你可以随意重试多次。
供您自行复习的自动自测。您的答案在浏览器中核对,我们不会查看或评估。这不是考试,也不是资格证书。
您的进度
“已完成”仅表示您将本课标记为已学完,并不是对您理解程度的测试。 学习进度和笔记仅保存在此设备的此浏览器中,而非账户中。