说明
东京证券交易所公开了前10名的方案,链接如下:
https://github.com/J-Quants/JPXTokyoStockExchangePrediction
东京证券交易所官方也有对前10名的方案的评述,链接如下:
https://www.youtube.com/watch?v=Ax3ON-2FLBM
东京证券交易所官方将方案进行了分类。其中第一名、第二名、第三名、第六名、第七名、第八名的方案属于常规方案,第四名、第五名、第十名的方案属于创新方案。
第九名的方案是后来提交的,当时第九名的方案还没有提交给东京证券交易所,所以没有对第九名进行分类。我在看了第九的名的方案后,认为第九名的方案也属于创新方案。
本章会讨论常规方案:
- 第一名
- 第二名
- 第三名
- 第六名
- 第七名
- 第八名
在下一章《JPX-3.前十名的方案 [2/2]》会讨论创新方案:
- 第四名
- 第五名
- 第九名
- 第十名
第一名
代码
1 | import numpy as np |
1 | train_stock_prices = pd.read_csv("../input/jpx-tokyo-stock-exchange-prediction/train_files/stock_prices.csv") |
1 | def featuring_train(data): |
1 | data_train = data[data['Date']<'2022-04-01'] |
1 | model = LinearRegression() |
1 | def featuring_test(data): |
1 | env = jpx_tokyo_market_prediction.make_env() |
复现
第一名不但提交代码给了东京证券交易所,还在Kaggle开源了其代码,这两份代码是一样的。
但是,第一名提供的代码,无法复现0.381这个分数,我实验了多次,分数都是0.277,第一名可能有所保留。
(和我对第一名的方案进行重构没有关系,即使我用重构前的代码,也还是0.277,无法复现。)
Kaggle链接:https://www.kaggle.com/code/shokisakai/jpx-regression
解读
模型
线性回归模型。
缺失值处理
对于ExpectedDividend
,填充为0。
对于Open
、High
、Low
、Close
,填充为前后值。示例代码:
1 | cols = ['Open', 'High', 'Low', 'Close'] |
对于Volume
,该方案没有对其缺失值进行处理。
特征衍生
作者衍生了两个特征
Daily_Range
,收盘价减去开盘价。Mean
,(最高价 + 最低价) / 2,然后向下取整。
1 | data['Daily_Range'] = data['Close'] - data['Open'] |
标准化
最后,作者对Open
、High
、Low
、Close
、Volume
、Daily_Range
、Mean
这些特征进行标准化(Z-Score标准化),即把原始数据映射到均值为0,方差为1的范围内。
其中
- 代表平均值
- 代表标准差
东京证券交易所二部
对于东京证券交易所二部的数据,作者也将其加入了模型的训练。
困惑
特征工程部分
作者的特征工程代码,有一处不合逻辑:缺失值的处理。
作者应该按股票进行分组,再按日期进行排序,然后进行缺失值处理,对于Open
、High
、Low
、Close
,填充为前后值。
如果我略微调整一下数据的顺序,例如我加上这么一行
1 | data.sort_values(by='Close', inplace=True) |
这时候的分数是-0.063。
测试数据处理部分
还有一处,其实存在争议。测试数据的特征处理。
在对训练数据进行标准化的时候,是基于全市场的所有股票的所有训练日期的数据进行标准化。
但是,在对测试数据进行标准化的时候,是基于全市场的所有股票的某一天的数据进行标准化,该处不合理。
姑且可以认为,在大样本情况下(样本数2000),该部分可以忽略。
第二名
代码
1 | import math |
1 | # 设置随机数种子 |
1 | # 只用了东京证券交易所一部的数据 |
1 | # 特征衍生 |
1 | # 均方误差 |
1 | train = add_features(train) |
1 | # 每只股票的最大Target |
1 | features = ['High', 'Low', 'Open', 'Close', 'Volume', 'return_1month', 'return_2month', 'return_3month', |
1 | # 测试 |
1 | sample_submission = pd.read_csv("../input/jpx-tokyo-stock-exchange-prediction/example_test_files/sample_submission.csv") |
复现
第二名的方案可以复现,0.356。
而且其原代码中有一个设置随机数种子的部分,作者设置的是42,我改成0后,依旧可以能取的0.356的分数。
解读
模型
LightGBM。
作者表示,采用LightGBM,基于两点原因:
- 作者在尝试了LightGBMRanker和XGBoost,但是在验证集下,分数都不好,所以决定采用LightGBM。
- 作者曾经交易过加密货币,根据其加密货币的经验,作者认为LightGBM的效果会比其他模型都好。
缺失值和异常值处理
对于缺失值和np.inf
、-np.inf
两个异常值,一律用0
代替。
特征衍生
作者对收盘价Close
进行了如下的衍生
- 相比20个交易日前的变化
- 相比40个交易日前的变化
- 相比60个交易日前的变化
- 过去20个交易日的波动率
- 过去40个交易日的波动率
- 过去60个交易日的波动率
- 过去20个交易日的平均
- 过去40个交易日的平均
- 过去60个交易日的平均
1 | # 相比20个交易日前的变化 |
训练集和验证集的划分
在训练集和验证集的划分方面,作者并不是按照时间顺序划分,更不是乱序后进行划分,而是按照Target进行划分。
作者首先计算了2017-01-04
到2021-12-03
的每一只股票的最大Target和最小Target的差,然后
- 训练集为:Target差最大的1000只股票的数据
- 验证集为:Target差最大的1000只股票的数据,Target差最小的1000只股票的数据,共2000只股票的数据。
最后用2021-12-06
到2022-06-24
的数据进行了测试。
而2021-12-06
到2022-06-24
的数据,并没有加入训练集,对模型重新进行训练。最终的模型,只用了从2017-01-04
到2021-12-03
的Target差最大的1000只股票数据作为训练数据。
结果提交
巧妙的设计
在结果提交方面,该方案有一个巧妙的设计,注意如下两行:
1 | prices['target_mean'] = prices.groupby("Date")["Target"].transform('median') |
作者修改了list_spred_h(Target
差最大的1000只股票)的'Target'
值,修改为中位数。
可能的原因
在作者提交给东京证券交易所的代码和文档中,没有说明他这么做的原因。但是在第十名的方案中,第十名的观点,比赛以夏普比率为衡量,夏普比率最大,就需要最大化收益的均值,同时最小化收益的波动。
在金融业务中,每一只股票都具有一定的特性,有些股票可能本身就属于收益波动很大的股票,而这些股票比较有可能在list_spred_h(Target
差最大的1000只股票)中,作者将这些股票的Target设置为中位数,那么这些股票就不太会被算作要做多或做空的股票。
我想,这是作者这么操作的原因。
困惑
该方案在特征工程部分,同样存在困惑。
- 作者应该按股票分组,计算移动平均等特征,但是作者没有这么做。
- 对于最后提交部分,其中
prices
只有某一天的数据,作者基于某一天的数据,去算移动平均,是没有意义的。
1 | for (prices, options, financials, trades, secondary_prices, sample_prediction) in iter_test: |
验证猜想
猜想修改list_spred_h(Target
差最大的1000只股票)的'Target'
为中位数,这个发挥了作用。
为了验证这个猜想,不修改ist_spred_h(Target
差最大的1000只股票)的'Target'
,再进行提交,结果为0.231,小于修改情况下的0.36。
第三名
代码
1 | import numpy as np |
1 | path = "../input/jpx-tokyo-stock-exchange-prediction/" |
1 | def fill_nans(prices): |
1 | def calc_spread_return_per_day(df, portfolio_size, toprank_weight_ratio): |
1 | def add_rank(df, col_name="pred"): |
1 | def predictor(feature_df): |
1 | df_prices = fill_nans(df_prices) |
1 | np.random.seed(0) |
1 | model = DecisionTreeRegressor(max_depth=max_depth) |
1 | env = jpx_tokyo_market_prediction.make_env() |
复现
第三名的方案可以复现,0.352。
而且其代码中有一个设置随机数种子的部分,作者设置的是0,我改成1后,意外取的了更高的分数,0.406。
解读
模型
决策树回归模型。
缺失值处理
缺失值处理的处理的步骤如下:
ExpectedDividend
,缺失值填充为0。- 其他字段都向前填充。
- 如果还有为空的,填充0。
1 | def fill_nans(prices): |
特征衍生
该方案只用了四个最基本的特征,没有做其他任何的特征衍生。
训练数据
该方案只选取了2021-10-01及之后的数据进行训练。
只选择部分数据进行训练,这个操作很常见。我们从一些私募基金的致歉信中,可以窥见一斑。
模型训练
决策树中有一个超参数,是决策树的最佳深度,作者搜索了一个决策树的最佳深度。搜索代码如下:
1 | np.random.seed(0) |
困惑
作者应该按股票进行分组,再按日期进行排序,然后进行缺失值处理,向前填充。
第六名
代码
1 | import numpy as np |
1 | base_dir = "../input/jpx-tokyo-stock-exchange-prediction" |
1 | TRAIN_END = "2019-12-31" |
1 | # 对收盘价进行复权 |
1 | df_price = adjust_price(df_price) |
1 | # 特征衍生 |
1 | buff = [] |
1 | def get_label(price, code): |
1 | train_X, train_y, test_X, test_y = get_features_and_label(df_price, codes, feature) |
1 | lgbm_params = { |
1 | df_price_train_raw = pd.read_csv(f"{train_files_dir}/stock_prices.csv") |
1 | env = jpx_tokyo_market_prediction.make_env() |
复现
第六名的方案可以复现,0.308。
解读
模型
LightGBM。
缺失值处理
按股票进行分组,按时间排序,然后向前填充。
1 | # 对收盘价进行复权 |
特征衍生
只衍生了一个特征,当天Close与前一个交易日的Close的差。
示例代码:
1 | def get_features(price, code): |
训练数据
训练数据时间范围
与第三名不一样,第四名是选取截止2019-12-31
作为训练数据。
可能的原因
在作者提交给东京证券交易所的代码和文档中,没有说明他这么做的原因。
我想可能是想去除2020年年初,因为新冠疫情导致的金融市场异动,作者把这部分的数据作为了异常数据。
或者是基于宏观因素考虑,用2017-01-04
到2019-12-31
的数据作为训练数据,去预测2022-07-06
到2022-10-07
的市场。
没有困惑
对于该方案,我没有困惑。所有的缺失值处理、特征衍生包括模型训练等,都可以理解。
第七名
代码
1 | import os |
1 | def data_pipeline(dir_path: str) -> Tuple[pd.DataFrame, pd.DataFrame]: |
1 | train, supplemental, sec_info = data_pipeline("../input/jpx-tokyo-stock-exchange-prediction") |
1 | class LGBMHierarchModel(): |
1 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
1 | env = jpx_tokyo_market_prediction.make_env() |
复现
第七名的方案可以复现,0.301。
解读
模型
LightGBM。
更准确的说,是33个LightGBM模型。
作者发现了东京证券交易所市场的股票有一个行业效应,即同一个行业的股票,彼此容易有相类似的表现。
所以,作者针对每一个行业,都训练了一个LightGBM的回归模型。
缺失值处理
对于缺失值,一律填充0。
特征衍生
没有进行任何特征衍生。
训练数据
作者只选取了部分数据(2020-12-23
及之后)参与训练。
没有困惑
对于该方案,我没有困惑。所有的缺失值处理、特征衍生包括模型训练等,都可以理解。
第八名
代码
Features.py
Features.py
:
1 | from enum import Enum |
Preprocessing.py
Preprocessing.py
:
1 | import numpy as np |
Trackers.py
Trackers.py
:
1 | from enum import Enum |
Validation.py
Validation.py
:
1 | import pandas as pd |
模型训练
1 | import os |
1 | if not os.path.exists(r"./Features.py"): |
1 | features = [Features.Amplitude(), Features.OpenCloseReturn(), Features.Return(), |
1 | st = StateTracker(features) |
1 | training_cols = ['SecuritiesCode', 'Open', 'High', 'Low', 'Close', 'Volume', 'AdjustmentFactor', 'ExpectedDividend', 'SupervisionFlag'] |
1 | model = lgbm.LGBMRegressor() |
1 | with open("./lgbm.pickle", "wb") as file: |
结果提交
1 | import os |
1 | if not os.path.exists(r"./Features.py"): |
1 | features = [Features.Amplitude(), Features.OpenCloseReturn(), Features.Return(), |
1 | st = StateTracker(features) |
1 | training_cols = ['SecuritiesCode', 'Open', 'High', 'Low', 'Close', 'Volume', 'AdjustmentFactor', 'ExpectedDividend', 'SupervisionFlag'] |
1 | model = None |
1 |
|
1 | env = jpx_tokyo_market_prediction.make_env() |
复现
第八名的方案无法复现,分数是0.001,达不到0.289。
第八名可能有所保留。
(和我对第八名的方案进行重构没有关系,即使我用重构前的代码,也还是0.001,无法复现。)
解读
模型
LightGBM。
缺失值处理
确实值一律填充为0。
特征衍生
作者衍生了如下的特征:
Features.Amplitude()
:振幅,当天的最高价减去最低价。Features.OpenCloseReturn()
:收盘价减去开盘价的差,再除以开盘价。Features.Return()
:当日的涨跌(收盘价减去前一个交易日的收盘价的差,再除以收盘价)。Features.Volatility(10)
:过去10个交易日,Return的波动率。Features.Volatility(30)
:过去30个交易日,Return的波动率。Features.Volatility(50)
:过去50个交易日,Return的波动率。Features.SMA("Close", 3)
:过去3个交易日的Close的移动平均线。Features.SMA("Close", 5)
:过去5个交易日的Close的移动平均线。Features.SMA("Close", 10)
:过去10个交易日的Close的移动平均线。Features.SMA("Close", 30)
:过去30个交易日的Close的移动平均线。Features.SMA("Return", 3)
:过去3个交易日的Return的移动平均线。Features.SMA("Return", 5)
:过去5个交易日的Return的移动平均线。Features.SMA("Return", 10)
:过去10个交易日的Return的移动平均线。Features.SMA("Return", 30)
:过去30个交易日的Return的移动平均线。
以Close的SMA(移动平均线)为例:
困惑
train_files/stock_prices.csv
的时间范围是2017-01-04
到2021-12-03
。
supplemental_files/stock_prices.csv
的时间范围是2021-12-06
到2022-06-24
作者在对2022-07-05
到2022-10-07
的数据进行预测的时候,应该把supplemental_files/stock_prices.csv
的数据也加上,否则计算移动的移动平均不准确,但是作者没有这么做。