① 索引时区随调用方式变
history() 返回带交易所时区的索引(实测美股 America/New_York、A 股 Asia/Shanghai、港股 Asia/Hong_Kong);download() 的日线结果默认 tz-naive。两者直接拼接会因为时区不同而报错或错位。
yfinance 取数 · history 与 download 的返回结构
很多人刚开始用这个库时卡住,不是取不到数,而是取到的 DataFrame 形状和想象的不一样:history() 是单层列、索引带交易所时区;download() 是多层列,而且只传一只股票也仍然是两层。更隐蔽的是 end 是开区间、日线批量下载的索引是 tz-naive、跨市场下载会返回并集并把 Volume 的 dtype 抬成 float。本页把官方文档一句带过、但在代码里会真实咬人的细节逐条列清楚。
下面代码块里的注释都是本站实测结果(0.2.58 与 1.7.0 一致),不是推测。
import yfinance as yf
# ① 单标的:单层列 + 带时区的索引
df1 = yf.Ticker("AAPL").history(period="1mo")
# shape=(22, 7) columns=[Open, High, Low, Close, Volume, Dividends, Stock Splits]
# index=DatetimeIndex name="Date" tz=America/New_York time=00:00:00
# ② 批量:两层列 + tz-naive 的索引
df2 = yf.download(["AAPL", "MSFT"], period="1mo", progress=False)
# shape=(22, 10) columns=('Close','AAPL'), ('Close','MSFT'), ...
# index=DatetimeIndex name="Date" tz=None
# df2.columns.nlevels == 2 -> Truehistory 默认 7 列(auto_adjust=True 时没有 Adj Close);②download 即便只有一只标的也是 nlevels=2;③两者的索引时区默认不同——这一点在拼接多个来源的数据时最容易出错。下表每一行都对应本站一次真实调用;写断言前请先对照这张表。
| 调用 | 实测列数 | 实测列清单 | 什么情况下你会需要它 |
|---|---|---|---|
history(period="1mo") | 7 | Open, High, Low, Close, Volume, Dividends, Stock Splits | 默认口径:复权后价格 + 分红拆股两列,适合直接算收益 |
history(period="1mo", auto_adjust=False) | 8 | 上列 + Adj Close | 要同时看到真实收盘价与复权收盘价 |
history(period="1mo", auto_adjust=None) | 8 | 同上(显式传 None 实测仍保留 Adj Close) | None 并不等同于 True,别想当然 |
history(period="1mo", actions=False) | 5 | Open, High, Low, Close, Volume | 只想要纯净价格表,不要事件列 |
history(period="1mo", repair=True) | 8 | 上列 + Repaired?(bool) | 怀疑数据有价格错误时,用它标记哪些行被修过 |
Ticker("SPY").history(period="1mo") | 8 | 7 列 + Capital Gains | ETF/基金会多出资本利得分派列 |
download([...], actions=True) | 多层列 | 字段包含 Dividends / Stock Splits(各标的各一列) | 批量取数同时要事件列 |
download([...], auto_adjust=False) | 多层列 | 字段包含 Adj Close | 批量取数时保留复权列 |
时区、区间边界、索引名——这三处决定了你能不能把两张表正确对齐。
history() 返回带交易所时区的索引(实测美股 America/New_York、A 股 Asia/Shanghai、港股 Asia/Hong_Kong);download() 的日线结果默认 tz-naive。两者直接拼接会因为时区不同而报错或错位。
实测日线索引的时间部分都是 00:00:00,日期代表的是交易日而不是某个时点。拿日线去和分钟数据对齐、或按小时聚合之前,先把语义想清楚。
start 含当天、end 不含当天。实测 end="2026-09-20" 最后一行是 2026-09-18;同一区间用 start="2026-09-01", end="2026-09-20" 得到 13 行,首行正是 09-01。
period 的形参默认值在源码里是一个字符串哨兵 (period_default,其值就是 "1mo if start & end None"),实现靠它判断「用户到底有没有指定周期」。所以 period 与 start/end 同时传入时,行为以官方实现为准,不要凭直觉猜。把不同市场的标的放进同一个 download,得到的是「并集表」。
| 场景 | 实测行数 | 索引 | 副作用与处理建议 |
|---|---|---|---|
download("AAPL", period="1mo") | 22 | tz-naive | 仍是两层列,别用 df["Close"] 直接取 |
download(["AAPL","MSFT"], period="1mo") | 22 | tz-naive | 列数 = 字段数 × 标的数(实测 10 列) |
download("AAPL MSFT NVDA", period="1mo") | 22 | tz-naive | 空格分隔与列表等价,实测 15 列 |
download(["AAPL","600519.SS"], period="1mo") | 23 | tz-naive(并集) | 缺失侧为 NaN;Volume dtype 由 int64 变 float64 |
只有 A 股:Ticker("600519.SS").history(period="1mo") | 17 | Asia/Shanghai | 同一 1 个月窗口只有 17 个交易日,与美股 22 行的差异来自交易日历 |
| 失败标的混入批量请求 | 0 或正常标的行数 | 视情况 | 实测会打印 1 Failed download: 并返回空表,而不是抛异常 |
df.isna().sum() 的缺失分布;②需要整数成交量就显式 astype,但含 NaN 的行不能直接转整数;③不同市场建议分开下载再按各自日历处理,不要把并集表当成交叉验证依据。下面三段覆盖「单标的」「批量拍平」「落盘前检查」三种场景,可直接粘进项目再改。
import yfinance as yf
# 1) 单标的:两种口径各取一张,用断言确认差异只在 Adj Close
t = yf.Ticker("AAPL")
raw = t.history(period="1y", auto_adjust=False)
adj = t.history(period="1y", auto_adjust=True)
assert set(raw.columns) - set(adj.columns) == {"Adj Close"}
# 2) 批量:group_by="ticker" 后按标的切,dropna 掉跨日历产生的空行
data = yf.download(["AAPL", "MSFT", "NVDA"], period="1y",
group_by="ticker", progress=False, threads=True)
for sym in ["AAPL", "MSFT", "NVDA"]:
one = data[sym].dropna(how="all")
assert one.index.is_monotonic_increasing
# 3) 落盘前的最小检查(完整 13 项见数据审计页)
def quick_check(df):
price_cols = [c for c in ("Open", "High", "Low", "Close") if c in df]
return {
"rows": len(df),
"dup_index": int(df.index.duplicated().sum()),
"index_tz": str(getattr(df.index, "tz", None)),
"has_adj_close": "Adj Close" in df.columns,
"negative_price": bool((df[price_cols] < 0).any().any()),
}
下面的回答都指向可核验的官方文件或本站实测;与官方表述冲突时,以官方仓库与 docs 为准。
yf.download("AAPL") 只传一只股票,也返回两层列?这是 download 的既定行为:它统一返回 MultiIndex(轴顺序为「字段 → 标的」)。本站实测 0.2.58 与 1.7.0 都是 nlevels=2,列名形如 ('Close','AAPL')。1.7.0 的签名里还多了 multi_level_index=True 这个显式参数。要单层列就用 history(),或自己对列做 droplevel。
group_by 到底影响什么?只影响列的轴顺序,不影响数值。默认 group_by="column" 得到 ('Close','AAPL');改成 group_by="ticker" 得到 ('AAPL','Open')。源码里对 group_by == "column" 的多层列做了 swaplevel(0, 1),这就是两种顺序的来源。
end 写 2026-09-20,为什么最后一行是 09-18?因为 end 是开区间。官方签名文档原文:end : str … exclusive. E.g. for end="2023-01-01", the last data point will be on "2022-12-31"。本站实测 end="2026-09-20" 时最后一行是 2026-09-18(周末休市),而 start 是闭区间,区间起点的当天是包含在内的。
download 出来的日期没有时区,history 有,正常吗?正常,由 ignore_tz 决定。官方文档写明:默认值取决于 interval——日内为 False、日线及以上为 True;为 True 时索引是 tz-naive,为 False 时索引会转换到所请求标的里最常见的交易所时区。本站实测日线 download 的索引 tz=None,而 history 是 America/New_York。
两个市场的交易日历不同。本站实测 yf.download(["AAPL","600519.SS"], period="1mo") 返回 23 行(单独取美股是 22 行、单独取 A 股是 17 行),按日期取并集,缺失侧为 NaN。随之而来的副作用是 Volume 的 dtype 从 int64 变成 float64——做整数运算前要先处理。
官方签名文档直接写了:Intraday data cannot extend last 60 days(日内数据不能超过最近 60 天)。另外官方还说明 30m 是按 15 分钟数据取回后重采样的,用于绕开 Yahoo 的一个接口缺陷。本站实测 period="5d", interval="1m" 返回 1,949 行、索引名为 Datetime;period="40d", interval="1m" 返回空表。