第三章:数据获取与清洗——用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)} 只成分股")
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}")
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)}")
处理缺失值,我一般分三步走:
- 前向填充——用上一个交易日的数据填充。适合停牌的情况。
- 线性插值——如果缺失天数不多,用前后值线性插值。
- 直接删除——如果某只股票缺失太多(比如超过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])
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')
3.8 本章知识体系总览
下面这张图,把数据获取与清洗的整个流程串起来了。你可以把它当作操作手册,每次做数据准备时对照着来:
数据准备这一步,看似枯燥,但却是整个量化投资流程的基石。我见过太多人花几个月写策略,结果因为数据没处理好,回测结果全是假的。嗯,希望你能重视起来。
下一章,咱们会基于这些干净的数据,开始构建投资组合。到时候你就知道,今天花时间把数据收拾干净,绝对值。
公众号:蓝海数据掘金营,微信deep3321