Skip to content

3.4 金融数据特色处理

概念详解

为什么金融数据需要特殊处理

金融数据与其他领域的数据有本质区别,主要体现在两个方面:时间结构的复杂性市场制度带来的特殊性。一个简单的pct_change()远远不够。

具体而言,金融数据处理需要处理的独特问题包括:

  • 期货合约的有限生命周期(主力合约切换)
  • 公司事件(分红、送股、拆股并股)的影响
  • 节假日与交易日历的差异
  • 高频数据的异步性与不规则采样
  • 衍生品的希腊字母与风险度量
名词解释:主力合约

在期货市场中,成交量和持仓量最大的合约月份,通常将其作为连续交易的代表。

名词解释:展期 (Roll)

在期货合约到期前,将仓位从近月合约转移到远月合约的过程。展期日期的选择决定了连续价格序列中的"跳跃"。

名词解释:Tick数据

逐笔成交数据,记录每一笔交易的价格、成交量、时间和买卖方向。是最高精度的市场数据,但数据量大、噪声多、需要特殊处理。

期货主力合约拼接:看似简单实则复杂

期货合约有固定的到期日,到期后该合约不再交易。要构建用于回测的连续价格序列,必须将不同合约"拼接"起来。拼接方式的选择会显著影响回测结果:

方式1:简单价差拼接(在切换日调整价差)

PYTHON1 行 · 47 B
📄此处有展示代码1 行 · 47 B展开 ▼
python
continuous[t] = price_new[t] - roll_adjustment
点击展开可浏览运行结果
📘 本段为公式演示片段(伪代码占位符,无完整上下文)

优点:保持展期前后的收益率连续性;缺点:回到历史某个时点的绝对价格没有经济意义

方式2:收益率拼接

PYTHON1 行 · 54 B
📄此处有展示代码1 行 · 54 B展开 ▼
python
continuous[t] = continuous[t-1] * (1 + return_new[t])
点击展开可浏览运行结果
📘 本段为公式演示片段(伪代码占位符,无完整上下文)

优点:不产生负价格;缺点:累积误差

方式3:比值拼接

PYTHON1 行 · 66 B
📄此处有展示代码1 行 · 66 B展开 ▼
python
continuous[t] = continuous[t-1] * (price_new[t] / price_new[t-1])
点击展开可浏览运行结果
📘 本段为公式演示片段(伪代码占位符,无完整上下文)

优点:保留完整的收益率结构;缺点:绝对价格逐渐漂移

股息与公司事件处理

A股公司的公司事件远不止分红:

  • 现金分红:除息日价格跳空,需前复权或后复权
  • 送股:股东获得额外股份,总市值不变,每股价格相应调整
  • 转增股本:资本公积金转增,效果类似送股
  • 配股:股东可按折扣价购买新股,影响比送股更复杂
  • 拆股/并股:改变每股面值,总股本相应调整

所有这些都是"复权"要处理的问题。一个正确的复权因子应为:

adj\_factor_t = \prod_{i=1}^{t} \left(1 + \frac{dividend_i}{P_{i, cum}} + bonus\_ratio_i + rights\_ratio_i \cdot discount_i\right)

Python实战

案例1:期货主力拼接

PYTHON1 行 · 117 B
📄此处有展示代码1 行 · 117 B展开 ▼
python
continuous = pd.concat([group.iloc[:,1:].sum().idxmax() for _, group in monthly_data.groupby(pd.Grouper(freq='M'))])
点击展开可浏览运行结果
📘 本段为公式演示片段(伪代码占位符,无完整上下文)

案例2:期货主力合约切换的完整实现

PYTHON127 行 · 5.2 KB
📄此处有展示代码127 行 · 5.2 KB展开 ▼
python
import pandas as pd
import numpy as np

def build_continuous_futures(contracts_data, roll_rule='volume', roll_days_before=5):
    """
    构建期货连续价格序列
    
    contracts_data: DataFrame, 包含字段:
        - date: 交易日
        - symbol: 合约代码 (如 'IF2401', 'IF2402')
        - close: 收盘价
        - volume: 成交量
        - open_interest: 持仓量
    roll_rule: 展期规则 'volume'(量最大)/'oi'(持仓最大)/'fixed'(到期前N日)
    roll_days_before: fixed规则下,到期前多少天切换
    """
    df = contracts_data.copy()
    df['date'] = pd.to_datetime(df['date'])
    
    # 提取品种和到期月份
    df['product'] = df['symbol'].str.extract(r'([A-Za-z]+)')
    df['month'] = df['symbol'].str.extract(r'(\d{4})$')
    df['year'] = df['month'].str[:2].astype(int) + 2000
    df['mon'] = df['month'].str[2:].astype(int)
    
    # 按日期排序
    df = df.sort_values(['product', 'date'])
    
    results = {}
    
    for product in df['product'].unique():
        prod_df = df[df['product'] == product].copy()
        
        # 找出每个交易日的主力合约
        if roll_rule == 'volume':
            prod_df['rank_vol'] = prod_df.groupby('date')['volume'].rank(ascending=False)
            dominant = prod_df[prod_df['rank_vol'] == 1].copy()
            next_dominant = prod_df[prod_df['rank_vol'] == 2].copy()
        elif roll_rule == 'oi':
            prod_df['rank_oi'] = prod_df.groupby('date')['open_interest'].rank(ascending=False)
            dominant = prod_df[prod_df['rank_oi'] == 1].copy()
        
        # 检测主力合约切换点
        dominant = dominant.set_index('date').sort_index()
        dominant['prev_symbol'] = dominant['symbol'].shift(1)
        dominant['roll_date'] = dominant['symbol'] != dominant['prev_symbol']
        
        # 展期日列表
        roll_dates = dominant.index[dominant['roll_date']].tolist()
        
        # 构建连续价格(价差拼接法)
        continuous = dominant[['close', 'volume', 'symbol']].copy()
        continuous['roll_adjustment'] = 0.0
        
        cumulative_adj = 0.0
        for roll_date in roll_dates[1:]:  # 跳过第一个(没有前值可比较)
            # 找到切换日前后的新旧主力合约价格
            mask_before = (continuous.index < roll_date) & (continuous.index >= roll_dates[roll_dates.index(roll_date)-1])
            mask_after = continuous.index >= roll_date
            
            if mask_before.any() and mask_after.any():
                prev_price = continuous.loc[mask_before, 'close'].iloc[-1]
                # 在roll_date那一天,新旧合约的价格差异
                if roll_date in prod_df['date'].values:
                    day_data = prod_df[prod_df['date'] == roll_date]
                    old_contract = continuous.loc[continuous.index < roll_date, 'symbol'].iloc[-1]
                    new_contract = continuous.loc[roll_date, 'symbol'] if roll_date in continuous.index else None
                    
                    if old_contract is not None:
                        old_price = day_data[day_data['symbol'] == old_contract]['close'].values
                        if len(old_price) > 0 and new_contract is not None:
                            new_price_day = day_data[day_data['symbol'] == new_contract]['close'].values
                            if len(new_price_day) > 0:
                                cumulative_adj += (new_price_day[0] - old_price[0])
                
                continuous.loc[mask_before, 'roll_adjustment'] = -cumulative_adj
        
        # 调整后的连续价格
        continuous['adjusted_close'] = continuous['close'] + continuous['roll_adjustment']
        
        results[product] = continuous
    
    return results


def futures_term_structure(contracts_data, target_date=None):
    """
    分析期货的期限结构(升水/贴水)
    
    期限结构是多空商品策略的关键alpha来源
    """
    df = contracts_data.copy()
    df['date'] = pd.to_datetime(df['date'])
    
    if target_date is None:
        target_date = df['date'].max()
    
    # 提取某一天所有合约的价格
    day_data = df[df['date'] == target_date].copy()
    day_data['month'] = day_data['symbol'].str.extract(r'(\d{4})$')
    day_data['mon_num'] = day_data['month'].str[2:].astype(int)
    
    # 按到期月排序
    day_data = day_data.sort_values('mon_num')
    
    # 计算展期收益率 (Roll Yield)
    # 展期收益 = (近月价格 - 远月价格) / 近月价格
    # 正 = 贴水 (Backwardation, 做多有利)
    # 负 = 升水 (Contango, 做多不利)
    
    if len(day_data) >= 2:
        front_price = day_data['close'].iloc[0]
        second_price = day_data['close'].iloc[1]
        roll_yield = (front_price - second_price) / front_price
        structure = 'Backwardation (贴水)' if roll_yield > 0 else 'Contango (升水)'
        
        return {
            'date': target_date,
            'front_month': day_data['symbol'].iloc[0],
            'front_price': front_price,
            'second_month': day_data['symbol'].iloc[1],
            'second_price': second_price,
            'roll_yield': roll_yield,
            'structure': structure
        }
    
    return None
点击展开可浏览运行结果
📘 本段代码定义了 2 个函数/类:函数 `build_continuous_futures`(构建期货连续价格序列)、函数 `futures_term_structure`(分析期货的期限结构(升水/贴水))。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

案例3:股票分红复权——从头实现

PYTHON95 行 · 3.3 KB
📄此处有展示代码95 行 · 3.3 KB展开 ▼
python
import pandas as pd
import numpy as np

def calculate_adjustment_factor(dividends, splits, bonuses, rights, prices):
    """
    手动计算复权因子序列
    
    dividends: 每股现金分红
    splits: 拆股比例(如1拆2,splits=2)
    bonuses: 送股比例(如10送3,bonuses=0.3)
    rights: 配股比例和配股价
    prices: 调整前的收盘价(不复权价格)
    """
    
    # 合并所有事件按照发生日期
    events = []
    
    for date, div in dividends.items():
        events.append(('dividend', date, div))
    
    for date, split in splits.items():
        events.append(('split', date, split))
    
    for date, bonus in bonuses.items():
        events.append(('bonus', date, bonus))
    
    events.sort(key=lambda x: x[1])  # 按日期排序
    
    # 从最早的事件到最新,累积计算复权因子
    adj_factor = 1.0
    
    # 前复权因子(调整历史价格)
    # adj_factor_product[t] = 从第t天到最新日之间的所有调整因子的乘积
    adj_factors = pd.Series(1.0, index=prices.index)
    
    for event_type, date, value in events:
        if event_type == 'dividend':
            # 分红调整:找到除息日前后的价格
            if date in prices.index:
                cum_price = prices.loc[date]  # 除息前的价格(含权)
                # 调整因子 = (除权前价格 - 每股分红) / 除权前价格
                # 即价格需要乘以这个因子
                factor = (cum_price - value) / cum_price
                adj_factor *= factor
        
        elif event_type == 'bonus':
            # 送股:总市值不变,股数变多,每股价格变低
            # 10送3 => 10股变13股,价格变 10/13
            factor = 1 / (1 + value)
            adj_factor *= factor
        
        elif event_type == 'split':
            # 拆股:1拆2 => 1股变2股,价格变1/2
            factor = 1 / value
            adj_factor *= factor
    
    # 将累积调整因子应用到历史价格上
    # 因为是前向累积(从前往后),我们需要反向传播
    
    return adj_factors


def detect_corporate_actions(price_df, threshold=0.15):
    """
    从价格序列中检测可能的公司事件(分红/拆股等导致的跳跃)
    
    用于验证复权数据是否完整
    """
    df = price_df.copy()
    df['return'] = df['close'].pct_change()
    
    # 检测价格跳跃
    # 在有涨跌停限制的A股市场,单日收益率超过一定范围通常意味着公司事件
    jumps = df[df['return'].abs() > threshold]
    
    events = []
    for date, row in jumps.iterrows():
        # 检查是否伴随成交量异常
        prev_date = df.index[df.index.get_loc(date) - 1]
        vol_ratio = row.get('volume', 1) / df.loc[prev_date, 'volume']
        
        events.append({
            'date': date,
            'return': row['return'],
            'volume_ratio': vol_ratio,
            'likely_action': 'dividend/bonus/split' if vol_ratio > 2 else 'possible data error'
        })
    
    if events:
        print(f"检测到 {len(events)} 个可能的事件日:")
        for e in events[:10]:
            print(f"  {e['date'].date()}: 收益={e['return']:.2%}, "
                  f"量比={e['volume_ratio']:.1f}, 可能={e['likely_action']}")
    
    return events
点击展开可浏览运行结果
📘 本段代码定义了 2 个函数/类:函数 `calculate_adjustment_factor`(手动计算复权因子序列)、函数 `detect_corporate_actions`(从价格序列中检测可能的公司事件(分红/拆股等导致的跳跃))。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

常见误区

误区一:永续合约价格可以直接用于回测

事实:直接使用连续的期货价格序列进行回测是危险的。在展期日,合约切换会产生"跳空",但这是人为的而非真实的损益。如果策略在展期日前一天做多、展期日继续持有,回测中会错误地记录这个跳空。正确的做法是使用展期调整后的价格序列或直接在展期日平仓。

误区二:所有的复权都可以互相转换

事实:前复权和后复权的数学关系是 Pforward×factor=Pbackward,但在计算过程中两者可能因为浮点精度和四舍五入而产生不一致。特别是经过多年、多次公司事件后,这种累积误差可能大到影响回测结果。最佳实践是直接使用数据源提供的复权数据,而非自行转换。

误区三:Tick数据处理和K线处理一样

事实:Tick数据涉及大量微观结构噪声,需要专门的清洗步骤:

  1. 交易时间外数据清除
  2. 异常价格过滤(远偏离买卖报价)
  3. 重复tick移除
  4. 买卖方向推断(Lee-Ready算法)
  5. 不规则时间戳的重采样

误区四:同花顺/东方财富的数据和Wind一样

事实:不同数据源的处理方式可能不同:

  • 复权基准日和方法的差异
  • 成交量的单位(手vs股)
  • 财务数据的调整方法
  • 指数成分股调整的时间点

在多数据源交叉验证中发现不一致,是日常量化工作的常态。

实战练习

  1. 期货连续合约构建:用AkShare获取螺纹钢(RB)过去三年所有合约的日线数据,分别用三种方法(简单价差拼接、收益率拼接、比值拼接)构建连续价格序列。比较三种方法下:(a) 累计收益率的差异,(b) 波动率的差异,(c) 最大回撤的差异。

  2. 分红复权验证:选取一只高分红的A股(如银行股或白酒股),比较其"不复权价格"、"前复权价格"和"后复权价格"。验证:(a) 前复权价格 × 复权因子 ≈ 不复权价格,(b) 分红日的"不复权收益率" + "股息率" ≈ "前复权收益率"。

  3. Tick数据降噪:如果你有tick级数据,尝试用中位数滤波或Kalman滤波对tick价格序列进行降噪,比较降噪前后的价格Autocorrelation结构变化,讨论降噪是否引入了新的偏差。

延伸阅读

  • 《Options, Futures, and Other Derivatives》—— John Hull,第2-3章期货定价与展期
  • 《Trading and Exchanges》—— Larry Harris,订单与交易机制细节
  • 《Python for Finance》—— Yves Hilpisch,第7-8章金融数据处理专题
  • 各交易所官方数据接口文档(上期所CTP、中金所、深交所/上交所MDDP)
  • Ederington, L. (1979): "The Hedging Performance of the New Futures Markets"

本章要点

  • 期货主力合约拼接是量化CTA的基础,展期方法的选择会显著影响回测结果
  • 复权是股票数据处理的基石,理解前复权/后复权的数学原理比盲信数据源更重要
  • 金融数据处理的三大难点:时间对齐、事件调整和制度特征
  • 不同数据源的结果天然不同,事前进行质量对比和交叉验证是专业习惯
  • Tick数据处理需要专门的微观结构知识和清洗步骤,不能简单套用K线处理逻辑