当前位置: 首页 > news >正文

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

前言

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

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

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

获取数据

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

#导入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()
-codemcapna1/DA rationet incomegrowthRD
0600028.XSHG6888.79249.762930e+111.8807516.749000e+09-4.61NaN
1600030.XSHG3734.77782.987667e+111.2116005.138545e+0923.74NaN
2600031.XSHG1617.87337.300160e+101.9223961.113674e+0912.33242669000.0
3600036.XSHG10612.51101.233475e+121.1129703.552000e+107.53NaN
4600048.XSHG1048.61083.450036e+111.3353001.544011e+09-21.62NaN

建模

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

#把股票代码做成数据表的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)

训练并预测

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

查看模型参数

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

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 这两个因子

计算差值并排序

#使用真实的市值,减去模型预测的市值
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') ,得以重现。

回测代码

这里给出的回测代码,主要是交易部分
以下是完整的回测代码,你可以复制后,放在策略中回测

# 导入函数库
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级别低的loglog.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.valuesdf = 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/numelse:cash =0num =0for stock in stockset[:g.stocknum]:if stock in sell_list:passelse:stock_buy = stockorder_target_value(stock_buy, cash)num = num - 1if num == 0:break#同时天数加1g.days += 1#如果天数不能被5整除else:#不执行交易,直接天数加1g.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不可能这样去实现,可能将目标的绝对值转化为相对值(比值),比如差值除以总市值成为比值,但同时在训练前,又没有差值,那样就不好弄,总之这个方法可以进一步探讨。

相关文章:

  • JAVA---字符串
  • docker配置mysql遇到的问题:网络连接超时、启动mysql失败、navicat无法远程连接mysql
  • Nginx技术培训文档
  • 【Linux网络#1】:网络基础知识
  • strings.TrimLeft 使用详解
  • ssm乡村合作社商贸网站设计与实现(源码+lw+部署文档+讲解),源码可白嫖!
  • 网络流之最大流(Dinic)
  • 26考研——指令系统_CISC 和 RISC 的基本概念(4)
  • [详细无套路]MDI Jade6.5安装包下载安装教程
  • setup语法糖
  • AudioVideoMerger 下载与使用
  • ZBrush2025建模软件下载 ZBrush中文版免费下载 ZBrush版本大全
  • c++初始化数组
  • python输出
  • 利用知识图谱提升测试用例生成精准性:基于Graphiti与DeepSeek-R1的实战指南
  • 23种设计模式-行为型模式之迭代器模式(Java版本)
  • Redis数据结构SDS,IntSet,Dict
  • 【读论文】面向小目标的轻型变电设备缺陷检测算法
  • Python文件操作及数据库交互(Python File Manipulation and Database Interaction)
  • cron定时任务
  • 第一集丨《无尽的尽头》值得关注,《榜上佳婿》平平无奇
  • 103岁抗战老兵、抗美援朝老兵、医学专家张道中逝世
  • 俄罗斯称已收复库尔斯克州,普京发表讲话
  • 东北财大“一把手”调整:方红星任校党委书记,汪旭晖任校长
  • 证监会发布上市公司信披豁免规定:明确两类豁免范围、规定三种豁免方式
  • “五一”假期云南铁路预计发送旅客超330万人次