如果将Zeta模型运用于选股:2022最好的选择是离场观望
Zeta模型是Z-Score模型的进阶版,由Altman等人提出,用于判断一家企业破产的可能性。今天我们来探索一下用这种方法进行选股的可行性,由于只是探索,有些细节问题只会简单地点出来,不会详细处理。但即使简单的尝试,它所给出的启示居然是2021年年底以来,股票投资的最好选择是离场观望。
Zeta模型包括资产报酬率、收入稳定性、债务偿还、积累盈利、流动比率、资本化率、规模7个变量,我们分别用总资产报酬率roa、近十年营业收入同比增长率or_yoy的标准差、已获利息倍数ebit_to_interest、盈余公积金surplus_rese、流动比率current_ratio、资产总计total_assets/股东权益total_hldr_eqy_inc_min_int、资产总计total_assets的对数来表征。这些数据分别可以从tushare的balancesheet接口和fina_indicator接口获取。
首先,调用tushare接口:
import tushare as ts
pro = ts.pro_api('your_token')
其次,获取所有股票代码:
ts_codes = pro.stock_basic()
ts_codes
需要注意的是这里获取的是所有上市公司的代码,用它们进行回测可能会出现幸存者偏差。
这里,我们只需要股票代码。
ts_codes = ts_codes['ts_code']
再次,依次从每个股票的年度资产负债表获取盈余公积金、资金总计和股东权益。实际操作中可以考虑用季度数据,但要小心有些行业在在季节性。
ts_code = ts_codes[0]
ts_code
data = pro.balancesheet(ts_code=ts_code, end_type=4, fields='ts_code, end_date, surplus_rese, total_assets, total_hldr_eqy_inc_min_int')
data.dropna(how='any', inplace=True)
data
import pandas as pd
import time
all_data = pd.DataFrame(columns=['ts_code', 'surplus_rese', 'total_assets', 'total_hldr_eqy_inc_min_int'])
for ts_code in ts_codes:
print(ts_code)
data = pro.balancesheet(ts_code=ts_code, end_type=4, fields='ts_code, surplus_rese, total_assets, total_hldr_eqy_inc_min_int')
all_data = all_data.append(data)
time.sleep(3)
all_data
第三,依次从每个股票的年度财务指标数据中获取总资产报酬率roa、近十年营业收入同比增长率or_yoy的标准差、已获利息倍数ebit_to_interest、流动比率current_ratio。
all_data.index = list(range(len(all_data)))
ts_codes = list(set(all_data['ts_code'].to_list()))
for ts_code in ts_codes:
print(ts_code)
data = pro.fina_indicator(ts_code=ts_code, fields='end_date, roa, or_yoy, ebit_to_interest, current_ratio')
for i in data.index:
index = all_data[(all_data['ts_code']==ts_code) & (all_data['end_date']==data['end_date'][i])].index
all_data.loc[index, 'roa'] = data['roa'][i]
all_data.loc[index, 'or_yoy'] = data['or_yoy'][i]
all_data.loc[index, 'ebit_to_interest'] = data['ebit_to_interest'][i]
all_data.loc[index, 'current_ratio'] = data['current_ratio'][i]
time.sleep(3)
all_data
第四,计算十年的营业收入增长率的波动率,删除不到十年的数据和中间有缺失的数据。
import numpy as np
all_data.index = list(range(len(all_data)))
ts_codes = list(set(all_data['ts_code'].to_list()))
all_data1 = all_data[all_data['ts_code']=='']
for ts_code in ts_codes: print(ts_code)
data = all_data[all_data['ts_code']==ts_code]
if len(data)>9:
data.sort_values(by='end_date', inplace=True)
data['time_spread'] = np.array(data['end_date'].astype(int).to_list()) - np.array([int(data['end_date'].to_list()[0])-10000, ] + data['end_date'].astype(int).to_list()[:-1])
data.drop(index=(data[data['time_spread']==0].index), inplace=True)
data.index = list(range(len(data)))
if len(data[data['time_spread']>10000].index):
drop_index = max(data[data['time_spread']>10000].index)
data = data.loc[drop_index:]
data.index = list(range(len(data)))
for i in data.index[9:]:
data.loc[i, 'volatility'] = np.std(data['or_yoy'][i-9:i+1])
all_data1 = all_data1.append(data[9:])
all_data1
第五,计算资本化率
all_data1['cap_rate'] = all_data1['total_assets'] / all_data1['total_hldr_eqy_inc_min_int']
第六,数据整理和探索性分析,使用《几行代码实现可视的数据集探索性分析》中分享的pandas_profiling。
可以看到数据集没有重复行,但有3%的空缺值,并且还有20条警告。
从警告可以看到,主要是某些变量相关性比较高,比如roa与ebit_to_interest,并且后者的缺失比较严重,所以我们对指标进行精简,去掉ebit_to_interest、surplus_rese和cap_rate。
第七,确定因变量,在一些研究中,往往把是否ST作为因变量,即使在一些股票投资的研究中。这明显是一个不合理的设计,我们这里改成年下跌幅度20%的为坏样本,当然,这也大概率不是一个合理的指标。我们用2021年之前的数据作为训练样本,2021年底以来的半年作为检测。
import data_operate as do
all_data3 = all_data2[all_data2['end_date']!='20211231']
all_data3.index = list(range(len(all_data3)))
for i in all_data3.index:
end_price = do.curfsql("select close from daily where ts_code='%s' and trade_date<=%d order by trade_date desc limit 0, 1" % (all_data3['ts_code'][i], int(all_data3['end_date'][i])+10000))
start_price = do.curfsql("select close from daily where ts_code='%s' and trade_date<=%d order by trade_date desc limit 0, 1" % (all_data3['ts_code'][i], int(all_data3['end_date'][i])))
if len(start_price):
all_data3.loc[i, 'return'] = 1 if end_price[0][0] / start_price[0][0] > 0.8 else 0
all_data3
第八,逻辑回归
all_data3.dropna(inplace=True)
import statsmodels.api as sm
train_cols = all_data3.columns[2:-1]
logit = sm.Logit(all_data3['return'].astype(bool), all_data3[train_cols])
result = logit.fit()
result.summary()
可以看到波动率不是显著的,因此去掉这个无效变量。
logit = sm.Logit(all_data3['return'].astype(bool), all_data3[['roa', 'current_ratio', 'total_assets']])
result = logit.fit()
result.summary()
第九,预测
all_data4 = all_data2[all_data2['end_date']=='20211231']
all_data4['result'] = result.predict(all_data4[['roa', 'current_ratio', 'total_assets']])
all_data4
先来统计下2021年底以来的回报率
all_data4.index = list(range(len(all_data4)))
for i in all_data4.index:
end_price = do.curfsql("select close from daily where ts_code='%s' and trade_date<=%d order by trade_date desc limit 0, 1" % (all_data4['ts_code'][i], int(all_data4['end_date'][i])+10000))
start_price = do.curfsql("select close from daily where ts_code='%s' and trade_date<=%d order by trade_date desc limit 0, 1" % (all_data4['ts_code'][i], int(all_data4['end_date'][i])))
if len(start_price):
all_data4.loc[i, 'return'] = end_price[0][0] / start_price[0][0] - 1
all_data4
all_data4.sort_values(by='result', inplace=True)
all_data4.dropna(inplace=True)
np.average(all_data4['return'][:342]), np.average(all_data4['return'][-342:])
可以看到,2021年底以来,评分最高的10%亏损6%,而评分最低的10%亏损16%。
而如果以0.5为分界线,2021年底以来的回归结果,90%以上的股票将下跌20%以上,空仓离场是最好的选择。
当然,这不能算一个严谨的研究,即使用于参考都还远远不够。
- 点赞
- 收藏
- 关注作者
评论(0)