第三章 数据获取与清洗:期权链数据的预处理艺术

说实话,做量化交易这么多年,我最大的体会就是:数据质量决定了策略的生死。你策略再牛,模型再炫,喂进去一堆脏数据,结果就是垃圾进垃圾出。今天咱们就来聊聊期权链数据的获取与清洗,这是波动率套利的第一步,也是最容易被忽视的一步。

核心要点:期权数据不同于股票数据,它有到期日、行权价、看涨看跌等多个维度。获取后必须经过严格的清洗,否则后续计算隐含波动率时会出现各种诡异错误。

3.1 使用yfinance获取期权链数据

yfinance这个库,我用得最多。它免费、简单,而且数据质量还不错。不过要注意,它获取的是美式期权数据,对于欧式期权需要额外处理。

先看个基础示例:

import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# 获取标的资产
ticker = yf.Ticker("SPY")

# 获取所有到期日
expirations = ticker.options
print(f"可用到期日: {expirations[:5]}")  # 只看前5个

嗯,这里有个坑。yfinance返回的到期日列表是按字符串排序的,不是按日期排序。我刚开始做的时候没注意,结果数据顺序全乱了。建议你拿到后先转成datetime再排序:

# 排序到期日
exp_dates = sorted([datetime.strptime(d, "%Y-%m-%d") for d in expirations])
exp_dates_str = [d.strftime("%Y-%m-%d") for d in exp_dates]

个人经验:我一般只取未来30-90天的期权数据。太近的期权时间价值衰减太快,太远的流动性又差。这个区间做波动率套利最舒服。

获取单个到期日的期权链:

# 获取最近到期日的期权链
target_date = exp_dates_str[0]
opt_chain = ticker.option_chain(target_date)

# 分离看涨和看跌
calls = opt_chain.calls
puts = opt_chain.puts

print(f"看涨期权数量: {len(calls)}")
print(f"看跌期权数量: {len(puts)}")

你想想看,一个到期日就有几百个期权合约。如果我们要做全期限分析,数据量会非常大。所以我建议你一次性获取多个到期日的数据:

def get_option_chain(ticker_symbol, exp_dates):
    """批量获取期权链数据"""
    ticker = yf.Ticker(ticker_symbol)
    all_data = []
    
    for exp_date in exp_dates:
        try:
            chain = ticker.option_chain(exp_date)
            calls = chain.calls
            calls['option_type'] = 'call'
            calls['expiration'] = exp_date
            
            puts = chain.puts
            puts['option_type'] = 'put'
            puts['expiration'] = exp_date
            
            all_data.append(calls)
            all_data.append(puts)
        except Exception as e:
            print(f"获取 {exp_date} 数据失败: {e}")
            continue
    
    return pd.concat(all_data, ignore_index=True)

# 获取5个到期日的数据
target_dates = exp_dates_str[:5]
option_data = get_option_chain("SPY", target_dates)
print(f"总数据量: {len(option_data)}")

注意:yfinance有请求频率限制。我试过一次性请求太多,结果被限制了半小时。建议每次请求间隔0.5秒,或者用time.sleep()控制节奏。

3.2 数据清洗与预处理

数据拿到手了,但别急着用。yfinance返回的数据里有很多字段,我们真正需要的其实就几个:行权价、最后交易价、隐含波动率、希腊值等。先看看原始数据长什么样:

# 查看数据列
print(option_data.columns.tolist())
# ['contractSymbol', 'strike', 'currency', 'lastPrice', 'change', 
#  'percentChange', 'volume', 'openInterest', 'bid', 'ask', 
#  'expiration', 'option_type', ...]

我个人习惯只保留核心字段,其他的删掉。为什么?因为数据越干净,后续处理越不容易出错。我曾经因为保留了多余的列,导致merge时出现重复数据,排查了半天。

def clean_option_data(df):
    """清洗期权数据"""
    # 选择核心字段
    core_cols = ['strike', 'lastPrice', 'bid', 'ask', 'volume', 
                 'openInterest', 'impliedVolatility', 'expiration', 
                 'option_type']
    
    df_clean = df[core_cols].copy()
    
    # 重命名列
    df_clean.columns = ['strike', 'last_price', 'bid', 'ask', 
                        'volume', 'open_interest', 'iv', 
                        'expiration', 'option_type']
    
    # 删除缺失值
    df_clean = df_clean.dropna(subset=['strike', 'last_price', 'iv'])
    
    # 过滤异常数据
    df_clean = df_clean[df_clean['strike'] > 0]
    df_clean = df_clean[df_clean['last_price'] > 0]
    df_clean = df_clean[df_clean['iv'] > 0]
    
    # 添加时间戳
    df_clean['fetch_time'] = datetime.now()
    
    return df_clean

cleaned_data = clean_option_data(option_data)
print(f"清洗后数据量: {len(cleaned_data)}")

这里有个关键点:隐含波动率(iv)。yfinance返回的iv是年化值,但有时候会出现异常值。我遇到过iv大于10的情况,这明显不合理。所以我会加一个过滤:

# 过滤异常IV值
df_clean = df_clean[(df_clean['iv'] > 0.05) & (df_clean['iv'] < 2.0)]
# 保留IV在5%到200%之间的数据

避坑指南:我曾经遇到过某个到期日的所有期权iv都是0的情况。后来发现是yfinance的数据缺失。这种情况直接跳过整个到期日,不要强行填充。

3.3 存储到本地数据库

数据清洗完了,接下来就是存储。我个人推荐用SQLite,轻量、无需安装、适合个人研究。当然,如果你做生产环境,可以考虑PostgreSQL。

先创建数据库和表结构:

import sqlite3
from sqlite3 import Error

def create_database(db_path="options_data.db"):
    """创建期权数据库"""
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    # 创建期权数据表
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS option_chain (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            ticker TEXT NOT NULL,
            strike REAL NOT NULL,
            last_price REAL,
            bid REAL,
            ask REAL,
            volume INTEGER,
            open_interest INTEGER,
            iv REAL,
            expiration TEXT NOT NULL,
            option_type TEXT NOT NULL,
            fetch_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    
    # 创建索引,加速查询
    cursor.execute('''
        CREATE INDEX IF NOT EXISTS idx_ticker_exp 
        ON option_chain(ticker, expiration)
    ''')
    
    conn.commit()
    conn.close()
    print("数据库创建成功")

create_database()

然后写个批量插入函数:

def save_to_database(df, ticker, db_path="options_data.db"):
    """保存数据到数据库"""
    conn = sqlite3.connect(db_path)
    
    # 添加ticker列
    df['ticker'] = ticker
    
    # 选择要保存的列
    save_cols = ['ticker', 'strike', 'last_price', 'bid', 'ask', 
                 'volume', 'open_interest', 'iv', 'expiration', 
                 'option_type']
    
    df_to_save = df[save_cols].copy()
    
    # 批量插入
    df_to_save.to_sql('option_chain', conn, if_exists='append', index=False)
    
    conn.close()
    print(f"成功保存 {len(df_to_save)} 条数据")

# 执行保存
save_to_database(cleaned_data, "SPY")

小技巧:每次获取数据时,建议先检查数据库里是否已有相同日期的数据。避免重复存储。我一般用expiration + ticker + option_type作为唯一键来判断。

最后,写个查询函数方便后续使用:

def query_option_data(ticker, expiration=None, option_type=None, db_path="options_data.db"):
    """查询期权数据"""
    conn = sqlite3.connect(db_path)
    
    query = "SELECT * FROM option_chain WHERE ticker = ?"
    params = [ticker]
    
    if expiration:
        query += " AND expiration = ?"
        params.append(expiration)
    
    if option_type:
        query += " AND option_type = ?"
        params.append(option_type)
    
    df = pd.read_sql_query(query, conn, params=params)
    conn.close()
    
    return df

# 示例查询
spy_calls = query_option_data("SPY", option_type="call")
print(f"SPY看涨期权数据: {len(spy_calls)} 条")

3.4 完整流程与SVG框架图

好了,整个流程串起来就是这样。我画了个流程图,帮你理清思路:

期权数据获取与清洗流程 1. 获取期权链数据 2. 数据清洗与过滤 3. 存储到SQLite数据库 4. 查询与使用 关键注意事项 • 控制请求频率,避免被封 • 过滤异常IV值(0.05-2.0) • 删除缺失值和零价格 • 建立索引加速查询 • 避免重复存储数据 • 只保留核心字段 • 排序到期日按时间顺序 • 跳过数据缺失的到期日 • 添加时间戳便于追溯 • 定期清理过期数据 数据质量 = 策略成功的基石。花80%时间在数据上,剩下20%做策略,这是值得的。

这个流程看起来简单,但每一步都有细节。我刚开始做的时候,光数据清洗就折腾了两周。后来总结出一套标准流程,现在每次获取数据都按这个来,基本没出过问题。

总结一下:

  • 用yfinance获取期权链数据,注意控制频率
  • 清洗时过滤异常值、缺失值,只保留核心字段
  • 存储到SQLite,建立索引,避免重复
  • 写查询函数方便后续调用

数据准备好了,下一步就是计算隐含波动率。不过那是下一章的事。今天先把数据基础打牢,后面才能走得稳。

专注资料整理