基于 Beneish 模型增强选股策略
引言
Beneish模型是一个公司金融领域常见的财务模型。它主要着眼于公司的财务指标,对公司的财务合理程度进行打分,以此来判断一家公司是否具有操控盈利数据,财务报表作假的现象。本文将要介绍的选股策略即是基于Beneish模型的一个多因子选股方法。
概念简介
Beneish 模型是由Messod D. Beneish(1999)在文章中提出的,文中模拟建立了一个检测利润操纵的模型,模型的变量用来补货利润操纵的可能结果或是可能促使公司进行利润操纵的先决条件。该模型通过对公司的一些财务比率进行分析计算,为公司进行打分,称为M 打分法。其计算公式的原始版本如下:
其中,各参数的含义和计算公式为:
分数越高,表明一家公司财务操纵的可能性越大,通常当M Score 大于-2.22 则认为公司很有可能出现了财务操纵的行为。综合考虑以上8 个指数,历史研究证明该模型可以较有效地判断公司是否有财务操纵行为。
其中打分计算中各个指数的系数,是其论文中以1982-1992年被美国证监会(SEC)查处的74 家财务造假公司为观察样本,按照行业和年度配比了2332个控制样本,通过统计估计而得来的。
策略思路
通常来讲,Beneish模型的本意只是为了判断公司财务造假的可能性。对于MScore很高的公司,我们当然可以选择判断其股价虚高而不买入——但是并不能卖空。那么对于一个策略而言,什么样的股票是我们应该买入的呢?很自然的,我们会想到买入MScore最低的一部分股票。不过从逻辑上讲,一个公司财务造假的可能性更低并不代表它是一个值得买入的好公司。随着金融市场的发展和相关法律监管的完善,我们有理由相信,大部分的公司没有严重的财务造假的现象,也就是说,依据MScore得到的差异可能是不明显的。
那么,Beneish模型可以当做单纯的打分选股法来使用吗?我们还是要通过实际测试来检验。
策略细节
在实践中,我们首先应当筛选出更加适用于A股市场的指标,并剔除相关性过高的一些因子,最后通过基于A股市场上的数据回归拟合出合适的系数,对函数进行重构。这里我们直接采用研报中的重构计算公式:
策略的具体步骤为:
- 对A股全体个股,依据财报数据计算其MScore;
- 按照MScore排序,选取分值最低的100支股票;
- 持有一个季度后调仓。这里我们采用60个交易日。
策略实现
为了对比不同投资期限的投资效果,我们进行了3次回测,回测时长分别为1年,3年,10年。
(1)Beneish模型选股
调仓周期:60天
回测时间:2016.01.01~2017.01.01
回测时长:1年
收益曲线
收益归因
业绩分析
观察近一年的回测结果,我们可以发现该策略的表现很一般,与大盘收益基本持平,胜率只有38.5%。不过值得注意的是,损失主要发生在2016年年初的一波下行行情,此后策略收益有略优于大盘的表现。
(2)Beneish模型选股
调仓周期:60天
回测时间:2014.01.01~2017.01.01
回测时长:3年
收益曲线
收益归因
业绩分析
当回测时间有所延长时,我们发现该策略取得了一定的效果。年化收益率达到31.4%,复合年化收益率达到24.9%,累计超额收益49.99%。
(3) Beneish模型选股
调仓周期:60天
回测时间:2007.01.01~2017.01.01
回测时长:10年
收益曲线
收益归因
业绩分析
最后看十年期回测,我们取得了45.3%的年化收益率,18.9%的复合年化收益率。相较于3年期的结果,复合收益率略有下降,但考虑到在较长的时间保持收益,该策略依然表现良好。
小结
本文介绍了Beneish模型在选股策略方面的一个简单应用,在实际测试中取得了不错的成绩。其成功的逻辑可能在于,对于计算MScore的每一个因子,大体可以看做上期业绩与本期业绩的比值。因此MScore越小的公司,本期相对于上期的上升越大,我们就越倾向于认可该公司的价值并投资。
当然,该策略也存在很多问题,首先即是其收益与大盘相关性过大,当大盘下行时,策略很难幸免,这导致了几次回测中都出现了较大的回撤;其次,该策略在很多时候都有贴近大盘的表现,取得超额收益的时段并不多;最后,由于单纯的按照财务因子打分选股,可能出现选出的股票过于集中于某些行业和板块的情况。这些都值得投资者多加注意。
Beneish 模型作为一个传统的公司财务模型,当我们将其应用在量化选股策略中时,取得了不错的成果。这展现出量化投资海乃百川的优越性,也提示我们打开视野,从更高更广的角度考虑问题,发现不一样的灵感。
Code
# -*- coding:utf-8 -*-
from CloudQuant import SDKCoreEngine # 导入量子金服SDK
from CloudQuant import AssetType
from CloudQuant import QuoteCycle
import numpy as np
import pandas as pd
np.seterr(invalid='ignore')
config = {
'username': 'pengkun',
'password': '111111',
'rootpath': 'c:/cStrategy', # 客户端所在路径
'assetType': AssetType.Stock,
'initCapitalStock': 100000000,
'startDate': 20070101,
'endDate': 20170101,
'cycle': QuoteCycle.D,
'strategyName': 'Beneish',
'feeRate': 0.001,
'feeLimit': 5,
'stampTaxRate': 0.001,
'dealByVolume': True
}
HOLDING_PERIOD=60
HOLDING_NUMBER=100
def initial(sdk):
sdk.setGlobal('c',0)
def initPerDay(sdk):
pass
# M Score*=91.07-22.9×GMI-49.91×AQI+35.21×SGI-18.17×LVGI
def strategy(sdk):
count=sdk.getGlobal('c')
if count==0:
GM=sdk.getFactorData('LZ_CN_STKA_FIN_IND_GROSSPRFTMARGIN')
GMI=GM[-1]/GM[-1-HOLDING_PERIOD]
SG=sdk.getFactorData('LZ_CN_STKA_PRF_COMBO_Q_OPR_REV')
SGI=SG[-1]/SG[-1-HOLDING_PERIOD]
AQ=sdk.getFactorData('LZ_CN_STKA_BAL_COMBO_INTANG_ASTS')
AQI=AQ[-1]/AQ[-1-HOLDING_PERIOD]
LVG=sdk.getFactorData('LZ_CN_STKA_FIN_IND_DEBTTOASTS')
LVGI=LVG[-1]/LVG[-1-HOLDING_PERIOD]
M_SCORE=91.07-22.9*GMI-49.91*AQI+35.21*SGI-18.17*LVGI
ST = sdk.getFactorData('LZ_CN_STKA_SLCIND_ST_FLAG')[-1] # ST
STOP = sdk.getFactorData('LZ_CN_STKA_SLCIND_STOP_FLAG')[-1] # 停牌
stock_list = sdk.getStockList()
stock_list = np.array(stock_list)
condition_ST = ST == 0
condition_STOP = STOP == 0
condition = condition_ST * condition_STOP
stock_list=stock_list[condition]
M_SCORE=M_SCORE[condition]
M_SCORE=pd.Series(M_SCORE,index=stock_list)
M_SCORE=M_SCORE.sort_values()
# n=int(len(stock_list)/5)
stock_pool=M_SCORE.index[:HOLDING_NUMBER]
transferPosition(sdk, stock_pool)
count+=1
if count==HOLDING_PERIOD:
count=0
sdk.setGlobal('c',count)
def transferPosition(sdk,stock_pool):
position = sdk.getPositions()
position_dict = dict([i.code, i.optPosition] for i in position)
stock_to_buy = set(stock_pool) - set(position_dict.keys())
stock_to_sell = set(position_dict.keys()) - set(stock_pool)
quotes = sdk.getQuotes(list(stock_to_buy | stock_to_sell))
if stock_to_sell:
sell_orders = []
for stock in stock_to_sell:
if stock in quotes.keys():
price = quotes[stock].current
volume = position_dict[stock]
order = [stock, price, volume, -1]
sell_orders.append(order)
if sell_orders:
sdk.makeOrders(sell_orders)
sdk.sdklog("------------------------------------------")
sdk.sdklog(sdk.getNowDate(),"DATE")
sdk.sdklog(sell_orders,"SELL")
if stock_to_buy:
available_cash = sdk.getAccountInfo().availableCash
available_cash_one_stock = available_cash / len(stock_to_buy)
buy_orders = []
for stock in stock_to_buy:
if stock in quotes.keys():
price = quotes[stock].open
volume = int(available_cash_one_stock / (price * 100)) * 100
if volume > 0:
order = [stock, price, volume, 1]
buy_orders.append(order)
if buy_orders:
sdk.makeOrders(buy_orders)
sdk.sdklog("------------------------------------------")
sdk.sdklog(sdk.getNowDate(),"DATE")
sdk.sdklog(buy_orders,'BUY')
def run_all():
config['initial'] = initial
config['strategy'] = strategy
config['preparePerDay'] = initPerDay
# 启动SDK
SDKCoreEngine(**config).run()
if __name__ == '__main__':
run_all()
关键字:模型预测控制, 股票投机, 财务分析
风险提示及免责条款
市场有风险,投资需谨慎。本文不构成个人投资建议,也未考虑到个别用户特殊的投资目标、财务状况或需要。用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!