Kaggle赛题:Optiver 预测美股收盘走势

Optiver - Trading at the Close | Kaggle

赛题背景

证券交易所是快节奏、高风险的环境,每一秒都很重要。随着交易日接近尾声,这种强度不断升级,并在关键的最后十分钟达到顶峰。这些时刻通常以波动加剧和价格快速波动为特征,在塑造当今全球经济叙事方面发挥着关键作用。

纳斯达克证券交易所的每个交易日都以纳斯达克收盘交叉拍卖结束。此过程确定了在交易所上市的证券的官方收盘价。这些收盘价是投资者、分析师和其他市场参与者评估个别证券和整个市场表现的关键指标。

在这个复杂的金融环境中,Optiver 是一家全球领先的电子做市商。在技​​术创新的推动下,Optiver 交易大量金融工具,例如衍生品、现金股票、ETF、债券和外币,在全球主要交易所为数千种此类工具提供具有竞争力的双边价格。 在纳斯达克交易所交易时段的最后十分钟,Optiver 等做市商将传统订单簿数据与拍卖簿数据合并。这种整合两个来源信息的能力对于向所有市场参与者提供最优惠的价格至关重要。

在本次比赛中,您面临的挑战是开发一个模型,能够使用订单簿和股票收盘竞价中的数据来预测数百只纳斯达克上市股票的收盘价变动。拍卖信息可用于调整价格、评估供需动态以及识别交易机会。 您的模型可以有助于整合来自拍卖和订单簿的信号,从而提高市场效率和可访问性,特别是在交易的最后十分钟内。您还将获得处理现实世界数据科学问题的第一手经验,类似于 Optiver 的交易员、定量研究人员和工程师所面临的问题。

赛题任务

在本次比赛中,您面临的挑战是开发一个模型,能够使用订单簿和股票收盘竞价中的数据来预测数百只纳斯达克上市股票的收盘价变动。拍卖信息可用于调整价格、评估供需动态以及识别交易机会。

评价指标

Submissions are evaluated on the Mean Absolute Error (MAE) between the predicted return and the observed target. The formula is given by:

M A E=\frac{1}{n} \sum_{i=1}^n\left|y_i-x_i\right|
  • n is the total number of data points.
  • y_i is the predicted value for data point i.
  • x_i is the observed value for data point i.

赛题时间轴

  • 2023 年 9 月 20 日 - 开始日期。
  • 2023 年 12 月 13 日 - 报名截止日期。您必须在此日期之前接受比赛规则才能参加比赛。
  • 2023 年 12 月 13 日 - 团队合并截止日期。这是参与者可以加入或合并团队的最后一天。
  • 2023 年 12 月 20 日 - 最终提交截止日期。

赛题数据

数据集包含纳斯达克证券交易所每日十分钟收盘拍卖的历史数据。您的挑战是相对于由纳斯达克上市股票组成的综合指数的未来价格走势来预测股票的未来价格走势。 这是一个使用时间序列API的预测竞赛。私人排行榜将使用提交期结束后收集的真实市场数据来确定。

[train/test].csv - 竞拍数据。测试数据将通过API提供。

  • stock_id - 股票的唯一标识符。并不是每个股票ID都存在于每个时间段中。

  • date_id - 日期的唯一标识符。日期ID在所有股票中是连续且一致的。

  • imbalance_size - 当前参考价格下未匹配的金额(以美元计)。

  • imbalance_buy_sell_flag - 反映竞拍不平衡方向的指示器。

    • 买方不平衡;1

    • 卖方不平衡;-1

    • 无不平衡;0

  • reference_price - 最大化配对股票的价格,最小化不平衡,以及最小化距离买卖双方报价中点的距离的价格,按照这个顺序。也可以认为是在最佳买卖价之间限定的接近价格。

  • matched_size - 当前参考价格下可以匹配的金额(以美元计)。

  • far_price - 基于仅考虑竞拍兴趣的情况下,将最大化匹配股票数量的交叉价格。此计算不包括连续市场订单。

  • near_price - 基于竞拍和连续市场订单,将最大化匹配股票数量的交叉价格。

  • [bid/ask]_price - 非竞拍订单簿中最具竞争力的买入/卖出水平的价格。

  • [bid/ask]_size - 非竞拍订单簿中最具竞争力的买入/卖出水平的美元名义金额。

  • wap - 非竞拍订单簿中的加权平均价格。

  • seconds_in_bucket - 自交易日收盘竞拍开始以来经过的秒数,始终从0开始。

  • target - 股票wap的未来60秒变动,减去合成指数的未来60秒变动。仅提供给训练集。

    • 合成指数是由Optiver为这个竞赛构建的纳斯达克上市股票的自定义加权指数。

    • 目标的单位是基点(basis points),这是金融市场中常见的测量单位。1个基点价格变动相当于0.01%的价格变动。

sample_submission - 一个有效的样本提交,通过API提供。

revealed_targets - 此文件中每个日期的第一个time_id提供了前一日期的整个日期的真实target值。所有其他行都包含大部分空值。

public_timeseries_testing_util.py - 一个可选的文件,旨在使运行自定义离线API测试变得更容易。请参考脚本的文档字符串以获取详细信息。

example_test_files/ - 旨在说明API如何运作的数据。包括API提供的相同文件和列。

optiver2023/ - 启用API的文件。预计API将在不到五分钟内交付所有行,并且仅占用不到0.5GB的内存。

优胜方案

第一名

Optiver - Trading at the Close | Kaggle

最终模型(CV/私人LB为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个特征。

model name validation set w/o PP validation set w/ PP test set w/o OL w/ PP test set w/ OL one time w/ PP test set w/ OL five times w/ 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*(overtime)

验证策略

我的验证策略非常简单,从前400天开始训练,选择最后81天作为我的保留验证集。CV分数与榜单分数非常吻合,这让我相信这次比赛不会有太大波动。因此,我大部分时间都专注于提高CV分数。

Magic Features

模型最终有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, i)) for col in base_features]
  1. 基于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的特征。

模型

像往常一样,对于CatBoost没有什么可说的,只是简单地训练和预测。GRU的输入张量形状为(batch_size, 55, dense_feature_dim),接着是4层GRU,输出张量形状为(batch_size, 55)。

Transformer的输入张量形状为(batch_size, 200, dense_feature_dim),接着是4层Transformer编码器层,输出张量形状为(batch_size, 200)。一个小技巧是将输出转换为零均值是有帮助的。

out = out - out.mean(1, keepdim=True)

在线学习策略

为了实现每12天重新训练一次,总共5次,以及利用数据加载技巧来增加特征数量,你可以采取以下步骤:

  1. 每天保存一份训练数据文件:将每天的训练数据保存为一个单独的文件,确保每个文件只包含当天的数据。
  2. 逐日加载数据:在训练过程中,每次加载数据时,只加载当天的数据。这样可以有效地减少内存的使用量,并且可以处理更多的特征。
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

结果后处理

根据指标的反馈,减去加权平均值比零均值更好。

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()

第六名

Optiver - Trading at the Close | Kaggle

  1. 数据预处理:使用零填充处理缺失值,并采用标准缩放对特征进行归一化处理。

  2. 特征工程:总共使用了35-36个特征,包括原始输入特征、针对“seconds_in_bucket”变量的二进制标志,以及从公共笔记本中借鉴的额外特征,如成交量、中间价格和各种不平衡度量(流动性、匹配、大小、成对、谐波)。

  3. 建模方法

    • 序列到序列Transformer(3个略有变化的模型):64维度,编码器使用2天的历史数据,解码器使用4个堆叠的Transformer层,头部使用简单线性输出层。编码器和解码器都使用基于股票的(注意力)层。
    • GRU(1个模型):类似的序列到序列架构(仅解码器),128维度,解码器使用2个GRU层,头部由2个全连接层组成。解码器和头部都使用基于股票的(注意力)层。
      所有模型的输出形状为(批量大小,股票数量,55)。
      为了利用竞赛的时间序列特性,采用在线增量学习,每天仅使用新的未见数据更新模型(用于解码器)。
  4. 验证策略:采用简单的基于时间的拆分进行验证,前359天用于训练,最后121天用于验证。由于每个训练周期后评估指标不稳定,采用指数移动平均法对值进行平滑处理,以比较模型。为了评估在线增量学习,使用最新的20天数据对模型进行验证。

  5. 后处理:除一个模型外,所有模型均采用额外约束进行训练,以强制模型输出之和为零。

  6. 集成:最终集成模型由3个Transformer模型和1个GRU模型的预测结果的平均值组成。

  7. 最终结果:最终提交在私人排行榜上排名第6,平均绝对误差(MAE)为5.4285。

第七名

方法

我的方法结合了LightGBM和神经网络模型,对神经网络进行了最小程度的特征工程。其目标是将这些模型协同作用,以减少最终预测结果的方差。

特征工程

  • LightGBM增强
    • 利用的特征包括:
      • 订单簿不平衡度:利用公开分享的imb1、imb2等特征。
      • 趋势指标:使用diff()函数进行时间变化。
      • 基于成交量的累积:对时间进行成交量累积。
      • 全局股票统计信息:计算历史股票数据的均值、中位数和标准差。
      • 偏差特征:树模型和神经网络模型都受益于以下原始特征与中位数的偏差:
      • 在线学习:用于神经网络和LightGBM模型。

偏差特征在线学习有助于显著降低误差。

对原始特征进行偏差特征处理的函数如下:

def create_deviation_within_seconds(df, num_features):
    groupby_cols = ['date_id', 'seconds_in_bucket']
    new_columns = {}
    for feature in num_features:
        grouped_median = df.groupby(groupby_cols)[feature].transform('median')
        deviation_col_name = f'deviation_from_median_{feature}'
        new_columns[deviation_col_name] = df[feature] - grouped_median
    return pd.concat([df, pd.DataFrame(new_columns)], axis=1)

神经网络架构:该架构包括LSTM和ConvNet模型,结合全局股票统计信息和偏差特征,以提高收敛性。

我已经在Kaggle上发布了神经网络模型的结构,链接如下:

验证策略

采用简单的基于时间的拆分进行模型验证。

扩展的一维卷积模型

# 省略部分代码...

def create_rnn_model_with_residual(window_size, numerical_features, initial_learning_rate=0.001):
    # 省略部分代码...

    kernel_sizes = [2, 3]
    do_ratio = 0.4

    flattened_conv_output = apply_conv_layers(numerical_input, kernel_sizes, do_ratio=do_ratio)
    flattened_conv_output_cat = apply_conv_layers(embedding, kernel_sizes, do_ratio=do_ratio)
    flattened_conv_output_diff = apply_conv_layers(combined_diff_layer, kernel_sizes, do_ratio=do_ratio)

    dense_output = Concatenate(axis=-1)([flattened_conv_output, flattened_conv_output_cat, flattened_conv_output_diff, Reshape((-1,))(combined_diff_layer), first_numerical, first_embedding])

    # 省略部分代码...

    return model

第九名

train: GitHub - ChunhanLi/9th-kaggle-optiver-trading-close
inference: 9th-submission | Kaggle

模型

  • 使用3个不同的种子训练Xgboost模型,特征数量为157。
    • 在评分方面,Xgboost和Lightgbm的差别不大。但是,GPU上的Xgboost训练速度比GPU上的Lightgbm快。

特征工程

  • 首先,根据原始特征创建一些“基本特征”(例如,从原始特征中加减乘除)。同时,创建一些基于原始大小特征的中位数标准化特征。
size_col = ['imbalance_size', 'matched_size', 'bid_size', 'ask_size']
for col in size_col:
    train[f"scale_{col}"] = train[col] / train.groupby(['stock_id'])[col].transform('median')
  • 其次,在原始特征和“基本特征”上进行进一步的特征工程/聚合:
    • imb1、imb2特征
    • 从公开笔记本中复制的市场紧急程度特征
    • 不同时间窗口的差异特征
    • 不同时间窗口的位移特征
    • 不同时间窗口的滚动均值/标准差特征
    • 使用历史wap计算6秒前的目标值,然后进行一些滚动均值
    • 一些全局date_id+seconds加权特征
    • MACD特征
    • 根据stock_id + seconds_in_bucket进行目标滚动均值

特征选择

  • 由于推断时间和内存受限,进行一些特征选择是至关重要的。我将特征分组,并检查本地交叉验证是否改善。每个特征组通常包含10-30个特征。如果一个特征组使本地交叉验证得分提高,我会逐个添加此特征组内的特征,并通常只保留5-10个最有效的特征。
  • 我在最终模型中保留了157个特征。

后处理

  • 减去加权和。根据目标的定义,所有股票的目标加权和应该为零。
test_df['pred'] = lgb_predictions
test_df['w_pred'] = test_df['weight'] * test_df['pred']
test_df["post_num"] = test_df.groupby(["date_id","seconds_in_bucket"])['w_pred'].transform('sum') / test_df.groupby(["date_id","seconds_in_bucket"])['weight'].transform('sum')
test_df['pred'] = test_df['pred'] - test_df['post_num']

其他

  • 使用xgb的MAE目标函数
  • 对于最近45天的数据,使用1.5的样本权重
  • 在线训练。我只重新训练了模型两次。一次是N天(N是私有排行榜的开始日期),另一次是N+30天。
  • Polars和reduce_mem_usage函数起到了很大的帮助作用。

第14名

Business context: Optiver - Trading at the Close | Kaggle
Data context: Optiver - Trading at the Close | Kaggle

方法概述

首先,我们修复了公开笔记本中的错误,并对本地验证进行了小改动,以避免在处理全局特征时出现数据泄漏。

df['mid_price_movement'] = df.groupby(["stock_id"])['mid_price'].diff(periods=5).apply(lambda x: 1 if x > 0 else (-1 if x < 0 else 0))

(顺便说一句,这个修复提高了CV但降低了公共LB得分。我也不知道为什么哈哈)

此外,我们为RSI、MACD和布林带指标编写了自己的函数,因为公开的函数在推断时产生了不准确的结果。

有效的方法(对CV的影响)

  • 基于已揭示目标的特征。我们使用了lag 1,2,3对按stock_id和seconds_in_bucket分组的目标作为模型的特征(-0.005)。
  • 不平衡大小的有符号表示(-0.003)。
  • 使用已揭示目标进行连续模型训练。我们定期重新训练LGB和CatBoost模型(稍后详述)(-0.006)。
  • 以流式方式进行CV。为此,我们将每个时间段的数据保存在_.csv文件中,并按时间顺序逐个传送它们以计算CV。这花费了更多时间,但与公共LB的相关性更好。
  • 零和后处理(-0.005)(但我们不确定在私有LB上的效果,所以我们只选择了一个具有此功能的提交)。
  • 全局特征(-0.004)。每当我们想要重新训练模型以使其保持最新时,我们都必须重新初始化这些值。
  • RSI、MACD和布林带技术指标。为了获得良好的结果,我们不得不重写它们(-0.002)。

未起作用的方法(对CV的影响)

  • 对滚动特征使用以date_id和stock_id分组,而不是只以stock_id分组(+0.003)。最后我们没有这样做。
  • 使用较大的x值对滞后特征shift(x)。CV得分更好,但公共LB得分更差。
  • 使用较大的x值对窗口x的滚动特征。CV得分更好,但公共LB得分更差。
  • 行业特征(+0.002)。
  • 神经网络。
  • 三元不平衡(+0.001)。我们意识到这个特征的值非常不稳定,因为精度问题,所以我们决定放弃它,尽管它提高了公共LB得分(从5.3315变为5.3327)。
  • 基于特征重要性丢弃特征。
  • 零均值后处理。因为某种原因,这使得我们的集成模型与LB/CV的相关性更差,所以我们没有选择这个。事实上,与零和后处理相比,集成模型效果更差(5.333比5.3327)。

有关方法的其他讨论点在这里中进行了概述。

提交详情

总的来说,经过仔细考虑,我们选择了以下两个提交。感谢 @cody11null 调优参数并使用您的大型91模型脚本进行测试。

170个特征,无后处理。公共得分5.3384,私有得分5.4457。LGB + CatBoost。
重新训练策略(假设X天是第一天,当前得分为True):

  • 第X天:重新训练LGB,CAT
  • 第X+6天:重新训练LGB
  • 第X+12天:重新训练CAT
  • 第X+18天:重新训练LGB
  • 第X+54天:重新训练LGB
  • 第X+60天:重新训练CAT

193个特征,零和后处理。公共得分5.3327,私有得分5.4458。LGB + CatBoost。
重新训练策略(假设X天是第一天,当前得分为True):

  • 第X天:重新训练LGB,CAT
  • 第X+9天:重新训练LGB
  • 第X+18天:重新训练CAT
  • 第X+27天:重新训练LGB
  • 第X+54天:重新训练CAT

在每个时间段,我们使用最新的LGB和最新的CAT,并提交这两个模型的平均预测值。

我们尝试了一段时间使用股票权重,但在重新训练时遇到了提交评分错误,所以没有继续进行 :cry: