Z-Score模型的进阶版:Zeta模型
可能很多人都有听过或用过Z-Score模型,该模型是国外Altman教授在1968年开发,用来分析、判断一家企业破产的可能性。后来,Altman教授等人对Z-Score模型进行了二次开发,即Zeta信用风险模型。
Zeta模型从Z-Score模型的5个变量增加到了7个,分别是:
1.资产报酬率,采用税息前收益/总资产衡量。
2.收入的稳定性,采用对X在5-10年估计值的标准误差指标作为这个变量的度量。
3.债务偿还,可以用人们所常用的利息保障倍数(覆盖率)即利税前收益/总利息偿付来度量。
4.积累盈利,可以用公司的留存收益(资产减负债/总资产)来度量。
5.流动比率,可以用人们所熟悉的比率衡量。
6.资本化率,可以用普通股权益/总资本。
7.规模,可以用公司总资产的对数形式来度量。
PS:以上信息来源于百度百科
出于商业机密,Zeta模型的变量系数没有公开(其实公开了也不适用于当下的A股),网上能获取的相关信息也不多,但我们在知网上查到了一篇应用该模型的论文:《基于ZETA模型的我国上市公司信用风险度量研究》。今天就是以其作为本次策略研究的参考。
论文中对相关变量进行了一些调整,同时基于Fisher判别分析求得了一系列变量系数并得出相关结论:
最后的模型为:
其中,由于未进行变量标准化,模型需要多一个常数项,常数项为-4.526,变量3的系数为0;模型的阈值有两个,我们选用第二类错误更低的阈值:0.45。当模型ZETA值低于0.45时,可以认为该公司是ST类型公司,有较大信用风险。
所以,我们可以设计一个简单的反向策略,每月月底计算ZETA,买入ZETA值低于阈值的股票,回测设置如下:
-
回测时间:2017-01-01至2022-08-29(月底换股)
-
回测品种:全A股(停牌股和一年以内的次新股)
-
初始资金:100万
-
手续费:0.0008(双边万三佣金+单边千一印花税,共千1.6,即双边万8)
-
滑点:0.00123(双边千1.23)
不过,当程序刚开始运行时我们就发现,模型筛选出来的股票数量过多。平均竟有上千只股票,而这与实际情况是很不相符的。反过来审视论文,发现问题可能出现在以下几个方面。
首先,论文中的样本数量可能过少,实验组和检验组各只有23只ST股票,同时作为对照的46只相应行业的非ST股票的选取规则并没有披露。
其次,从模型拟合出来的系数来看,变量4的系数为负跟生活经验相悖,没有人期望“留存收益/总资产”比值越低越好吧?再者,这些样本只是2017年的ST股票,在模型正确的条件下也只能反应2017年的情况。
所以对于我们而言,该模型的结果应用性并不是很强。不过论文的思路还是挺好的,可以借鉴。
下面,我们保留该论文中设计的七个变量,重新计算变量系数,并且每月月底滚动更新该系数。
如何计算模型的变量系数呢?ZETA模型本质来说是一个多元线性方程,常见的方法有判别分析、Logistic回归、聚类、神经网络等。这里我们采用Logistics回归。
回归的目标为ST和非ST,用T-1期的变量和T期ST状态做模型训练,再基于训练出来的模型和T期的变量去预测T+1期的ST状态;随后等权买入模型预测的ST股(为避免退市异常,在个股退市前十个交易日卖出持仓)。
调整后重新运行回测,结果如下:
根据回测结果来看,整体是亏损的,但并没有个人想象中的持续亏损。
查询策略具体持仓之后发现,今年6月份以来垃圾股持续火热,例如持仓中的*ST顺利和*ST西源,红红火火恍恍惚惚竟然都走出了翻倍行情......只是,当潮水退去,才知道谁在裸泳。
下附部分策略源码供预览,完整源码已分享至掘金量化社区,需要的可以自行下载研究。
传送门:https://bbs.myquant.cn/thread/3140
# coding=utf-8
from __future__ import print_function, absolute_import
from gm.api import *
import os
import math
import pickle
import datetime
import numpy as np
import pandas as pd
import statsmodels.api as sm
# 策略中必须有init方法
def init(context):
context.to_buy = []
context.delisted_date = pd.DataFrame()
# 每日定时任务
schedule(schedule_func=algo, date_rule='1d', time_rule='14:50:00')
def algo(context):
# 当前时间str
today = context.now.strftime("%Y-%m-%d %H:%M:%S")
# 下一个交易日
next_date = get_next_trading_date(exchange='SZSE', date=today)
# 上一个交易日
last_date = get_previous_trading_date(exchange='SHSE', date=context.now)
# 每月最后一个交易日,换股
if today[5:7]!=next_date[5:7]:
all_stocks,all_stocks_str,context.delisted_date = get_normal_stocks(context.now)
# 注:get_fundamentals_n中end_date对标的是财报季度最后一天,而非财报发布日期,所以获取的数据会有未来数据,要先剔除
# EBIT
data1 = get_fundamentals_n(table='deriv_finance_indicator', symbols=all_stocks, end_date=last_date, fields='EBIT', count=3, df=True).set_index('symbol')
data1 = data1[data1['pub_date']<context.now].sort_values(['symbol','end_date'])
data1 = data1.groupby(['symbol']).filter(lambda x: len(x)>=2)
ebit_t = data1.groupby(['symbol'])['EBIT'].apply(lambda df:df.iloc[-1]).sort_values()# EBIT t期
ebit_t1 = data1.groupby(['symbol'])['EBIT'].apply(lambda df:df.iloc[-2]).sort_values()# EBIT t-1期
# 总资产、总负债、流动资产、流动负债