主题切换
3.4 金融数据特色处理
概念详解
为什么金融数据需要特殊处理
金融数据与其他领域的数据有本质区别,主要体现在两个方面:时间结构的复杂性和市场制度带来的特殊性。一个简单的pct_change()远远不够。
具体而言,金融数据处理需要处理的独特问题包括:
- 期货合约的有限生命周期(主力合约切换)
- 公司事件(分红、送股、拆股并股)的影响
- 节假日与交易日历的差异
- 高频数据的异步性与不规则采样
- 衍生品的希腊字母与风险度量
名词解释:主力合约
在期货市场中,成交量和持仓量最大的合约月份,通常将其作为连续交易的代表。
名词解释:展期 (Roll)
在期货合约到期前,将仓位从近月合约转移到远月合约的过程。展期日期的选择决定了连续价格序列中的"跳跃"。
名词解释:Tick数据
逐笔成交数据,记录每一笔交易的价格、成交量、时间和买卖方向。是最高精度的市场数据,但数据量大、噪声多、需要特殊处理。
期货主力合约拼接:看似简单实则复杂
期货合约有固定的到期日,到期后该合约不再交易。要构建用于回测的连续价格序列,必须将不同合约"拼接"起来。拼接方式的选择会显著影响回测结果:
方式1:简单价差拼接(在切换日调整价差)
此处有展示代码展开 ▼
python
continuous[t] = price_new[t] - roll_adjustment点击展开可浏览运行结果
📘 本段为公式演示片段(伪代码占位符,无完整上下文)
优点:保持展期前后的收益率连续性;缺点:回到历史某个时点的绝对价格没有经济意义
方式2:收益率拼接
此处有展示代码展开 ▼
python
continuous[t] = continuous[t-1] * (1 + return_new[t])点击展开可浏览运行结果
📘 本段为公式演示片段(伪代码占位符,无完整上下文)
优点:不产生负价格;缺点:累积误差
方式3:比值拼接
此处有展示代码展开 ▼
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:期货主力拼接
此处有展示代码展开 ▼
python
continuous = pd.concat([group.iloc[:,1:].sum().idxmax() for _, group in monthly_data.groupby(pd.Grouper(freq='M'))])点击展开可浏览运行结果
📘 本段为公式演示片段(伪代码占位符,无完整上下文)
案例2:期货主力合约切换的完整实现
此处有展示代码展开 ▼
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:股票分红复权——从头实现
此处有展示代码展开 ▼
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`(从价格序列中检测可能的公司事件(分红/拆股等导致的跳跃))。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
常见误区
误区一:永续合约价格可以直接用于回测
事实:直接使用连续的期货价格序列进行回测是危险的。在展期日,合约切换会产生"跳空",但这是人为的而非真实的损益。如果策略在展期日前一天做多、展期日继续持有,回测中会错误地记录这个跳空。正确的做法是使用展期调整后的价格序列或直接在展期日平仓。
误区二:所有的复权都可以互相转换
事实:前复权和后复权的数学关系是
误区三:Tick数据处理和K线处理一样
事实:Tick数据涉及大量微观结构噪声,需要专门的清洗步骤:
- 交易时间外数据清除
- 异常价格过滤(远偏离买卖报价)
- 重复tick移除
- 买卖方向推断(Lee-Ready算法)
- 不规则时间戳的重采样
误区四:同花顺/东方财富的数据和Wind一样
事实:不同数据源的处理方式可能不同:
- 复权基准日和方法的差异
- 成交量的单位(手vs股)
- 财务数据的调整方法
- 指数成分股调整的时间点
在多数据源交叉验证中发现不一致,是日常量化工作的常态。
实战练习
期货连续合约构建:用AkShare获取螺纹钢(RB)过去三年所有合约的日线数据,分别用三种方法(简单价差拼接、收益率拼接、比值拼接)构建连续价格序列。比较三种方法下:(a) 累计收益率的差异,(b) 波动率的差异,(c) 最大回撤的差异。
分红复权验证:选取一只高分红的A股(如银行股或白酒股),比较其"不复权价格"、"前复权价格"和"后复权价格"。验证:(a) 前复权价格 × 复权因子 ≈ 不复权价格,(b) 分红日的"不复权收益率" + "股息率" ≈ "前复权收益率"。
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线处理逻辑