Yahoo Finance、akshare、Polygon 数据太乱怎么办?美股行情清洗实战:字段映射、时间对齐、复权处理

5 阅读11分钟

开篇:每个美股数据源都有自己的“脾气”

做美股量化的朋友,一定遇到过这些场景:

  • yfinance 拉数据,发现 Adj CloseClose 有时候一样,有时候不一样,到底该信哪个?
  • akshare 拿美股历史,返回的列名全是中文(开盘收盘),而且分页还只能拿 200 条。
  • Polygon.io 的聚合接口,发现 1 分钟 K 线居然“跳分钟”了,是不是数据丢了?

每个数据源都有自己的“脾气”。如果你没有一套统一的清洗框架,每次换源都要重写一遍解析逻辑,不仅浪费时间,还容易埋下数据质量隐患。

另外,有读者问到美股数据源的安全性。与国内部分依赖爬虫、接口不稳定的数据源不同,主流美股数据源(如 Polygon、TickDB)普遍提供标准的 REST/WebSocket API,采用 TLS 加密传输和 API Key 认证,数据传输安全性更高。受 SEC 监管的数据服务商在数据完整性、服务可用性(SLA)方面也有更严格的承诺。

今天这篇文章,我们就以 yfinance、akshare、Polygon.io 三个主流美股数据源为例,手把手教你如何构建一套通用的数据清洗管道。内容包括:

  • 字段映射:把千奇百怪的字段名统一成标准模型
  • 时间对齐:处理时区、时间戳格式,以及不同频率的对齐技巧
  • 复权处理:拆股、分红后价格怎么调
  • 缺失值处理:分钟数据缺失、节假日跳空
  • 统一清洗框架:适配器模式 + 交易日历 + 健壮的错误处理

最后,我们会聊聊如何通过 TickDB 这类内置标准化能力的数据源,省去 90% 的清洗代码。


一、先定义你的“标准模型”

无论接入哪个数据源,最终都要转换成内部统一的格式。推荐以下标准模型(以 K 线为例):

字段名类型说明单位/格式
symbolstring统一 ticker(如 AAPL.US带市场后缀
timestampintUnix 毫秒时间戳UTC
openfloat开盘价美元/股
highfloat最高价美元/股
lowfloat最低价美元/股
closefloat收盘价美元/股
volumeint成交量

有了这个标准,后续每个数据源的适配器只需要做一件事:把源数据映射到这个模型


二、三个美股数据源的真实“乱象”与清洗方案

2.1 Yahoo Finance(yfinance)

原始数据结构

yfinance 是目前最流行的免费美股数据 Python 库。它返回的是一个 Pandas DataFrame,典型字段如下(以日线为例):

DateOpenHighLowCloseAdj CloseVolume
2025-04-01175.00176.50174.80176.20176.2012345678
  • 字段名:首字母大写,Adj Close 带空格。
  • 时间索引:Datetime 对象,默认时区为美东时间(EST/EDT)。
  • 成交量:单位是“股”。
  • 复权:提供 Adj Close 列,但 OpenHighLowClose 默认是已复权的(auto_adjust=True),Adj Close 实际上是冗余列。

常见问题与清洗要点

① 复权字段混淆
当设置 auto_adjust=False 时,会返回原始未复权价格,以及单独的 Adj Close 列。很多用户发现 Adj CloseClose 在某些时间段完全相同,以为出了 bug。实际上,这是因为该时间段内没有发生拆股或分红,复权价自然等于未复权价。清洗时需明确:默认使用 Close(已复权),无需额外处理。

② 时区问题
yfinance 返回的时间索引是带时区的(美东时间)。如果你在 UTC 时区的服务器上运行,直接与其他数据源合并会导致错位。解决方案:统一转换为 UTC 毫秒时间戳。

# 将 yfinance DataFrame 的索引转为 UTC 毫秒
df.index = df.index.tz_convert('UTC')
df['timestamp'] = df.index.astype('int64') // 10**6

③ 缺失数据
部分历史日期(尤其是早于 2000 年的数据)可能缺失。yfinance 不会自动填充,需在清洗层处理。对于回测场景,建议使用前向填充(ffill)或直接剔除。

清洗代码示例(yfinance → 标准模型)

import yfinance as yf
import pandas as pd

def clean_yfinance(ticker, start, end):
    raw = yf.download(ticker, start=start, end=end, auto_adjust=True)
    # 重置索引,将日期转为列
    df = raw.reset_index()
    # 时间戳转换:美东时间 -> UTC 毫秒
    df['timestamp'] = pd.to_datetime(df['Date']).dt.tz_localize('US/Eastern').dt.tz_convert('UTC').astype('int64') // 10**6
    # 重命名并选择标准字段
    df = df.rename(columns={
        'Open': 'open',
        'High': 'high',
        'Low': 'low',
        'Close': 'close',
        'Volume': 'volume'
    })
    df['symbol'] = ticker
    return df[['symbol', 'timestamp', 'open', 'high', 'low', 'close', 'volume']]

2.2 akshare(美股部分)

原始数据结构

akshare 是一个国内开源的金融数据库,通过聚合多个公开数据源(如东方财富、新浪)提供接口。以美股历史数据为例,使用 stock_us_hist 接口(东方财富源),返回的 DataFrame 字段为中文:

日期开盘收盘最高最低成交量成交额...
2025-04-01175.00176.20176.50174.80123456782.17e9...
  • 字段名:全中文。
  • 时间:字符串格式 YYYY-MM-DD
  • 成交量:经实测,stock_us_hist 返回的成交量单位为“股”,但其他接口可能为“手”,建议在清洗层通过配置化方式处理。
  • 复权:akshare 的 stock_us_hist 接口支持 adjust 参数,可设为 'qfq'(前复权)或 'hfq'(后复权)。默认不包含复权,需要显式指定。

常见问题与清洗要点

① 中文列名
直接使用中文列名在代码中不优雅,且容易出错。必须映射为英文标准字段。

② 分页限制
部分接口(如获取美股列表)默认每页只能返回 200 条,需要循环分页才能获取完整数据。对于历史 K 线,一般不存在分页问题。

③ 复权支持
akshare 的美股接口支持复权参数,但不同底层源(东方财富、新浪)的返回格式可能略有差异,建议使用时查阅最新文档。

清洗代码示例(akshare → 标准模型)

import akshare as ak
import pandas as pd

def clean_akshare(symbol):
    # 示例:获取美股日线,symbol 如 'AAPL',使用前复权
    raw = ak.stock_us_hist(symbol=symbol, period='daily', adjust='qfq')
    # 中文列名映射
    raw = raw.rename(columns={
        '日期': 'date',
        '开盘': 'open',
        '收盘': 'close',
        '最高': 'high',
        '最低': 'low',
        '成交量': 'volume'
    })
    # 时间戳转换:日期字符串 -> UTC 毫秒(假设数据为美东时间收盘)
    raw['timestamp'] = pd.to_datetime(raw['date']).dt.tz_localize('US/Eastern').dt.tz_convert('UTC').astype('int64') // 10**6
    raw['symbol'] = symbol
    return raw[['symbol', 'timestamp', 'open', 'high', 'low', 'close', 'volume']]

注意:akshare 的美股数据准确性依赖底层源(东方财富),建议与 yfinance 交叉验证。


2.3 Polygon.io

原始数据结构

Polygon.io 是一个专业的金融数据 API 服务商,提供 REST 和 WebSocket 接口。其聚合(Aggregates)端点返回的 JSON 格式如下:

{
  "ticker": "AAPL",
  "results": [
    {"t": 1711910400000, "o": 175.00, "h": 176.50, "l": 174.80, "c": 176.20, "v": 12345678}
  ]
}
  • 字段t(时间戳,毫秒)、ohlcv(成交量)。
  • 时间戳:Unix 毫秒,UTC。
  • 成交量:单位是“股”。
  • 复权:可通过参数 adjusted=true/false 控制是否返回复权数据。

常见问题与清洗要点

① 分钟数据“缺失”
很多用户发现 Polygon 的 1 分钟聚合数据不连续,比如 10:01 有数据,10:02 没有,10:03 又有。实际上,这是因为在 10:02 这一分钟内没有任何交易发生(Volume=0),Polygon 不会生成该分钟的聚合记录。这不是错误,是设计如此。清洗时需要用前向填充(forward-fill)补全缺失的分钟。

② 复权计算潜在误差
有部分用户在 Reddit 等社区反馈,在极端拆股场景下(如 1:10 反向拆股),Polygon 的 adjusted=true 返回的价格可能存在计算误差。建议在生产环境中使用 adjusted=false 获取原始价格,并结合独立的拆股数据库自行计算复权,或通过交叉验证确认数据准确性。

③ 实时数据乱序
WebSocket 推送的实时 tick 可能因网络延迟而乱序到达。不能依赖本地接收时间排序,必须使用消息内的 sip_timestamp(交易所撮合时间)进行排序。

清洗代码示例(Polygon REST → 标准模型)

import requests
import pandas as pd

def clean_polygon(ticker, api_key):
    url = f"https://api.polygon.io/v2/aggs/ticker/{ticker}/range/1/day/2025-01-01/2025-04-01?adjusted=false&apiKey={api_key}"
    resp = requests.get(url).json()
    records = []
    for item in resp['results']:
        records.append({
            'symbol': ticker,
            'timestamp': item['t'],  # 已经是毫秒
            'open': item['o'],
            'high': item['h'],
            'low': item['l'],
            'close': item['c'],
            'volume': item['v']
        })
    df = pd.DataFrame(records)
    return df

前向填充缺失分钟(针对分钟数据):

# 假设 df 是分钟数据,时间戳列 'timestamp'(毫秒)
df.set_index('timestamp', inplace=True)
full_index = pd.date_range(start=df.index.min(), end=df.index.max(), freq='1min')
df = df.reindex(full_index).fillna(method='ffill')
df['volume'] = df['volume'].fillna(0)

三、时间对齐的进阶处理

对于日线数据,对齐到交易日历即可;对于分钟数据,需要注意交易所的休市时间段(如午休、提前收盘)。可以使用 pandas_market_calendars 获取交易所的 schedule,其中包含 market_openmarket_close 时间,据此过滤非交易时段的数据。

import pandas_market_calendars as mcal

nyse = mcal.get_calendar('NYSE')
schedule = nyse.schedule(start_date='2025-01-01', end_date='2025-12-31')
# 生成标准的交易日历(美东时间日期)
trading_days = nyse.valid_days(schedule)

# 将日期转为 UTC 毫秒时间戳(当日 00:00:00 UTC)
trading_timestamps = [int(pd.Timestamp(d).tz_localize('US/Eastern').tz_convert('UTC').timestamp() * 1000) for d in trading_days]

# 将数据 reindex 到交易日历
df = df.set_index('timestamp').reindex(trading_timestamps).fillna(method='ffill')

💡 架构师笔记:WebSocket 实时流的时间戳对齐

在构建实时数据管道时,最容易被忽视的是 WebSocket 流的数据对齐。不同数据源的时间戳含义不同:

  • Polygon.io:使用 sip_timestamp(交易所撮合时间,纳秒精度)
  • 某些免费源:使用本地接收时间(受网络延迟影响)

切记:必须按照交易所时间戳排序,而不是本地接收时间。否则在网络抖动时,数据会乱序到达,导致 K 线计算错误。


四、统一清洗框架:适配器模式 + 交易日历 + 健壮错误处理

4.1 适配器模式(Adapter Pattern)

为了优雅地支持多个数据源,建议采用适配器模式:定义一个统一的数据获取接口,每个数据源实现自己的适配器,内部完成字段映射、时间转换、复权处理。

from abc import ABC, abstractmethod
import pandas as pd

class DataSourceAdapter(ABC):
    @abstractmethod
    def get_ohlcv(self, symbol, start, end, interval='1d') -> pd.DataFrame:
        pass

class YFinanceAdapter(DataSourceAdapter):
    def get_ohlcv(self, symbol, start, end, interval='1d'):
        # 调用 yfinance,清洗后返回标准 DataFrame
        pass

class AkshareAdapter(DataSourceAdapter):
    def get_ohlcv(self, symbol, start, end, interval='1d'):
        # 调用 akshare,清洗后返回标准 DataFrame
        pass

class PolygonAdapter(DataSourceAdapter):
    def get_ohlcv(self, symbol, start, end, interval='1d'):
        # 调用 Polygon API,清洗后返回标准 DataFrame
        pass

这样,上层业务代码只需调用统一的 get_ohlcv,切换数据源时只需更换适配器,无需修改任何清洗逻辑。

4.2 健壮的错误处理与重试机制

在实际生产环境中,网络超时、API 限流、空数据等情况非常常见。下面是一个带重试和限流处理的请求示例(使用 tenacity 库):

import time
import requests
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def fetch_with_retry(url, headers=None, timeout=10):
    resp = requests.get(url, headers=headers, timeout=timeout)
    if resp.status_code == 429:
        retry_after = int(resp.headers.get('Retry-After', 5))
        time.sleep(retry_after)
        raise Exception("Rate limited")
    resp.raise_for_status()
    return resp.json()

五、不想自己清洗?试试内置标准化的数据源

数据清洗虽然必要,但非常繁琐。如果你希望开箱即用,可以考虑像 TickDB 这类内置了标准化的数据源。TickDB 的特点包括:

  • 统一接口:REST 和 WebSocket 返回相同的数据结构,字段名、单位、时间戳格式完全统一。
  • 自动复权:K 线接口默认返回前复权数据,无需手动计算。
  • 多市场覆盖:同时支持美股、港股、A股、加密货币,且 ticker 命名规范(如 AAPL.US700.HK000001.SZ)。
  • AI 友好:内置 Skill 能力,可无缝集成到 AI 应用中。
  • 生产级 WebSocket:如果接入 TickDB 的 WebSocket 实时流(wss://api.tickdb.ai/v1/realtime),记得在客户端实现每秒发送 {"cmd": "ping"} 的心跳保活,这是保障高可用的基本操作。

对于生产级系统,使用 TickDB 可以节省 90% 的清洗代码,让你专注于策略本身。当然,如果你喜欢自己掌控细节,ClawHub 上也有不少开源清洗工具可以参考。


六、总结

本文以 yfinance、akshare、Polygon.io 三个主流美股数据源为例,详细讲解了:

  • 各数据源的原始数据结构与常见“坑”
  • 字段映射、时间对齐(含交易日历)、复权处理、缺失值填充的具体代码
  • 如何用适配器模式构建统一清洗框架
  • 健壮的错误处理与重试机制
  • 美股数据源的安全性优势

无论你选择哪个数据源,一套健壮的清洗管道都是量化系统的地基。希望这篇文章能帮你少走弯路,把时间花在策略上,而不是数据格式上。

本文纯技术分享,提到的数据源均为公开服务,不构成任何投资建议。市场有风险,投资需谨慎。