多因子策略之StockRanker:基于梯度提升树的排序学习|对多支股票进行排序预测(代码+数据)

之前分享过系列的代码:

行业指数轮动:一个可实盘策略的“魔改”历程,十年年化15%(策略+代码+数据下载)

我们下载29个A股主要行业指数。

下面是etf与指数的对应关系:

etfs_indexes = {
'159870.SZ':'000813.CSI',
'512400.SH':'000819.SH',
'515220.SH':'399998.SZ',
'515210.SH': '930606.CSI',
'516950.SH': '930608.CSI',
'562800.SH': '930632.CSI',

'515170.SH': '000815.CSI',
'512690.SH': '399987.SZ',
'159996.SZ': '930697.CSI',
'159865.SZ': '930707.CSI',
'159766.SZ': '930633.CSI',

'515950.SH': '931140.CSI',
'159992.SZ': '931152.CSI',
'159839.SZ': '399441.SZ',
'512170.SH': '399989.SZ',
'159883.SZ': 'h30217.CSI',

'512980.SH': '399971.CSI',
'159869.SZ': '930901.CSI',
'515050.SH': '931079.CSI',
'515000.SH': '931087.CSI',
'515880.SH': '931160.CSI', '512480.SH': 'h30184.CSI', '515230.SH': 'h30202.CSI', '512670.SH': '399973.SZ', '515790.SH': '931151.CSI', '159757.SZ': '980032.CNI', '516110.SH': 'h30015.CSI', '512800.SH': '399986.SZ', '512200.SH': '931775.CSI', }

使用指数数据的好处在于,可回测周期比较长。

数据我已经帮大家下载好,并完成格式统一化:

图片

准备因子集——由于机器学习需要的因子比较多,不像传统量化,可能就几条规则几个因子,我们直接手写就好了。

from abc import abstractmethod


class AlphaBase:
    # 返回因子集的fields, names
    @abstractmethod
    def get_factors(self):
        pass

    def get_field_by_name(self, name):
        fields, names = self.get_factors()
        for f,n in zip(fields, names):
            if n == name:
                return f


    def get_labels(self):
        return ["label(shift(close, -1)/close - 1,0)"], ['label']

    def get_ic_labels(self):
        days = [1, 5, 10, 20]
        fields = ['shift(close, -{})/close - 1'.format(d) for d in days]
        names = ['return_{}'.format(d) for d in days]
        return fields, names

    def get_all_fields_names(self, b_ic=False):
        fields, names = self.get_factors()
        if not b_ic:
            label_fields, label_names = self.get_labels()
        else:
            label_fields, label_names = self.get_ic_labels()

        fields.extend(label_fields)
        names.extend(label_names)
        return fields, names


class AlphaLit:
    def get_all_features_names(self):
        fields,names = self.get_fields_names()
        label_field, label_name = self.get_label()

        all_fields = fields.copy()
        all_fields.append(label_field)

        all_names = names.copy()
        all_names.append(label_name)
        return all_fields, all_names

    def get_fields_names(self):
        fields = []
        names = []

        windows = [2, 5, 10, 20]
        fields += ['close/shift(close,%d) - 1' % d for d in windows]
        names += ['roc_%d' % d for d in windows]

        fields += ['avg(volume,1)/avg(volume,5)']
        names += ['avg_amount_1_avg_amount_5']

        fields += ['avg(volume,5)/avg(volume,20)']
        names += ['avg_amount_5_avg_amount_20']

        fields += ['rank(avg(volume,1))/rank(avg(volume,5))']
        names += ['rank_avg_amount_1_avg_amount_5']

        fields += ['avg(volume,5)/avg(volume,20)']
        names += ['rank_avg_amount_5_avg_amount_20']

        windows = [2, 5, 10]
        fields += ['rank(roc_%d)' % d for d in windows]
        names += ['rank_roc_%d' % d for d in windows]

        fields += ['rank(roc_2)/rank(roc_5)']
        names += ['rank_roc_2_rank_roc_5']

        fields += ['rank(roc_5)/rank(roc_10)']
        names += ['rank_roc_5_rank_roc_10']

        return fields, names

    def get_label(self):
        return "shift(close, -10)/close - 1,0)", 'label'



dataloader支持截面函数:

df_all.set_index([df_all.index,'symbol'],inplace=True)
for rank_field, rank_name in rank_exprs:
    #支持截面rank,逻辑加在这里
    se = calc_expr(df_all, rank_field)
    se.name = rank_name
    df_all[rank_name] = se

#df_all['symbol'] = df_all.index.
#df_all = df_all.set_index(df_all.index.l)
# todo groupby('date').rank(pct=True)
df_all = df_all.loc[self.start_date: self.end_date]
return df_all

支持截面函数rank(时间序列排序是ts_rank):

def rank(se: pd.Series):
    ret = se.groupby(level=1).rank(pct=True)
    return ret

图片

吾日三省吾身

今早悟到几句话,与大家分享。

作为普通人,进攻才是最好的防守。

守正很重要,出奇更重要。

普通人就是没有特别的家世,没有耀眼的资源加持,光着脚或者刚穿上鞋。一味守正,患得患失,抵御风险能力有限。

守正,注意风险底限,不碰触法律,以及不做带来不可逆风险的决策。建立系统,做时间的朋友。

但还不够多,这个时代,最焦虑的就是中产。

经济形势风吹草动都带来影响。

中考分流,行业裁员,房价波动。。。

刚装上的鞋子怕掉了。

出奇——是反脆弱,是杠铃配置。追热点,高赔率的投资,顶级的人脉圈。

可遇而不可求,但这些才是人生安全感的来源。

——游泳的时候,如何不掉下去?——往前游就是了。

时间会解释一切,因为长期来看,我们都会死,历史长河不过沧海一粟。

——人生没有意义,人生只是一场体验。

有些人小心翼翼一辈子,却没有真正活过。

在守正的基础上,大胆去尝试,去折腾。

生命在于体验,允许一切发生,体验而已。

这里说的是“AI量化”,区别于传统量化。

量化相对主观而言,以数量化的方式产生信号,“机械”执行信号。

但传统量化之于主观量化,并没有明显的优势。至少在国内这些年,仍然属于资管的边缘部门。

传统量化,更像传统技术分析的自动化版本。

只不过人工画K线,变成计算机来计算。金叉,通道突破等。技术分析的缺点,传统量化也有。

做得好一点的,加上超参数优化——对参数空间进行遍历,进而找到最优的参数集。——这一点看起来高级了一点点。

规则信号最大的问题,不好优化,就是hard-code的。而且更多的规则不好组合在一起,一般就是M条中满足N条这样的逻辑,不能更加模糊地组合到一起。

而多因子就没有这个问题,技术面,基本面,另类因子,可以通过机器学习按不同的权重复合到一个策略里,这样模型的预测能力就大幅度提升。

另外,通过排序算法,我们需要找出相对更优的股票、期货组合持有等。

StockRanker梯度提升树、排序学习用于股票排序 | gplearn和DeepAlphaGen端到端的因子挖掘系统(开源)

图片

好消息是,私募机构至少当下的主流都是多因子。

多因子很适合拥抱机器学习,深度学习。

因子,对应就是机器学习里的特征工程。

自动特征工程,自动参数优化,在AI领域都是日新月异的进步。

因此,咱们Quantlab后续的计划,主要是支持多因子策略,甚至端到端去构建的。StockRanker梯度提升树、排序学习用于股票排序 | gplearn和DeepAlphaGen端到端的因子挖掘系统(开源)

Qlib本身就是AI-驱动的量化平台,它更加彻底,压根不支持传统量化。

交易模板也仅有一个top K的轮动策略。

当然,它主要精力花在各种前沿的模型上,而不是因子挖掘和筛选上。这一点上qlib走错了方向。

AI量化还有一个很大的好处,它的形式非常统一,我们更多是去积累高质量的因子,而不是花心思去“凑”参数。这一部分甚至可以通过数据挖掘去大量产生,筛选。

比如gplearn和DeepAlphaGen这样的因子筛选框架。

咱们下周要重点在这块上发力。

星球群里好多同学在聊,实盘的事情。

量化交易系统自动化,工程化的事情。

客观地说,精力有点放错了地方。我之前也在回测系统的设计上花了很多的心力,当然这个是值得的——主要是为了写策略,优化策略更快,比如“因子表达式”、“模块化写策略”,“策略模板“,”自动化超参数优化“。

——这些在Quantlab 3.3基本都交付了。

Quantlab3.3代码发布:全新引擎 | 静态花开:年化13.9%,回撤小于15% | lightGBM实现排序学习

创业板指布林带突破策略:年化12.8%,回撤20%+| Alphalens+streamlit单因子分析框架(代码+数据)

交易里最重要的事情是策略。

策略的主流与未来是”多因子“,私募里的策略也是以多因子为主。

多因子策略的优点:容量大,容易与AI、机器学习相结合、容易优化,形式统一。

比如你从5个因子,扩展到500个因子,策略逻辑基本是不需要变的。

如果你是规则策略,这基本是做不到到。信号相互矛盾了如何处理等?

多因子分解为两个部分: 因子挖掘、因子合成(机器学习模型)。

因子合成,借鉴bigquant的思路我们来实现StockRanker。

就是通过多因子,对多支股票进行排序。

图片

下面的代码使用lightGBM对多支ETF进行排序预测。

import joblib
import os
import lightgbm as lgb


def prepare_groups(df):
    df_copy = df.copy(deep=True)
    df_copy['day'] = df_copy.index
    group = df_copy.groupby('day')['day'].count()
    return group


class StockRanker:
    def __init__(self, load_model=False, feature_cols=None):
        self.feature_cols = feature_cols
        if load_model:
            path = os.path.dirname(__file__)
            self.ranker = joblib.load(path + '/lgb.pkl')

    def predict(self, data):
        data = data.copy(deep=True)
        if self.feature_cols:
            data = data[self.feature_cols]

        pred = self.ranker.predict(data)
        return pred

    def train(self, x_train, y_train, q_train, model_save_path):
        '''
        模型的训练和保存
        :param x_train:
        :param y_train:
        :param q_train:
        :param model_save_path:
        :return:
        '''

        train_data = lgb.Dataset(x_train, label=y_train, group=q_train)
        params = {
            'task': 'train',  # 执行的任务类型
            'boosting_type': 'gbrt',  # 基学习器
            'objective': 'lambdarank',  # 排序任务(目标函数)
            'metric': 'ndcg',  # 度量的指标(评估函数)
            'max_position': 10,  # @NDCG 位置优化
            'metric_freq': 1,  # 每隔多少次输出一次度量结果
            'train_metric': True,  # 训练时就输出度量结果
            'ndcg_at': [10],
            'max_bin': 255,
            # 一个整数,表示最大的桶的数量。默认值为 255lightgbm 会根据它来自动压缩内存。如max_bin=255 时,则lightgbm 将使用uint8 来表示特征的每一个值。
            'num_iterations': 200,  # 迭代次数,即生成的树的棵数
            'learning_rate': 0.01,  # 学习率
            'num_leaves': 31,  # 叶子数
            # 'max_depth':6,
            'tree_learner': 'serial',  # 用于并行学习,‘serial’:单台机器的tree learner
            'min_data_in_leaf': 30,  # 一个叶子节点上包含的最少样本数量
            'verbose': 2  # 显示训练时的信息
        }

        #import lightgbm as lgb
        #gbm = lgb.LGBMRanker()

        #gbm.fit(x_train, y_train, group=q_train,
        #        eval_set=[(x_train, y_train)], eval_group=[q_train],
        #        eval_at=[5, 10, 20])
        #print(gbm.feature_importances_)
        gbm = lgb.train(params, train_data, valid_sets=[train_data])  # 这里valid_sets可同时加入train_dataval_data
        print(gbm.feature_importance())
        gbm.save_model(model_save_path)

    def predict(x_test, comments, model_input_path):
        '''
         预测得分并排序
        :param x_test:
        :param comments:
        :param model_input_path:
        :return:
        '''

        gbm = lgb.Booster(model_file=model_input_path)  # 加载model

        ypred = gbm.predict(x_test)

        predicted_sorted_indexes = np.argsort(ypred)[::-1]  # 返回从大到小的索引

        t_results = comments[predicted_sorted_indexes]  # 返回对应的comments,从大到小的排序

        return t_results


'''

    def train_bak(self, ds: DataSet):
        X_train, X_test, y_train, y_test = ds.get_split_data()
        X_train_data = X_train.drop('symbol', axis=1)
        X_test_data = X_test.drop('symbol', axis=1)

        # X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2, random_state=1)

        query_train = self._prepare_groups(X_train.copy(deep=True)).values
        query_val = self._prepare_groups(X_test.copy(deep=True)).values
        query_test = [X_test.shape[0]]

        import lightgbm as lgb
        gbm = lgb.LGBMRanker()

        gbm.fit(X_train_data, y_train, group=query_train,
                eval_set=[(X_test_data, y_test)], eval_group=[query_val],
                eval_at=[5, 10, 20], early_stopping_rounds=50)

        print(gbm.feature_importances_)

        joblib.dump(gbm, 'lgb.pkl')
'''

if __name__ == '__main__':
    from datafeed.dataloader import CSVDataloader
    from config import DATA_DIR

    symbols = [
        '511220.SH',  # 城投债
        '512010.SH',  # 医药
        '518880.SH',  # 黄金
        '163415.SZ',  # 兴全商业
        '159928.SZ',  # 消费
        '161903.SZ',  # 万家行业优选
        '513100.SH'  # 纳指
    ]  # 证券池列表

    loader = CSVDataloader(path=DATA_DIR.joinpath('etfs'), symbols=symbols)

    df = loader.load(fields=['slope(close,20)', 'qcut(shift(close,5)/close-1,5)'], names=['roc_20', 'label'])
    ranker = StockRanker()
    df.set_index([df.index, 'symbol'], inplace=True)
    df.sort_index(inplace=True)
    x_train = df[['roc_20']]
    y_train = df['label']
    q_train = prepare_groups(x_train)

    print(y_train)
    ranker.train(x_train=x_train, y_train=y_train, q_train=q_train, model_save_path='model.pkl')

代码在如下这个位置:

图片

发布者:股市刺客,转载请注明出处:https://www.95sca.cn/archives/103555
站内所有文章皆来自网络转载或读者投稿,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。敬请谅解!

(0)
股市刺客的头像股市刺客
上一篇 2024 年 7 月 29 日
下一篇 2024 年 7 月 29 日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注