第三章:数据获取与清洗——用yfinance搞定纳斯达克成分股历史数据

做量化投资,数据就是你的弹药。没有干净的数据,再牛的策略也是白搭。这一章,咱们就聊聊怎么用Python把纳斯达克成分股的历史数据拉下来,再把它收拾得服服帖帖。

我个人习惯用yfinance这个库,它免费、好用,而且数据质量还不错。你想想看,要是每次回测都要手动去雅虎财经下载CSV,那效率得多低?

3.1 环境准备与yfinance安装

先把工具准备好。打开你的终端,敲一行命令:

pip install yfinance pandas numpy

嗯,这里要注意。如果你用的是Anaconda环境,记得先激活你的虚拟环境再装。我在项目中遇到过有人直接在base环境里装了一堆包,结果版本冲突,折腾了半天。

3.2 获取纳斯达克成分股列表

纳斯达克成分股不是一成不变的。每年都有公司进进出出。我建议你从纳斯达克官网或者维基百科拉一份最新的列表。

这里我写了一个小函数,直接从维基百科抓取:

import pandas as pd
import requests
from bs4 import BeautifulSoup

def get_nasdaq_tickers():
    url = "https://en.wikipedia.org/wiki/Nasdaq-100"
    response = requests.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')
    table = soup.find('table', {'id': 'constituents'})
    df = pd.read_html(str(table))[0]
    tickers = df['Ticker'].tolist()
    return tickers

tickers = get_nasdaq_tickers()
print(f"共获取到 {len(tickers)} 只成分股")
注意:维基百科的表格结构偶尔会变。我曾经遇到过爬下来的数据全是NaN,后来发现是表格的id改了。建议你每次跑之前先看一眼网页结构。

3.3 使用yfinance批量下载历史数据

有了股票代码列表,接下来就是下载数据了。yfinance的用法很简单:

import yfinance as yf

# 下载苹果公司近5年的日线数据
aapl = yf.download('AAPL', start='2019-01-01', end='2024-01-01')
print(aapl.head())

但如果你要下载100只股票,一个个来就太慢了。我一般用循环加延时,避免被服务器封IP:

import time

data = {}
for ticker in tickers[:10]:  # 先试10只
    try:
        df = yf.download(ticker, start='2019-01-01', end='2024-01-01')
        data[ticker] = df
        time.sleep(0.5)  # 每只股票间隔0.5秒
        print(f"{ticker} 下载完成")
    except Exception as e:
        print(f"{ticker} 下载失败: {e}")
小技巧:yfinance默认返回的数据包含Open、High、Low、Close、Volume、Adj Close六列。其中Adj Close是复权收盘价,做回测时一定要用这个,否则分红送股会扭曲你的收益率计算。

3.4 处理缺失值——别让NaN毁了你的策略

数据下载下来,第一件事就是检查缺失值。为什么会有缺失?可能是停牌、可能是数据源没更新、也可能是节假日。

我习惯先看看每只股票的缺失情况:

def check_missing(data_dict):
    missing_report = {}
    for ticker, df in data_dict.items():
        missing_count = df['Adj Close'].isna().sum()
        if missing_count > 0:
            missing_report[ticker] = missing_count
    return missing_report

missing = check_missing(data)
print(f"有缺失值的股票数量: {len(missing)}")

处理缺失值,我一般分三步走:

  1. 前向填充——用上一个交易日的数据填充。适合停牌的情况。
  2. 线性插值——如果缺失天数不多,用前后值线性插值。
  3. 直接删除——如果某只股票缺失太多(比如超过10%),我建议直接扔掉。

代码实现:

def clean_data(df, max_missing_ratio=0.1):
    # 先检查缺失比例
    missing_ratio = df['Adj Close'].isna().mean()
    if missing_ratio > max_missing_ratio:
        return None  # 缺失太多,直接丢弃
    
    # 前向填充
    df = df.ffill()
    # 如果还有缺失,线性插值
    df = df.interpolate(method='linear')
    # 如果开头还有缺失,用第一个有效值填充
    df = df.bfill().ffill()
    
    return df

# 批量清洗
cleaned_data = {}
for ticker, df in data.items():
    cleaned = clean_data(df)
    if cleaned is not None:
        cleaned_data[ticker] = cleaned
    else:
        print(f"{ticker} 因缺失过多被剔除")
核心要点:清洗数据时,一定要记录你删除了哪些股票、填充了多少缺失值。这些信息在回测报告中要体现出来,否则别人会质疑你的数据可靠性。

3.5 复权价格处理——别被分红蒙蔽了双眼

复权价格,说白了就是把分红、送股这些事件考虑进去,让价格序列连续可比。yfinance返回的Adj Close已经帮你做了后复权。

但这里有个坑:Adj Close只调整了收盘价,Open、High、Low并没有调整。如果你要用这些价格做技术指标,需要自己手动调整。

我写了一个函数,把整个OHLC都做复权处理:

def adjust_ohlc(df):
    # 计算复权因子
    adj_factor = df['Adj Close'] / df['Close']
    
    # 调整所有价格列
    df['Open'] = df['Open'] * adj_factor
    df['High'] = df['High'] * adj_factor
    df['Low'] = df['Low'] * adj_factor
    df['Close'] = df['Adj Close']  # 直接用复权收盘价
    
    return df

# 应用复权
for ticker in cleaned_data:
    cleaned_data[ticker] = adjust_ohlc(cleaned_data[ticker])
避坑指南:我曾经在回测中直接用原始收盘价,结果某只股票因为大比例分红,价格从100块跌到30块,策略信号全乱了。后来才发现是没做复权处理。嗯,从那以后我再也不敢跳过这一步了。

3.6 数据质量检查与可视化

数据清洗完了,还得看看效果。我一般会画个图,直观对比清洗前后的差异:

import matplotlib.pyplot as plt

# 以苹果为例
aapl_cleaned = cleaned_data['AAPL']
plt.figure(figsize=(12, 6))
plt.plot(aapl_cleaned.index, aapl_cleaned['Close'], label='复权后收盘价')
plt.title('AAPL 复权收盘价走势')
plt.xlabel('日期')
plt.ylabel('价格 (USD)')
plt.legend()
plt.grid(True)
plt.show()

除了看图,我还会做几个统计检查:

  • 日收益率分布——看看有没有异常值(比如单日涨跌超过20%)
  • 成交量检查——如果某天成交量为0,大概率是数据问题
  • 日期连续性——确保没有跳过的交易日

代码示例:

def quality_check(df, ticker):
    # 计算日收益率
    returns = df['Close'].pct_change().dropna()
    
    # 检查异常收益率
    outliers = returns[(returns > 0.2) | (returns < -0.2)]
    if len(outliers) > 0:
        print(f"{ticker} 发现 {len(outliers)} 个异常收益率日期")
        print(outliers)
    
    # 检查成交量
    zero_volume = df[df['Volume'] == 0]
    if len(zero_volume) > 0:
        print(f"{ticker} 有 {len(zero_volume)} 天成交量为0")
    
    # 检查日期连续性
    date_diff = df.index.to_series().diff().dropna()
    missing_dates = date_diff[date_diff > pd.Timedelta(days=3)]
    if len(missing_dates) > 0:
        print(f"{ticker} 存在日期跳跃,共 {len(missing_dates)} 次")

# 对所有股票做质量检查
for ticker in cleaned_data:
    quality_check(cleaned_data[ticker], ticker)

3.7 数据持久化——把清洗好的数据存起来

每次跑策略都重新下载数据?太傻了。我习惯把清洗好的数据存成Parquet格式,又快又省空间:

# 保存为Parquet文件
for ticker, df in cleaned_data.items():
    df.to_parquet(f'data/{ticker}.parquet')

# 或者把所有数据合并成一个DataFrame
combined = pd.concat(cleaned_data, axis=1)
combined.to_parquet('data/nasdaq_cleaned.parquet')
推荐:Parquet格式比CSV快10倍以上,而且自动压缩。如果你团队里有人还在用CSV存金融数据,建议你拉他入伙用Parquet。

3.8 本章知识体系总览

下面这张图,把数据获取与清洗的整个流程串起来了。你可以把它当作操作手册,每次做数据准备时对照着来:

数据获取与清洗流程 步骤1:获取成分股列表 维基百科 / 纳斯达克官网 步骤2:下载历史数据 yfinance 批量下载 步骤3:处理缺失值 前向填充 / 插值 / 删除 步骤4:复权价格处理 调整OHLC / 使用Adj Close 步骤5:数据质量检查 异常值 / 成交量 / 日期连续性 步骤6:数据持久化 保存为Parquet / CSV 关键注意事项 • 下载时加延时,避免被封IP • 缺失超过10%的股票直接剔除 • 复权处理是回测准确性的前提 • 记录每一步的数据处理日志 • 用Parquet格式替代CSV存储 • 定期更新成分股列表(每季度) • 检查日期连续性,避免数据断层 • 异常收益率要人工复核

数据准备这一步,看似枯燥,但却是整个量化投资流程的基石。我见过太多人花几个月写策略,结果因为数据没处理好,回测结果全是假的。嗯,希望你能重视起来。

下一章,咱们会基于这些干净的数据,开始构建投资组合。到时候你就知道,今天花时间把数据收拾干净,绝对值。


公众号:蓝海数据掘金营,微信deep3321