【量化交易笔记】17.多因子的线性回归模型策略

前言

上一篇介绍了 因子的评价和分析方法,让我知道如何判断该因子的作用,以及对最终结果的影响,其最大的问题,他只能评价和分析单因子,而对多个因子,不能直接加以评价。我们自然会想到,如果是多因子,他是如何影响结果,每个因子起的作用是怎么样的,这些因子哪几个比较重要,他们的次序是怎么样的,如果给定这些因子的权重,能否合成一个新的因子,这个新的因子,加以评价分析。

以上这两个问题,就是本篇和以后文章需要解决的问题。首先就因子的重要性来分析,取决于用什么模型来架构,如果是线性模型,那自然是因子的权重系数,即为该因子的系数(或斜率);如果是非线性模型,如树型模型,就可以直接取重要性排序系统来确定,如果是神经网络模型,就很难获取,解释性自然很差了。

本篇从线性回归模型来分析因子,其中线性回归的模型很多,一般线性回归、岭回归,Lasso回归、Rubust回归,最常用的就是一般线性回归。基本的思路:现以上证50为标的,以财务总资产、总负债 、净利润、年度收入增长、研发费用等因素作为因子,以市值为作目标,建立模型线性,最后以预测值与真实值的差值,进行排序,找到市值被低估最多股票作为选股的标的,进行回测。

获取数据

为了与之前的文章统一,本文仍使用聚宽平台的数据,

python 复制代码
#导入jqdata的全部函数
from jqdata import *
#这回咱们就把选择上证50成分股做股票池
stocks = get_index_stocks('000016.XSHG')
#用query函数获取股票的代码
q = query(valuation.code,
          #还有市值
          valuation.market_cap,
          #净资产,用总资产减去总负债
         balance.total_assets - balance.total_liability,
          #再来一个资产负债率的倒数
         balance.total_assets/balance.total_liability,
          #把净利润也考虑进来
         income.net_profit,
          #还有年度收入增长
         indicator.inc_revenue_year_on_year,
          #研发费用
         balance.development_expenditure
         ).filter(valuation.code.in_(stocks))
#将这些数据存入一个数据表中
df = get_fundamentals(q)
#给数据表指定每列的列名称
df.columns = ['code', 
              'mcap', 
              'na', 
              '1/DA ratio', 
              'net income', 
              'growth', 
              'RD']
#检查一下是否成功
df.head()
- code mcap na 1/DA ratio net income growth RD
0 600028.XSHG 6888.7924 9.762930e+11 1.880751 6.749000e+09 -4.61 NaN
1 600030.XSHG 3734.7778 2.987667e+11 1.211600 5.138545e+09 23.74 NaN
2 600031.XSHG 1617.8733 7.300160e+10 1.922396 1.113674e+09 12.33 242669000.0
3 600036.XSHG 10612.5110 1.233475e+12 1.112970 3.552000e+10 7.53 NaN
4 600048.XSHG 1048.6108 3.450036e+11 1.335300 1.544011e+09 -21.62 NaN

建模

把市值作为目标值,其他财务因子作为特征,用 0 来填充缺失值。

python 复制代码
#把股票代码做成数据表的index
df.index = df['code'].values
#然后把原来代码这一列丢弃掉,防止它参与计算
df = df.drop('code', axis = 1)
#把除去市值之外的数据作为特征,赋值给X
X = df.drop('mcap', axis = 1)
#市值这一列作为目标值,赋值给y
y = df['mcap']
#用0来填补数据中的空值
X = X.fillna(0)
y = y.fillna(0)

训练并预测

python 复制代码
#使用线性回归来拟合数据
reg = LinearRegression().fit(X,y)
#将模型预测值存入数据表
predict = pd.DataFrame(reg.predict(X), 
                       #保持和y相同的index,也就是股票的代码
                       index = y.index,
                       #设置一个列名,这个根据你个人爱好就好
                       columns = ['predict_mcap'])
#检查是否成功
predict.head()
- predict_mcap
600028.XSHG 4833.806993
600030.XSHG 3125.629608
600031.XSHG 2175.318676
600036.XSHG 11034.704820
600048.XSHG 2496.955008

查看模型参数

python 复制代码
# (1) 各特征系数(对应 X.columns 的顺序)
print("特征系数:", reg.coef_)  

# (2) 偏置项(截距)
print("偏置值:", reg.intercept_)  # 

特征系数: [1.6610656144885165e-09 359.032360682994 2.103156475641299e-07

-0.13441721290214032 5.958907500769328e-08]

偏置值: 1116.8298565517507

python 复制代码
for feature, coef in zip(X.columns, reg.coef_):
    print(f"{feature}: {coef:.4f}")

na: 0.0000

1/DA ratio: 359.0324

net income: 0.0000

growth: -0.1344

RD: 0.0000

从上述数据来看,真正起作用的主要是 1/DA ratiogrowth 这两个因子

计算差值并排序

python 复制代码
#使用真实的市值,减去模型预测的市值
diff = df['mcap'] - predict['predict_mcap']
#将两者的差存入一个数据表,index还是用股票的代码
diff = pd.DataFrame(diff, index = y.index, columns = ['diff'])
#将该数据表中的值,按生序进行排列
diff = diff.sort_values(by = 'diff', ascending = True)
#找到市值被低估最多的10只股票
diff.head(10)
- diff
600276.XSHG -3535.849409
601988.XSHG -3470.535336
601328.XSHG -3014.714514
601668.XSHG -2815.339172
601390.XSHG -2792.367126
601398.XSHG -2723.120877
601919.XSHG -2678.387472
600050.XSHG -2321.983808
601225.XSHG -2308.808637
601888.XSHG -1822.134836

结果分析

从上表可以看出,模型将计算出实际市值与预测市值,将这些差值最多的股票选出,进行买入持有,等这个差值变小了再卖出,基于这个思想,制定股票交易策略。

注:以上结果是基于现在(今天2025-04-26),也许你的结果与我不一样,因为get_fundamentals()函数默认获取的是当前日期的内容。你可以修改函数,将日期给写上,df = get_fundamentals(q, '2025-04-26') ,得以重现。

回测代码

这里给出的回测代码,主要是交易部分

以下是完整的回测代码,你可以复制后,放在策略中回测

python 复制代码
# 导入函数库
from sklearn.linear_model import LinearRegression, Ridge
import numpy as np
import pandas as pd
from jqdata import *

# 初始化函数,设定基准等等
def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000001.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 输出内容到日志 log.info()
    log.info('初始函数开始运行且全局只运行一次')
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')

    ### 股票相关设定 ###
    # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
    # set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')

    #定义初始日期为0 
    g.days =0 
    #每5天调仓一次
    g.refresh_rate = 5
    #最大持股的个数为10个 
    g.stocknum = 10

    ## 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'000300.XSHG'或'510300.XSHG'是一样的)
      # 开盘前运行
    run_daily(before_market_open, time='before_open', reference_security='000300.XSHG')
      # 开盘时运行
    run_daily(market_open, time='open', reference_security='000300.XSHG')
      # 收盘后运行
    run_daily(after_market_close, time='after_close', reference_security='000300.XSHG')

## 开盘前运行函数
def before_market_open(context):
    # 输出运行时间
    log.info('函数运行时间(before_market_open):'+str(context.current_dt.time()))

    # 给微信发送消息(添加模拟交易,并绑定微信生效)
    # send_message('美好的一天~')

    # 要操作的股票:平安银行(g.为全局变量)
    # g.security = '000001.XSHE'

## 开盘时运行函数
def market_open(context):
    log.info('函数运行时间(market_open):'+str(context.current_dt.time()))
        #如果天数能够被5整除,
    #就运行我们在研究环境中写好的代码 
    if g.days % 5 == 0:
        #下面的代码是从研究环境中移植过来的
        #去掉了画图和查看表头的部分
        #此处就不逐行注释了
        stocks = get_index_stocks('000016.XSHG', date = None)
        q = query(valuation.code,
          #还有市值
          valuation.market_cap,
          #净资产,用总资产减去总负债
         balance.total_assets - balance.total_liability,
          #再来一个资产负债率的倒数
         balance.total_assets/balance.total_liability,
          #把净利润也考虑进来
         income.net_profit,
          #还有年度收入增长
         indicator.inc_revenue_year_on_year,
         balance.development_expenditure
         ).filter(valuation.code.in_(stocks))
        df = get_fundamentals(q, date=None)
        df.columns = ['code', 'mcap', 'na', '1/DA ratio','net income', 'growth','RD']
        df.index = df.code.values
        df = df.drop('code',axis = 1)
        df = df.fillna(0)
        X = df.drop('mcap', axis = 1)
        y = df['mcap']
        X = X.fillna(0)
        y = y.fillna(0)


        #下面是机器学习的部分
        reg = LinearRegression ()
        model = reg.fit(X,y)
        predict =pd.DataFrame(reg.predict(X),
                            index = y.index,
                            columns = ['predict_mcap'])
        diff = df['mcap'] - predict['predict_mcap']
        diff =pd.DataFrame(diff,index =y.index, columns = ['diff'])
        diff = diff.sort_values(by = 'diff', ascending = True)
        #下面是执行订单的部分
        #首先将把市值被低估最多的10只股票存入持仓列表
        stockset = list(diff.index[:10])
        
        #同时已经持有的股票,存入卖出的列表中
        sell_list = list(context.portfolio.positions.keys())
        #如果某只股票在卖出列表中
        for stock in sell_list:
            #同时又不在持仓列表中
            if stock not in stockset[:g.stocknum]:
                #就把这只股票卖出
                stock_sell = stock
                #卖出后该股票的持仓量为0,也就是直接清仓
                order_target_value(stock_sell, 0)
        
        #如果持仓的数量小于我们设置的最大持仓数
        if len(context.portfolio.positions) < g.stocknum:
            #我们就把剩余的现金,平均买入股票#例如持仓8只股票,剩余3万块现金
            #就买入2只列表中的股票,每只买入的金额上限为1.5万元
            num = g.stocknum - len(context.portfolio.positions)
            cash = context.portfolio.cash/num
        else:
            cash =0
            num =0
        for stock in stockset[:g.stocknum]:
            if stock in sell_list:
                pass
            else:
                stock_buy = stock
                order_target_value(stock_buy, cash)
                num = num - 1
                if num == 0:
                    break
        #同时天数加1
        g.days += 1
        #如果天数不能被5整除
    else:
        #不执行交易,直接天数加1
        g.days = g.days +1

## 收盘后运行函数
def after_market_close(context):
    log.info(str('函数运行时间(after_market_close):'+str(context.current_dt.time())))
    #得到当天所有成交记录
    trades = get_trades()
    for _trade in trades.values():
        log.info('成交记录:'+str(_trade))
    log.info('一天结束')
    log.info('##############################################################')

以上回测代码,没有删除原先模板的内容,主要内容可以查看market_open函数部分,特别是机器学习(线性回归)部分。

回测结果

结论

  1. 从上图来看,回测区间从2024-01-01~至今,策略的各项指标还不对,基准选的是上证指数,标的是上证50的50支股票选出10支。
  2. 上述只选5个因子,但真正起作用的是只有两个因子, 你可以将其他不起作用的因子剔除,增加其他因子,以提高模型的有效性。
  3. 上述分析,没有将模型进行验证,在实际的过程中,是需要将数据集分为训练集和验证集,并加以验证,考虑到是线性模型,另外,反过来说,本文是预测结果与真实值的差,越大越好,这就带了两个问题,
    3.1 模型预测值与真实值越大越好,则说明模型越差越好,那训练还有意义吗;
    3.2 既然差值最大越好,则说该股票的市值越大越好,因此,这个模型必须是大市值,小市值根本上了排行榜;所以我在测试非50标的,效果是非常差的。
  4. 上述只是一个简单的思路,在实际的实践中,这个目标标签y不可能这样去实现,可能将目标的绝对值转化为相对值(比值),比如差值除以总市值成为比值,但同时在训练前,又没有差值,那样就不好弄,总之这个方法可以进一步探讨。
相关推荐
我的golang之路果然有问题14 分钟前
案例速成GO+redis 个人笔记
经验分享·redis·笔记·后端·学习·golang·go
韩明君1 小时前
前端学习笔记(四)自定义组件控制自己的css
前端·笔记·学习
灏瀚星空2 小时前
从基础到实战的量化交易全流程学习:1.1 量化交易本质与行业生态
人工智能·笔记·学习·数学建模·信息可视化
Jumbuck_102 小时前
基于OpenMV+STM32+OLED与YOLOv11+PaddleOCR的嵌入式车牌识别系统开发笔记
笔记·stm32·嵌入式硬件
努力做小白3 小时前
Linux扩展
linux·c语言·笔记
pedestrian_h3 小时前
gin框架学习笔记
笔记·学习·go·web·gin
豆沙沙包?3 小时前
7.学习笔记-Maven进阶(P75-P89)-进度(p75-P80)
笔记·学习·maven
一点.点3 小时前
李沐动手深度学习(pycharm中运行笔记)——05.线性代数
pytorch·笔记·python·深度学习·pycharm·动手深度学习
Lester_11014 小时前
嵌入式学习笔记 - HAL_xxx_MspInit(xxx);函数
笔记·学习
愚昧之山绝望之谷开悟之坡4 小时前
什么是视频上墙
人工智能·笔记