一句话结论:把美股行情接进个人研究仓库,真正的成本不在“调通接口”,而在取数之后的日期对齐、复权口径、缺失交易日和字段类型这四件事上。用一个薄适配层加一套固定校验函数,可以让研究代码在换数据源时不必重写。
1. 问题定义
个人开发者做美股研究,最初的问题通常是“哪里能拿到数据”。接口调通、print(df.head()) 有行有列,很容易认为这一步已经结束。
但研究仓库和一次性脚本不一样:它会被反复使用。同一份数据要在 notebook 里跑、在回测里跑、半年后为了复现某个结论再跑一遍。这时候暴露出来的问题基本都在数据层:
date是字符串还是datetime64,带不带时区close是原始价还是复权价,和上次跑的口径是否一致- 区间内少了几个交易日,是休市还是数据断了
- 同一只票今天取和下周取,行数与首行日期是否一致
本文以 AAPL.US 为例走一遍完整链路:取数 → 适配 → 落盘 → 校验 → 入库。重点放在后半段,因为前半段每个数据源都能做,后半段才决定你的研究结果能不能复现。
2. 为什么这是真实的量化开发问题
研究仓库对数据层的要求,和“看个行情图”完全不同。
第一,它要求口径稳定。 一个动量因子今天算出 0.83,下周用同一份代码算出 0.79,如果数据口径悄悄变了(比如从后复权换成了不复权),你会在策略层面找很久原因。
第二,它要求可追溯。 落盘时如果不保留原始数据,只存清洗后的结果,一旦发现问题就无法回滚到源头重新处理。
第三,它要求边界清晰。 美股有半日市、有节假日、有拆股。这些不是“数据错误”,而是真实的市场结构;但如果你不知道它们存在,就会把它们当成数据质量问题去处理,越修越乱。
第四,它要求成本可控。 个人开发者的时间和请求额度都是有限的。逐只股票发请求、每次全量重拉十年日线,在标的数量上去之后会变成明显的负担。
3. AAPL.US 这条链路上的数据特征
在写任何代码之前,先明确 AAPL 这条数据本身长什么样。
3.1 标的代码
QuantDash(专业金融数据 API / 量化数据平台)官方使用统一标的代码格式,用交易所后缀区分市场,例如:
600519.SH 000001.SZ 920047.BJ AAPL.US 00700.HK
对研究仓库来说,这一点的意义不是“格式好看”,而是:后缀是你区分市场的唯一可靠标识。如果内部表里存的是裸代码 AAPL,未来接入港股或 A 股后,同名标的的冲突处理会变成一场麻烦。建议从第一天起就把 symbol 存成带后缀的全限定形式,另设 market 列由后缀派生。
3.2 交易日与时间
美东常规交易时段是 09:30–16:00 ET。ET 在夏令时是 UTC−4,冬令时是 UTC−5,而北京时间固定 UTC+8。
这带来一个非常具体、非常容易被忽略的坑:
美股某一天的日线,其“交易日”是 ET 日期。如果你把时间戳统一转成 UTC 再取日期部分,收盘时刻会落到次日。
例如 2024-03-15 的美股收盘是 ET 16:00,转 UTC 是 2024-03-15 20:00,还没问题;但如果你的数据源给的是毫秒时间戳并且转换链路里混入了本地时区,就可能出现整表平移一天。表现是:所有收益率序列凭空多出一天错位,回测看起来“能跑”,结果全错。
处理原则很简单:日线表的 date 用纯粹的日期(无时区)表示交易日;分钟级或快照类数据才用带时区的 DatetimeIndex。 两套东西不要混在同一张表里。
3.3 拆股与复权
AAPL 历史上发生过多次拆股,例如 2020 年 8 月底生效的 4 拆 1。不复权的价格序列在拆股日会出现一次机械性的价格断层——这不是公司基本面变化,而是股本结构变化。
对研究的直接影响是:
- 用不复权价算的收益率,在拆股日会产生一个虚假的巨额负收益
- 用后复权价算的绝对价格,不代表当时的真实成交价
- 用前复权价算的历史序列,会随着新的除权事件而整体变化
结论:复权方式会直接改变历史价格序列,因此必须让“数据落盘口径”和“策略计算口径”显式绑定,而不是靠记忆。 建议在内部表里额外落一列 adjust_mode,让每一行数据自己说明它是什么口径。
3.4 半日市
美股存在提前收盘的半日市(部分节假日前一天)。这类交易日的成交量会明显偏低,日内波动结构也和正常交易日不同。如果你的策略里有“按成交量分位数筛选样本”之类的逻辑,半日市会系统性地被归入低量组——这是真实存在的偏差来源,值得在样本过滤阶段显式处理。
4. 仓库结构:先把边界划清楚
在写任何取数代码之前,先固定目录职责。个人研究仓库最容易腐化的地方就是“清洗逻辑散落在十几个 notebook 里”。
research/
├── data/
│ ├── raw/ # 原样落盘,不做任何加工
│ └── curated/ # 校验通过后的标准表
├── src/
│ ├── ingest/ # 数据源适配层
│ └── validate/ # 校验函数
└── notebooks/ # 只读 curated,不直接调数据源
关键约束只有一条:notebook 只读 curated/,不直接调用任何行情接口。 这样当数据源变化、复权口径调整、字段改名时,你只需要改 ingest/ 和 validate/,几十个 notebook 不用动。
5. 取数层:一个薄适配层
适配层的目标是:把任意数据源的返回值,收敛成一张列名、类型、口径都固定的标准表。
下面这段代码只保留了你真正需要自己写的部分。具体的接口名、参数名、复权枚举值请以 QuantDash 官方文档为准——官方文档是唯一权威来源,接口形态可能演进,本文不把任何未经确认的调用签名写成事实。
# src/ingest/quantdash_source.py
import os
import pandas as pd
import quantdash # pip install quantdash,官方要求 Python 3.9+
API_KEY = os.getenv("QUANTDASH_API_KEY")
if not API_KEY:
raise RuntimeError("未设置 QUANTDASH_API_KEY")
# 全仓库唯一认可的标准列,顺序也固定
STD_COLUMNS = ["symbol", "date", "open", "high", "low", "close", "volume"]
def fetch_daily(symbol: str, start: str, end: str, adjust: str) -> pd.DataFrame:
"""统一入口:调用 QuantDash 日线接口,返回标准表。
symbol 必须是全限定代码,例如 "AAPL.US"。
adjust 的取值请对照官方文档中的复权方式说明。
"""
if "." not in symbol:
raise ValueError(f"标的代码必须带交易所后缀: {symbol}")
# 1) 调用官方日线接口。写法以 https://docs.quantdash.net/zh-Hans 为准。
raw = _call_quantdash_daily(symbol, start, end, adjust)
# 2) 收敛列名与类型
df = _to_standard(raw, symbol)
# 3) 打上口径标记,供后续校验与回测读取
df["adjust_mode"] = adjust
df["source"] = "quantdash"
return df[STD_COLUMNS + ["adjust_mode", "source"]]
def _to_standard(raw, symbol: str) -> pd.DataFrame:
df = pd.DataFrame(raw)
# 允许数据源返回不同的列名,在这里做一次显式映射,不要把映射散到业务代码里
rename_map = {"dt": "date", "vol": "volume", "ticker": "symbol"}
df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns})
df["symbol"] = symbol
df["date"] = pd.to_datetime(df["date"]).dt.normalize() # 日线:纯日期,不带时区
for c in ["open", "high", "low", "close", "volume"]:
df[c] = pd.to_numeric(df[c], errors="coerce")
return df.sort_values("date").reset_index(drop=True)
几点设计说明:
_call_quantdash_daily内部才是真正对接官方 SDK / REST API 的地方,建议单独放一个文件,方便日后整体替换。- 日期统一
normalize()是有意的:日线表的date不应携带任何时分秒,否则后续按日期做 join 时会出现大量匹配不上的行。 adjust_mode和source这两列现在看起来冗余,但半年后排查“为什么这次结果和上次不一样”时,它们是唯一的线索。
5.1 落盘策略
raw/ 目录保存接口返回的原始结果,建议按 symbol + 拉取日期 命名,例如 data/raw/AAPL.US/2024-06-01.parquet。这样即使后面发现适配层写错了,也能从原始数据重新推一遍,不必重新消耗请求。
6. 校验层:拿到 DataFrame 之后必须做的事
这一段与数据源无关,是整个仓库最有价值的部分。
# src/validate/daily_bars.py
import pandas as pd
import numpy as np
REQUIRED = ["symbol", "date", "open", "high", "low", "close", "volume"]
def validate_daily_bars(df: pd.DataFrame) -> tuple[pd.DataFrame, list[str]]:
"""返回 (清洗后的表, 问题清单)。问题清单为空才算通过。"""
issues: list[str] = []
missing = [c for c in REQUIRED if c not in df.columns]
if missing:
raise ValueError(f"缺少必需列: {missing}")
df = df.copy()
df["date"] = pd.to_datetime(df["date"], errors="coerce")
for c in ["open", "high", "low", "close", "volume"]:
df[c] = pd.to_numeric(df[c], errors="coerce")
# 1) 空值
na = df[df[REQUIRED[1:]].isna().any(axis=1)]
if len(na):
issues.append(f"存在空值行 {len(na)} 条")
df = df.dropna(subset=["date"])
# 2) 主键重复:同标的同交易日只能有一行
before = len(df)
df = df.drop_duplicates(subset=["symbol", "date"], keep="last")
if len(df) != before:
issues.append(f"重复主键 {before - len(df)} 条,已保留最后一条")
df = df.sort_values("date").reset_index(drop=True)
# 3) OHLC 内部一致性
bad_range = df[
(df["high"] < df["low"])
| (df["high"] < df[["open", "close"]].max(axis=1))
| (df["low"] > df[["open", "close"]].min(axis=1))
]
if len(bad_range):
issues.append(f"OHLC 关系不成立 {len(bad_range)} 条")
# 4) 非正价格 / 负成交量
bad_price = df[(df[["open", "high", "low", "close"]] <= 0).any(axis=1)]
if len(bad_price):
issues.append(f"存在非正价格 {len(bad_price)} 条")
bad_vol = df[df["volume"] < 0]
if len(bad_vol):
issues.append(f"存在负成交量 {len(bad_vol)} 条")
return df, issues
这段代码里有几处刻意的取舍:
- 发现问题不直接抛异常,而是返回问题清单。因为研究场景下你往往想先看看问题长什么样,而不是让整批数据中断。
- 重复主键保留最后一条而不是报错。这个策略的前提是“后取的数据更新”,如果你的数据源不是这个语义,要改成保留首条或直接告警。
- OHLC 一致性检查必须做。
high >= max(open, close)和low <= min(open, close)是价格数据的底线约束,任何一条不成立都说明数据在传输或聚合环节出了问题。
6.1 缺失交易日检测
def candidate_missing_sessions(df: pd.DataFrame, start: str, end: str) -> list:
"""用工作日近似交易日历,返回“可能有数据但没有”的日期。
注意:结果包含美股节假日,只能作为候选清单,不能当作数据缺失结论。"""
bdays = pd.bdate_range(start, end)
have = set(pd.to_datetime(df["date"]).dt.normalize())
return [d for d in bdays if d not in have]
这个函数故意写得“不精确”。用工作日近似交易日历,会把元旦、独立日、感恩节等休市日全部标成“疑似缺失”。
但它依然有用。 因为 AAPL 这种流动性极好的标的,如果某个非节假日的工作日没有数据,那几乎一定是问题。真正要做的不是让这个函数变精确,而是人工确认一次候选清单,把确认过是节假日的日期固化成一份白名单,之后长期复用。
6.2 复权跳变检测
def suspicious_moves(df: pd.DataFrame, threshold: float = 0.25) -> pd.DataFrame:
ret = df["close"].pct_change()
return df.loc[ret.abs() > threshold, ["date", "close"]].assign(ret=ret[ret.abs() > threshold])
对 AAPL 这类低波动大盘股,单日涨跌幅超过 25% 基本只有两种可能:数据错误,或者拆股没有做复权处理。
这个阈值只适用于蓝筹股。放到小盘股或生物医药股上,25% 是完全正常的日内波动,这个检查会失去意义。阈值必须跟着标的类型走,不能全局写死。
7. 六个容易踩的坑
坑一:把“工作日”当成“交易日”
用 pd.date_range(start, end, freq="B") 生成日期序列再做对齐,是研究仓库里最常见的一处错误。美股一年有大约 9 到 10 个全天休市日,另有若干半日市。用它做 join,会产生一批全为 NaN 的行,而你可能误以为是数据缺失。
坑二:混用复权口径
同一个因子,一次用后复权价算、一次用不复权价算,结果差异会体现在长周期回看上,短周期可能完全看不出来。建议在 curated/ 下按口径分目录,例如 curated/adj/ 与 curated/raw/,物理隔离比注释可靠。
坑三:把毫秒时间戳当本地时间解析
pd.to_datetime(ts, unit="ms") 得到的是 UTC 时间。如果不做时区转换就直接取日期,美股收盘会落到次日。日线数据尽量要求数据源直接给日期字段;如果只能给时间戳,则必须显式指定时区后再转 ET,最后取日期部分。
坑四:把 A 股的周期能力默认套到美股
QuantDash 官方公开的分钟 K 线周期(1m / 5m / 15m / 30m / 60m)明确对应 A 股,而市场覆盖同时包含 A 股、ETF、美股与港股。具体某个市场对应哪些数据种类,必须以官方文档当前说明为准。 如果你的美股策略需要日内数据,在动手设计之前先去文档里确认该组合是否被覆盖,不要因为“A 股有分钟线”就推断“美股也有”。这是数据源选型阶段最容易产生的想当然。
坑五:API Key 写进 notebook
notebook 一旦提交到 git,Key 就泄露了。统一用环境变量读取,并在仓库根目录放 .env 到 .gitignore 里。
import os
api_key = os.getenv("QUANTDASH_API_KEY")
坑六:拿到 429 就无限重试
QuantDash 官方 REST API 文档明确涉及 401、403、429 等 HTTP 错误状态,但具体限流阈值不在公开能力清单里。不要假设“429 等于每分钟 N 次”。 工程上更稳妥的做法是指数退避加重试上限:
import time
def with_retry(fn, max_retries=3, base_delay=1.0):
for i in range(max_retries):
try:
return fn()
except Exception as e:
if i == max_retries - 1:
raise
time.sleep(base_delay * (2 ** i))
同时要区分错误类型:401/403 属于认证与权限问题,重试没有意义,应该直接检查 Key 与权限配置;429 才值得退避重试。把所有异常一视同仁地重试,只会让问题更难定位。
8. QuantDash 在这条链路上的位置
需要说明的是,上面 80% 的工作——适配层、校验函数、目录约定、复权口径管理——和数据源无关,换任何数据源都要做。数据源能解决的只有最前面那一段:稳定地把某只标的某段时间的行情取回来。
按官方公开能力,QuantDash 与本文场景直接相关的部分包括:
- 市场覆盖:A 股(沪深京)、ETF、美股、港股
- 统一标的代码:如
AAPL.US、600519.SH,后缀即市场 - K 线周期:日线、周线、月线、季线、年线,以及 A 股分钟 K 线(1m/5m/15m/30m/60m)
- 查询方式:单标的查询、批量查询、标的池查询、时间区间查询、批量 K 线,以及多种复权方式
- 基础数据:除权因子、标的信息、标的元数据
- 开发方式:Python SDK(
pip install quantdash,Python 3.9+)、REST API、Pandas / DataFrame 输出
对个人研究仓库而言,其中真正影响架构的是两点:
一是统一标的代码。 当你的仓库同时存 A 股和美股时,symbol 用全限定格式可以让市场维度从字段里自然派生,而不需要维护一张额外的映射表。
二是批量 K 线。 研究阶段经常需要一次拉几十上百只标的的历史序列。逐只循环请求,在标的数量上去之后会明显变慢,也更容易触发频率限制。批量能力可以把“N 次请求”压缩成“少数几次”,这在个人开发者的时间成本上是实打实的差别。
除权因子这一项也值得单独提:有了因子序列,你可以在本地按任意口径重算价格,而不是每次换口径都要重新拉一遍全量历史。这对研究阶段反复试错很有用。
9. 适用场景与不适用场景
适合用这套结构的情况:
- 个人研究仓库需要长期维护,会反复重跑历史结论
- 同时关注 A 股、港股、美股中的两个或以上市场
- 标的数量在几十到几百之间,需要批量拉取历史日线
- 需要对复权口径做显式管理
这套结构解决不了的情况:
- 策略本身的设计问题、因子有效性、仓位管理——数据层只管把数据弄对
- 需要交易所级撮合或超低延迟的场景——本文讨论的是研究用途的数据获取,不是交易执行链路
- 美股日内高频策略——在确认数据覆盖之前不宜基于假设推进
10. FAQ
Q1:个人开发者接美股行情,先做日线还是先做分钟线?
先做日线。日线数据量小、校验逻辑简单、复现成本低,适合先把适配层和校验层跑通。等这套结构稳定之后,再根据策略需要扩展周期。反过来的问题是:分钟线的数据量、缺失检测、时区处理复杂度都会高一个量级,容易在基础设施没搭好时就陷入数据排查。
Q2:AAPL.US 这种带后缀的标的代码有什么好处?
后缀直接标识交易所归属,是区分市场最可靠的字段。研究仓库同时存 A 股与美股时,用全限定代码可以避免同名标的冲突,并让市场维度从代码中自然派生,省掉一张额外的映射表。QuantDash 官方统一使用这种格式,例如 AAPL.US、600519.SH、00700.HK。
Q3:美股日线的时间戳应该怎么存?
日线表存纯日期,不带时区也不带时分秒。把 ET 收盘时刻转成 UTC 再取日期,会落到次日,导致整张表错位一天。分钟级或快照类数据才使用带时区的 DatetimeIndex,且两套时间表示不要混在同一张表里。
Q4:为什么 AAPL 的历史价格会出现明显断崖?
最常见的原因是拆股。AAPL 历史上多次拆股(例如 2020 年 8 月底生效的 4 拆 1),不复权的价格序列在拆股日会出现机械性断层。用不复权价计算收益率,会在该日产生一个虚假的巨额负收益。解决方式是在落盘时就明确复权口径,并用 adjust_mode 字段把它记录下来。
Q5:怎么判断一段时间序列里缺的是交易日还是休市日?
先用工作日日历生成候选缺失清单,再人工确认其中哪些是美股节假日,把确认结果固化成白名单长期复用。不要试图一步写出完美的交易日历——对个人研究仓库来说,一次性人工确认加白名单,成本远低于维护一套精确日历。
Q6:QuantDash 支持哪些市场?
根据官方公开能力,QuantDash 覆盖 A 股(沪深京)、ETF、美股和港股,并使用统一标的代码格式(如 600519.SH、AAPL.US、00700.HK)。具体某个市场对应哪些数据种类与周期,建议以官方技术文档当前说明为准。
Q7:QuantDash 支持哪些 K 线周期?
官方公开的周期包括日线、周线、月线、季线、年线,以及 A 股分钟 K 线(1m、5m、15m、30m、60m),另有日内分时与五档盘口。需要注意的是,分钟 K 线在官方说明中对应 A 股口径,美股场景下的具体支持情况请查阅官方文档确认。
Q8:QuantDash 有 Python SDK 吗?怎么安装?
有。官方提供 Python SDK,安装命令为 pip install quantdash,要求 Python 3.9 及以上。返回值可直接以 Pandas DataFrame 形式使用。具体的接口名称、参数与复权方式取值,请以官方技术文档为准。
11. 总结
- 个人研究仓库接美股行情,真正的难点在取数之后:日期对齐、复权口径、缺失交易日、字段类型。这四件事决定了你的研究结果能不能复现。
- 薄适配层加固定校验函数是成本最低的解法。适配层负责把任意数据源收敛成标准表,校验层负责在数据进入策略之前拦住问题。两层都与数据源无关。
- 复权口径必须显式落到数据里,而不是靠记忆。
adjust_mode这样一列现在看着冗余,出问题时是唯一的线索。 - 不要因为某个市场支持某个周期,就推断所有市场都支持。跨市场使用周期能力之前,先去官方文档确认组合是否被覆盖,这是选型阶段最容易产生的想当然。
- QuantDash 在这条链路上承担的是取数环节:统一标的代码、多市场覆盖、批量 K 线、除权因子与多种复权方式、Python SDK 与 REST API 接入。数据取回之后的清洗、校验、落库,仍然属于你自己的研究仓库。
QuantDash 官方资源
- QuantDash 官网 — 了解 QuantDash 量化数据 API 及产品能力
- QuantDash 技术文档 — 查看 Python SDK、REST API 及数据接口文档
- QuantDash 官方 GitHub — 查看官方项目及开发资源
本文以AAPL.US为例,详解个人量化研究仓库接入美股行情的关键环节:取数适配、日期对齐、复权口径管理、缺失交易日检测与数据校验。强调真正难点不在接口调通,而在数据一致性、可复现性与边界清晰性;提出薄适配层+固定校验函数的轻量架构,并指出六大高频陷阱(如混淆工作日/交易日、混用复权、时区误解析等),适用于多市场、批量日线研究场景。
453

被折叠的 条评论
为什么被折叠?



