4、数据获取模块:获取主力合约列表、获取历史日线数据、处理复权数据、数据缓存与增量更新

做期货价差分析,第一步就是搞数据。这事儿听起来简单,但坑特别多。我刚开始做的时候,以为从交易所拉个K线就完事了,结果发现数据对不上、复权搞错、每次跑脚本都要重新下载……嗯,今天咱们就把这块彻底讲透。

4.1 获取主力合约列表

期货和股票不一样,每个品种有多个合约同时在交易。比如螺纹钢,有RB2401、RB2405、RB2410……你总不能全拿来做价差分析吧?所以我们要找「主力合约」——也就是成交量最大、持仓量最大的那个。

我个人习惯用「持仓量」来判断主力合约。为什么?因为成交量可能被刷单干扰,但持仓量是实打实的资金沉淀。

核心逻辑: 每天扫描所有同品种合约,选出持仓量最大的那个作为主力合约。

代码实现其实不复杂,我贴一段我常用的逻辑:

def get_main_contracts(exchange='SHFE'):
    """
    获取指定交易所所有品种的主力合约
    返回: {'RB': 'RB2410', 'HC': 'HC2401', ...}
    """
    # 1. 获取所有合约列表
    all_contracts = get_all_contracts(exchange)
    
    # 2. 按品种分组
    from collections import defaultdict
    grouped = defaultdict(list)
    for contract in all_contracts:
        symbol = extract_symbol(contract)  # 提取品种代码
        grouped[symbol].append(contract)
    
    # 3. 每个品种选持仓量最大的
    main_contracts = {}
    for symbol, contracts in grouped.items():
        max_oi = 0
        main = None
        for c in contracts:
            oi = get_open_interest(c)  # 获取持仓量
            if oi > max_oi:
                max_oi = oi
                main = c
        main_contracts[symbol] = main
    
    return main_contracts
小技巧: 我建议每天收盘后更新一次主力合约列表。因为主力合约切换通常发生在交割月前1-2个月,没必要实时刷新。

4.2 获取历史日线数据

拿到主力合约列表后,下一步就是拉历史K线。这里有个问题:主力合约是会变的。比如1月份主力是RB2405,到了5月份就变成RB2410了。如果你只拉一个合约的连续数据,那中间会有跳空。

所以,正确的做法是——拼接。把不同时间段的主力合约K线拼成一条连续的曲线。我管它叫「主力连续合约」。

举个例子:

时间段 主力合约
2024-01 ~ 2024-04 RB2405
2024-05 ~ 2024-08 RB2410
2024-09 ~ 2024-12 RB2501

拼接的时候要注意:切换日那天,前后两个合约的价格可能差很多。这时候就需要复权处理了。

4.3 处理复权数据

复权,说白了就是消除合约切换带来的价格跳空。我见过有人直接拿原始数据做分析,结果价差图上一堆莫名其妙的尖峰——其实就是合约切换造成的。

常用的复权方法有两种:

  • 前复权: 调整历史价格,让切换日前后价格连续。适合看历史走势。
  • 后复权: 调整当前价格,保持历史数据不变。适合做回测。

我个人做价差分析时,更倾向于用后复权。为什么?因为价差分析关注的是「相对关系」,后复权不会改变历史价差的形态。

注意: 复权不是万能的。如果两个合约的基本面发生了根本变化(比如交割规则改了),复权也救不了。我曾经在螺纹钢上吃过这个亏,后来加了「合约切换检测」才解决。

复权计算的代码示例:

def adjust_price(df, method='backward'):
    """
    复权处理
    df: 包含主力合约切换标记的DataFrame
    method: 'forward' 前复权, 'backward' 后复权
    """
    df = df.copy()
    
    # 找到所有切换点
    switch_points = df[df['is_switch'] == True].index
    
    if method == 'backward':
        # 后复权:从后往前调整
        for i in range(len(switch_points)-1, -1, -1):
            idx = switch_points[i]
            ratio = df.loc[idx, 'close_old'] / df.loc[idx, 'close_new']
            df.loc[:idx, 'close'] *= ratio
            df.loc[:idx, 'open'] *= ratio
            df.loc[:idx, 'high'] *= ratio
            df.loc[:idx, 'low'] *= ratio
    
    return df

4.4 数据缓存与增量更新

每次跑脚本都重新下载全部数据?太慢了。尤其当你分析几十个品种、几年数据的时候,下载一次可能要十几分钟。

我的做法是:本地存一份,每天只更新最新的数据。这就是「增量更新」。

数据缓存的结构我一般这样设计:

  • 合约列表缓存: 存成JSON文件,每天更新一次
  • 日线数据缓存: 按品种+合约存成CSV或Parquet,每天追加最新K线
  • 复权数据缓存: 存成HDF5或Feather,每周重新计算一次
推荐格式: 日线数据用Parquet,压缩率高、读写快。我试过CSV存3年数据要200MB,Parquet只要40MB。

增量更新的核心逻辑:

def update_daily_data(symbol, start_date=None):
    """
    增量更新日线数据
    """
    cache_file = f'data/{symbol}.parquet'
    
    # 1. 检查本地缓存
    if os.path.exists(cache_file):
        local_data = pd.read_parquet(cache_file)
        last_date = local_data.index[-1]
        print(f'本地已有数据,最后日期:{last_date}')
    else:
        last_date = None
        local_data = pd.DataFrame()
    
    # 2. 只下载缺失的数据
    if last_date:
        new_data = download_kline(symbol, start=last_date + timedelta(days=1))
    else:
        new_data = download_kline(symbol, start=start_date or '2020-01-01')
    
    # 3. 合并并去重
    if not new_data.empty:
        combined = pd.concat([local_data, new_data])
        combined = combined[~combined.index.duplicated(keep='last')]
        combined.sort_index(inplace=True)
        combined.to_parquet(cache_file)
        print(f'更新完成,共 {len(combined)} 条数据')
    else:
        print('数据已是最新,无需更新')

4.5 本章知识体系

下面这张图,把数据获取模块的整体流程串起来了。我建议你保存下来,写代码的时候对照着看:

数据获取模块流程图 获取主力合约列表 按品种分组,选持仓量最大 获取历史日线数据 拼接主力连续合约 处理复权数据 前复权 / 后复权 数据缓存与增量更新 Parquet缓存 · 每日增量更新 · 去重合并 每天收盘后执行 自动化脚本 注意合约切换 导致的跳空 价差分析推荐 后复权

整个流程走下来,你会发现数据获取其实是个「体力活」——逻辑不复杂,但细节特别多。我个人建议你先把主力合约列表和日线数据跑通,再慢慢加上复权和缓存。一步到位容易出bug,我当年就是这么过来的。

本章核心要点:
  • 主力合约按持仓量选,不是按成交量
  • 历史数据要拼接成连续合约,不能只拉一个合约
  • 复权用后复权,适合价差分析
  • 数据缓存用Parquet,增量更新每天跑一次

专注资料整理