【量化交易笔记】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()
- | 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 来填充缺失值。
#把股票代码做成数据表的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.XSHG | 4833.806993 |
600030.XSHG | 3125.629608 |
600031.XSHG | 2175.318676 |
600036.XSHG | 11034.704820 |
600048.XSHG | 2496.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 ratio
和growth
这两个因子
计算差值并排序
#使用真实的市值,减去模型预测的市值
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
函数部分,特别是机器学习(线性回归)部分。
回测结果
结论
- 从上图来看,回测区间从2024-01-01~至今,策略的各项指标还不对,基准选的是上证指数,标的是上证50的50支股票选出10支。
- 上述只选5个因子,但真正起作用的是只有两个因子, 你可以将其他不起作用的因子剔除,增加其他因子,以提高模型的有效性。
- 上述分析,没有将模型进行验证,在实际的过程中,是需要将数据集分为训练集和验证集,并加以验证,考虑到是线性模型,另外,反过来说,本文是预测结果与真实值的差,越大越好,这就带了两个问题,
3.1 模型预测值与真实值越大越好,则说明模型越差越好,那训练还有意义吗;
3.2 既然差值最大越好,则说该股票的市值越大越好,因此,这个模型必须是大市值,小市值根本上了排行榜;所以我在测试非50标的,效果是非常差的。 - 上述只是一个简单的思路,在实际的实践中,这个目标标签y不可能这样去实现,可能将目标的绝对值转化为相对值(比值),比如差值除以总市值成为比值,但同时在训练前,又没有差值,那样就不好弄,总之这个方法可以进一步探讨。