返回列表

1st place solution

598. Optiver - Trading at the Close | optiver-trading-at-the-close

开始: 2023-09-20 结束: 2024-03-22 量化投资 数据算法赛
1st place solution - Optiver Trading at the Close
作者: hyd (Kaggle Grandmaster)
排名: 第1名
比赛: Optiver Trading at the Close
发布日期: 2024年3月29日
得票数: 331

1st place solution

感谢Optiver和Kaggle举办这次精彩的金融竞赛。感谢优秀的notebook和讨论,我学到了很多。我非常高兴能够第二次solo夺冠!😃😀😀

概览

我的最终模型(CV/私有榜单得分 5.8117/5.4030)是CatBoost(5.8240/5.4165)、GRU(5.8481/5.4259)和Transformer(5.8619/5.4296)的组合,权重分别为0.5、0.3、0.2,这些权重来自验证集的搜索结果。这些模型共享相同的300个特征。

此外,在线学习(OL)和后处理(PP)在我的最终提交中也起到了重要作用。

模型名称 验证集(无PP) 验证集(有PP) 测试集(无OL,有PP) 测试集(一次OL,有PP) 测试集(五次OL,有PP)
CatBoost 5.8287 5.8240 5.4523 5.4291 5.4165
GRU 5.8519 5.8481 5.4690 5.4368 5.4259
Transformer 5.8614 5.8619 5.4678 5.4493 5.4296
GRU + Transformer 5.8233 5.8220 5.4550 5.4252 5.4109
CatBoost + GRU + Transformer 5.8142 5.8117 5.4438 5.4157 5.4030*(超时)

验证策略

我的验证策略非常简单,使用前400天训练,最后81天作为验证集。CV得分与榜单得分非常吻合,这让我相信这场比赛不会波动太大。所以我大部分时间都专注于提升CV得分。

魔法特征

我的模型最终有300个特征。其中大多数是常用特征,如原始价格、中间价格、不平衡特征、滚动特征和目标历史特征。
我将介绍一些非常有帮助且其他团队尚未分享的特征。

1 基于seconds_in_bucket_group的聚合特征

pl.when(pl.col('seconds_in_bucket') < 300).then(0).when(pl.col('seconds_in_bucket') < 480).then(1).otherwise(2).cast(pl.Float32).alias('seconds_in_bucket_group'),
*[(pl.col(col).first() / pl.col(col)).over(['date_id', 'seconds_in_bucket_group', 'stock_id']).cast(pl.Float32).alias('{}_group_first_ratio'.format(col)) for col in base_features],
*[(pl.col(col).rolling_mean(100, min_periods=1) / pl.col(col)).over(['date_id', 'seconds_in_bucket_group', 'stock_id']).cast(pl.Float32).alias('{}_group_expanding_mean{}'.format(col, 100)) for col in base_features]

2 按seconds_in_bucket分组的排名特征

*[(pl.col(col).mean() / pl.col(col)).over(['date_id', 'seconds_in_bucket']).cast(pl.Float32).alias('{}_seconds_in_bucket_group_mean_ratio'.format(col)) for col in base_features],
*[(pl.col(col).rank(descending=True,method='ordinal') / pl.col(col).count()).over(['date_id', 'seconds_in_bucket']).cast(pl.Float32).alias('{}_seconds_in_bucket_group_rank'.format(col)) for col in base_features],

特征选择

特征选择很重要,因为我们需要避免内存错误问题,并尽可能多地运行在线训练轮次。
我只是通过CatBoost模型的特征重要性来选择前300个特征。

模型

  1. 关于CatBoost没什么好说的,和往常一样,简单训练和预测即可。
  2. GRU输入张量的形状为(batch_size, 55个时间步, dense_feature_dim),然后是4层GRU,输出张量的形状为(batch_size, 55个时间步)。
  3. Transformer输入张量的形状为(batch_size, 200只股票, dense_feature_dim),然后是4层Transformer编码器层,输出张量的形状为(batch_size, 200只股票)。一个将输出转换为零均值的小技巧很有帮助。
out = out - out.mean(1, keepdim=True)

4 样本权重

在线学习策略

我每12天重新训练一次模型,总共5次。
我认为如果采用在线学习策略,大多数团队训练GBDT时最多只能使用200个特征。因为在将历史数据与在线数据连接时,需要双倍的内存消耗。
数据加载技巧可以大大增加这个数字。为了实现这一点,你应该每天保存一个训练数据文件,并逐天加载。

数据加载技巧

def load_numpy_data(meta_data, features):
    res = np.empty((len(meta_data), len(features)), dtype=np.float32)
    all_date_id = sorted(meta_data['date_id'].unique())
    data_index = 0
    for date_id in tqdm(all_date_id):
        tmp = h5py.File( '/path/to/{}.h5'.format(date_id), 'r')
        tmp = np.array(tmp['data']['features'], dtype=np.float32)
        res[data_index:data_index+len(tmp),:] = tmp
        data_index += len(tmp)
    return res

实际上,我最好的提交是在最后一次更新时超时的。如果总推理时间达到某个值,我就会跳过在线训练。
所以总共只有4次在线训练更新。我估计如果没有超时,最佳得分应该在5.400左右。
总之,我真的非常幸运!

后处理

根据评估指标的要求,减去加权平均值比平均平均值更好。

test_df['stock_weights'] = test_df['stock_id'].map(stock_weights)
test_df['target'] = test_df['target'] - (test_df['target'] * test_df['stock_weights']).sum() / test_df['stock_weights'].sum()

对我无效的方法

  1. 与1dCNN或MLP集成
  2. 对GRU模型使用多日输入而非单日输入
  3. 更大的Transformer,例如deberta
  4. 用GBDT预测目标桶均值

谢谢大家!

同比赛其他方案