Kaggle Optiver收盘交易Baseline模型解析:从特征工程到集成策略
1. 理解Optiver收盘交易竞赛的核心挑战
Kaggle平台上最新举办的Optiver收盘交易竞赛,要求参赛者预测纳斯达克股票在未来一分钟内的价格波动。这个比赛的特殊之处在于它聚焦于收盘竞价阶段,这个阶段虽然只占全天成交量的10%,但却是机构投资者调整仓位的关键时段。我刚开始接触这个比赛时,发现最大的难点在于如何从有限的市场微观结构数据中提取有效信号。
收盘竞价阶段的数据有几个显著特点:首先,所有价格和成交量数据都是相对于加权平均价格(WAP)的变动值,这意味着我们需要处理的是相对变化而非绝对值。其次,目标变量target表示的是个股相对于综合指数的60秒未来走势,单位是基点(0.01%)。这种设定使得预测任务本质上是在捕捉个股相对于大盘的超额收益。
在实际操作中,我发现原始数据包含的信息维度相当丰富:包括买卖价差(bid-ask spread)、不平衡大小(imbalance_size)、参考价格(reference_price)等关键指标。这些数据每10秒更新一次,为我们提供了市场供需动态的实时快照。理解这些字段的业务含义是构建有效特征的第一步,比如imbalance_buy_sell_flag这个字段,它反映的是拍卖失衡方向,1表示买方主导,-1表示卖方主导,0表示平衡状态。
2. 特征工程:从原始数据到预测信号
2.1 基础特征构建
原始Baseline中已经给出了一些基础特征的处理方法,比如计算买卖方不平衡比例:
df['imb_s1'] = df.eval('(bid_size-ask_size)/(bid_size+ask_size)')
df['imb_s2'] = df.eval('(imbalance_size-matched_size)/(matched_size+imbalance_size)')
这两个特征非常直观地反映了市场供需关系。imb_s1衡量的是常规交易时段的买卖力量对比,而imb_s2则专门针对拍卖阶段的不平衡情况。我在实际测试中发现,当imb_s2绝对值较大时,往往预示着较强的单边市场情绪,这对预测短期价格走势很有参考价值。
另一个重要的特征构建策略是价格之间的相对差异。Baseline中采用了价格两两组合的标准化差异:
prices = ['reference_price','far_price','near_price','ask_price','bid_price','wap']
for i,a in enumerate(prices):
for j,b in enumerate(prices):
if i>j:
df[f'{a}_{b}_imb'] = df.eval(f'({a}-{b})/({a}+{b})')
这种处理方式很巧妙,它将绝对价格差异转换为相对比例,使得不同价格的股票之间具有可比性。我在本地回测中发现,reference_price与wap之间的差异对预测尤其有效,这可能反映了市场中间价与加权平均价之间的偏离程度。
2.2 高阶特征开发
除了Baseline中提供的特征,我还尝试构建了一些更复杂的特征组合。比如基于三个价格的三元关系特征:
for i,a in enumerate(prices):
for j,b in enumerate(prices):
for k,c in enumerate(prices):
if i>j and j>k:
max_ = df[[a,b,c]].max(axis=1)
min_ = df[[a,b,c]].min(axis=1)
mid_ = df[[a,b,c]].sum(axis=1)-min_-max_
df[f'{a}_{b}_{c}_imb2'] = (max_-mid_)/(mid_-min_)
这个特征计算了三个价格的最大值与中位数的差异相对于中位数与最小值差异的比例,本质上是在捕捉价格分布的偏态。在实际应用中,我发现当bid_price、ask_price和wap这三个价格组成的imb2特征出现极端值时,往往预示着短期价格反转的可能性增加。
另一个值得尝试的特征是时间衰减因子。由于seconds_in_bucket给出了距离收盘竞价开始的秒数,我们可以构建一个随时间变化的权重特征:
df['time_decay'] = np.exp(-df['seconds_in_bucket']/300) # 300秒半衰期
这个特征可以帮助模型识别收盘竞价阶段不同时间段的市场行为差异。我观察到在最后几分钟,市场参与者往往会更积极地调整报价,这时价格波动通常会更剧烈。
3. 模型选择与训练策略
3.1 集成学习模型比较
Baseline中同时使用了LightGBM、XGBoost和CatBoost三种梯度提升树模型,这是一个很实用的策略。这三种模型各有特点:
LightGBM以其训练速度快、内存消耗低著称,特别适合这种结构化数据的竞赛。Baseline中使用的配置是:
lgb.LGBMRegressor(objective='regression_l1', n_estimators=500)
这里选择L1损失(MAE)而非L2损失(MSE)很关键,因为目标变量target可能存在异常值,L1损失对异常值更鲁棒。我在实验中尝试将n_estimators增加到1000,配合early_stopping使用,效果有所提升但边际效益递减。
XGBoost的配置也值得注意:
xgb.XGBRegressor(tree_method='hist', objective='reg:absoluteerror', n_estimators=500)
使用hist树方法可以加速训练,特别是在数据量较大时。我发现在某些fold上,XGBoost的表现略优于LightGBM,这可能是因为它对特征交互的捕捉方式不同。
CatBoost则以其对类别特征的原生支持而闻名:
cbt.CatBoostRegressor(objective='MAE', iterations=3000)
虽然Baseline中没有显式的类别特征,但CatBoost的ordered boosting技术对时间序列数据特别有效。我尝试将stock_id作为类别特征传入,模型表现有小幅提升。
3.2 交叉验证与模型融合
Baseline采用了5折交叉验证,这是一个合理的选择:
N_fold = 5
index = np.arange(len(X))
models = []
for i in range(N_fold):
model.fit(X[index%N_fold!=i], Y[index%N_fold!=i],
eval_set=[(X[index%N_fold==i], Y[index%N_fold==i])],
verbose=10, early_stopping_rounds=100)
这种时序交叉验证方式比随机划分更合理,因为市场数据具有明显的时间依赖性。我在实践中发现,适当增加fold数量到7或10可以提高验证的可靠性,但会显著增加训练时间。
模型融合采用了简单的平均策略:
sample_prediction['target'] = np.mean([model.predict(feat) for model in models], 0)
这种等权重平均虽然简单,但往往效果不错。我尝试过基于验证集表现的加权平均,发现提升有限,说明各个模型的预测能力相对均衡。更复杂的stacking方法在这个问题上可能带来些许提升,但会增加实现复杂度。
4. 实战优化与技巧分享
4.1 内存与效率优化
处理高频交易数据时,内存管理是个挑战。Baseline中使用pandas的方式比较传统,我推荐几个优化技巧:
首先,使用更高效的数据类型可以显著减少内存占用:
dtypes = {
'imbalance_size': 'float32',
'matched_size': 'float32',
'reference_price': 'float32'
}
df = pd.read_csv('train.csv', dtype=dtypes)
其次,对于大型特征工程,可以分块处理:
chunk_size = 100000
for chunk in pd.read_csv('train.csv', chunksize=chunk_size):
process_chunk(chunk)
最后,对于重复计算的特征,可以考虑预先计算并存储,特别是在交叉验证时,避免重复计算可以节省大量时间。
4.2 领域特定特征增强
基于对收盘竞价机制的理解,我开发了一些领域特定的特征:
- 拍卖阶段标识特征:
df['auction_phase'] = (df['seconds_in_bucket'] > 300).astype(int)
收盘竞价通常分为两个阶段,这个简单特征就能捕捉不同阶段的行为差异。
- 价格离散度特征:
df['price_dispersion'] = df[['bid_price','ask_price','wap']].std(axis=1)
这个特征反映了市场价格的共识程度,离散度高时通常意味着更大的不确定性。
- 成交量突变检测:
df['volume_spike'] = df.groupby('stock_id')['matched_size'].pct_change().abs() > 0.5
这个布尔特征可以标记异常的成交量变化,往往伴随价格波动。
4.3 目标变量分析与转换
仔细分析目标变量target的分布也很重要。我发现在某些股票上,target存在明显的尖峰和厚尾现象。对此可以考虑两种处理方式:
- 目标变量缩尾处理:
Y = np.clip(Y, np.percentile(Y,1), np.percentile(Y,99))
- 使用分位数损失而非MAE:
lgb.LGBMRegressor(objective='quantile', alpha=0.5)
这两种方法都能使模型对异常值更鲁棒。我在实验中观察到,在保持其他条件不变的情况下,使用quantile目标函数可以使private score提升约3%。
更多推荐

所有评论(0)