获得简街市场预测大赛金牌的预测策略模型

写在前面的话:“低买高卖”听起来很简单……事实上,交易获利一直是一个难以解决的问题,在当今瞬息万变且复杂的金融市场中更是如此。电子交易允许在几分之一秒内发生数千笔交易,从而为实时寻找和利用价格差异提供了几乎无限的机会。今天为大家介绍一则有关简街市场预测大赛(Jane Street Market Prediction)里获得金牌的方案。

一、比赛背景

        比赛的主办方为简街集团,此次赛事共有 29182 名参赛者,4245 支队伍参与,收到了 7211 份投稿。参赛者需在本次挑战赛的前三个月,利用来自全球主要证券交易所的市场数据构建自身的量化交易模型,以达成收益最大化。紧接着,主办方会测验模型对未来市场收益的预测能力,并对模型能力予以排行。

       通常来讲,若能生成一个具备高度预测性的模型来选定正确的交易,那它们也会在发送市场信号方面具有重要作用,促使价格更为趋近“公平”价值。也就是说,更优的模型意味着市场在未来会更加高效。然而,开发出优质的模型将会充满挑战,其原因众多,涵盖极低的信噪比、潜在的冗余、强烈的特征相关性以及难以提出适宜的数学公式。

        简街集团耗费数十年去开发自身的交易模型以及机器学习解决方案,用于识别盈利机遇并迅速决定是否执行交易。这些模型助力 Jane Street 每日在全球 200 个交易场所交易数千种金融产品。

       毋庸置疑,此项挑战赛极大地简化了简街集团每日处理的量化问题的深度,简街集团对其现有交易模型在这一特定问题上的表现甚为满意。然而,没有什么比一个好的谜题更出色的了,期望这项挑战赛能够成为简街集团每日或许要处理的一种数据科学问题的有趣导论。简街集团期待看到 Kaggle 社区将采用全新且富有创造性的方法来应对这一交易挑战。

       评估方式:本次比赛以效用得分作为衡量尺度。测试集中的每一行代表一个交易机会,您需要预测该机会的 action 价值,1 表示进行交易,0 表示放弃交易。每笔交易 j 都有一个相关联的 weight 和 resp,代表回报。其中,针对第 i 个交易日,我们进行定义:

最后的评分表示为:

二、金牌模型介绍

模型采用AutoEncoder以及MLP的架构,整体框架如下图所示:

        其中,Autoencoder 部分运用了去噪自编码器(Denoise Autoencoder),也就是在输入数据进入 Encoder 之前,首先添加高斯噪声,如此可起到数据增强的效果,有利于缓解过拟合,同时还能够提升特征的表示能力。自编码器的 Loss 涵盖两个部分,一部分是 MSE Loss,用于数据重构;另一部分是通过一个分类器模块开展多分类任务,以此实现 action 的分类,确保 Encoder 学习获取的特征具备跟交易动作相关的特性。MLP 部分的输入包含两部分,即原始数据输入以及 Encoder 的编码结果,多层 MLP 得到的结果用于最终交易动作的分类。

        这部分的实现代码如下,完整源码可以去该notebook下获取:

https://www.kaggle.com/code/gogo827jz/jane-street-supervised-autoencoder-mlp/notebook?scriptVersionId=73762661

def create_ae_mlp(num_columns, num_labels, hidden_units, dropout_rates, ls = 1e-2, lr = 1e-3):
    
    inp = tf.keras.layers.Input(shape = (num_columns, ))
    x0 = tf.keras.layers.BatchNormalization()(inp)
    
    encoder = tf.keras.layers.GaussianNoise(dropout_rates[0])(x0)
    encoder = tf.keras.layers.Dense(hidden_units[0])(encoder)
    encoder = tf.keras.layers.BatchNormalization()(encoder)
    encoder = tf.keras.layers.Activation('swish')(encoder)
    
    decoder = tf.keras.layers.Dropout(dropout_rates[1])(encoder)
    decoder = tf.keras.layers.Dense(num_columns, name = 'decoder')(decoder)

    x_ae = tf.keras.layers.Dense(hidden_units[1])(decoder)
    x_ae = tf.keras.layers.BatchNormalization()(x_ae)
    x_ae = tf.keras.layers.Activation('swish')(x_ae)
    x_ae = tf.keras.layers.Dropout(dropout_rates[2])(x_ae)

    out_ae = tf.keras.layers.Dense(num_labels, activation = 'sigmoid', name = 'ae_action')(x_ae)
    
    x = tf.keras.layers.Concatenate()([x0, encoder])
    x = tf.keras.layers.BatchNormalization()(x)
    x = tf.keras.layers.Dropout(dropout_rates[3])(x)
    
    for i in range(2, len(hidden_units)):
        x = tf.keras.layers.Dense(hidden_units[i])(x)
        x = tf.keras.layers.BatchNormalization()(x)
        x = tf.keras.layers.Activation('swish')(x)
        x = tf.keras.layers.Dropout(dropout_rates[i + 2])(x)
        
    out = tf.keras.layers.Dense(num_labels, activation = 'sigmoid', name = 'action')(x)
    
    model = tf.keras.models.Model(inputs = inp, outputs = [decoder, out_ae, out])
    model.compile(optimizer = tf.keras.optimizers.Adam(learning_rate = lr),
                  loss = {'decoder': tf.keras.losses.MeanSquaredError(), 
                          'ae_action': tf.keras.losses.BinaryCrossentropy(label_smoothing = ls),
                          'action': tf.keras.losses.BinaryCrossentropy(label_smoothing = ls), 
                         },
                  metrics = {'decoder': tf.keras.metrics.MeanAbsoluteError(name = 'MAE'), 
                             'ae_action': tf.keras.metrics.AUC(name = 'AUC'), 
                             'action': tf.keras.metrics.AUC(name = 'AUC'), 
                            }, 
                 )
    
    return model

       模型训练的时候采用了PurgedGroupTimeSeriesSplit方式对训练数据进行划分,它可以保证不会有未来数据的泄露,同时也可以采用n-folds的方式进行交叉验证,实验时作者采用了5-folds的方式。

if not TEST:
    scores = []
    batch_size = 4096
    gkf = PurgedGroupTimeSeriesSplit(n_splits = n_splits, group_gap = group_gap)
    for fold, (tr, te) in enumerate(gkf.split(train['action'].values, train['action'].values, train['date'].values)):
        ckp_path = f'JSModel_{fold}.hdf5'
        model = create_ae_mlp(**params)
        ckp = ModelCheckpoint(ckp_path, monitor = 'val_action_AUC', verbose = 0, 
                              save_best_only = True, save_weights_only = True, mode = 'max')
        es = EarlyStopping(monitor = 'val_action_AUC', min_delta = 1e-4, patience = 10, mode = 'max', 
                           baseline = None, restore_best_weights = True, verbose = 0)
        history = model.fit(X[tr], [X[tr], y[tr], y[tr]], validation_data = (X[te], [X[te], y[te], y[te]]), 
                            sample_weight = sw[tr], 
                            epochs = 100, batch_size = batch_size, callbacks = [ckp, es], verbose = 0)
        hist = pd.DataFrame(history.history)
        score = hist['val_action_AUC'].max()
        print(f'Fold {fold} ROC AUC:\t', score)
        scores.append(score)

        K.clear_session()
        del model
        rubbish = gc.collect()
    
    print('Weighted Average CV Score:', weighted_average(scores))

       除此以外,作者亦运用了一些别的 tricks,像是 Early-Stop、BatchNormalization 还有超参数搜索等等,此外,自编码器的激活函数运用到了 swish,而非 relu 抑或 leaky-relu,不过依据作者所言,采用哪种激活函数差别不大。如果大家对这个方案感兴趣,推荐延展阅读如下:

https://www.kaggle.com/competitions/jane-street-market-prediction/data

https://www.kaggle.com/code/gogo827jz/jane-street-supervised-autoencoder-mlp/data?scriptVersionId=73762661&select=JSModel_0.hdf5


本文内容仅仅是技术探讨和学习,并不构成任何投资建议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老余捞鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值