# 基于机器学习的高送转预测模型
![u=2612060739,592978439&fm=26&gp=0.jpg][1]
“高送转”,是指公司大比例送红股或以资本公积金转增股本,一般将每10股送或转5股及以上定义为高送转。上市公司选择“高送转”方案,通常表明公司对业绩保持高增长充满信心,公司正处于快速成长期,有助于保持良好的市场形象;另一方面一些股价高、流动性较差的公司,也可以通过“高送转”降低股价、增强公司股票流动性。因此,在过去几年的三季报披露到年报披露的时间段内,A股市场经常会掀起一波“高送转”的炒作行情。因而若能在高送转披露之前提前预测到进行高送转的上市公司股票,或能够获得可观的超额收益。
# 样本选择
训练集包括2012年至今的上市公司年度权益分配方案(判断是否高送转)、每年三季报的相关财务指标和10月份的交易数据,包含17019个训练样本。随后我们通过数据预处理(整合及标准化)得到如下训练集:
| 变量名 | 含义 |
| :--: | :--: |
| X1 | 每股资本公积与每股留存收益之和(元) |
| X2 | 归母净利润同比增长率(%) |
| X3 | 平均股价(元) |
| X4 | 总股本(亿股) |
| X5 | 平均上市时间(天) |
| X7 | 是否次新股(0或1) |
| y | 是否高送转(0或1) |
其中包含3123个正样本(进行了高送转)及13986个负样本(未进行高送转),出于正负样本平衡的原则,我们在负样本中随机抽取了3123个样本放入训练集中,再各选取10%作为验证集及测试集,得到最终数据集,包含4996个训练样本。
# 模型训练
因聚宽对机器学习的支持程度不高,因此我们选用sklearn作为学习框架,又根据训练集的大小,特征的数量,我们选择随机森林分类器作为此策略的学习算法,并采用随机搜索方法进行超参数的调优,代码如下。
```
clf = RandomForestClassifier(n_estimators=20)
param_dist = {"max_depth": [1, None],
"max_features": sp_randint(1, 7),
"min_samples_split": sp_randint(2, 11),
"min_samples_leaf": sp_randint(1, 11),
"bootstrap": [True, False],
"criterion": ['gini', 'entropy']
}
n_iter_search = 20
random_search = RandomizedSearchCV(clf, param_distributions=param_dist, n_iter=n_iter_search)
start = time()
random_search.fit(X_train, y_train)
print("RandomizedSearchCV took %.3f seconds for %d candidates"
"parameter settings." % ((time() - start), n_iter_search))
```
# 结果
最优模型在测试集上获得了71.92%的准确度及72.34%的F1-score值,可见模型并未过拟或欠拟合,且表现尚可。
# 策略制定
我们在每年的10月底的最后一个交易日开盘前进行选股,通过抽取以上特征进行高送转股票的预测。为了顺应监管政策的要求,我们在这个基础上,加入了一些更为严格的筛选条件,具体是:
- 公司总股本小于20亿股
- 每股资本公积及留存收益大于等于1.5元;
- 平均股价大于等于30元;
- 三季报的归母净利润同比增长率大于等于35%;
- 上市时间小于等于3年;
接着我们根据模型的预测概率,对预测高送转股票进行由高到低的排序,若超过50个股票的,选取前50名的股票进行等权购买,持有45个交易日后卖出。
# 回测
我们将策略对2010年10月30日至今的市场上进行了回测,选取全A作为基准,得到了总计21.58%的策略收益及10.98%的基准收益,12.35%的最大回撤。(因国君量化平台并未支持聚宽的所有API,故本策略并未在此平台上运行)
![Snipaste_2018-08-23_10-21-13.png][2]
# 改进
通过回测我们发现即使模型的表现尚可,可是策略的表现仍然不佳,可能有以下几个原因:
- 我们并未设定止盈止损条件
- 高送转行情时效较短,并不能长线运作
- 并未对仓位进行配置
# 回测代码
```
# 导入函数库
import pickle
import pandas as pd
from jqfactor import get_factor_values
import datetime as dt
import numpy as np
from sklearn.preprocessing import OneHotEncoder
import jqdata
enc = OneHotEncoder(categorical_features=[5],n_values = 2)
model = read_file('final_model.sav')
rf = pickle.loads(model)
# 初始化函数,设定要操作的股票、基准等等
def initialize(context):
# set the security pool
g.stocks = get_index_stocks('000002.XSHG')
# 设定全A作为基准
set_benchmark('000002.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# save all trade dates
g.all_tradeDates = jqdata.get_all_trade_days().tolist()
run_daily(sell, time='open')
run_monthly(get_buylist, -1, time='before_open')
run_monthly(buy, -1, time='open')
# 获取高送转预测因子
def get_indicators(stocks,endDate):
# 获取每股资本公积以及每股留存收益之和
data = get_factor_values(stocks, factors=['capital_reserve_fund_per_share','retained_earnings_per_share'],
start_date=endDate, end_date=endDate)
data = pd.merge(data['capital_reserve_fund_per_share'].T,data['retained_earnings_per_share'].T,
left_index=True,right_index=True)
if endDate.year >= 2017:
data = data[(data.iloc[:,0] >= 1.5) & (data.iloc[:,1] >= 1.5)]
data['X1'] = data.iloc[:,0]+data.iloc[:,1]
# 获取归母净利润同比增长率
q = query(
indicator.code,indicator.inc_net_profit_to_shareholders_year_on_year)
df = get_fundamentals(q, endDate)
data = pd.merge(data,df,left_index=True,right_on='code')
data = data.rename(columns={'inc_net_profit_to_shareholders_year_on_year':'X2'})
data = data[['X1','X2','code']]
# 获取当月(10月)平均股价
volume = history(1, unit='30d', field='volume', security_list=stocks, df=True, skip_paused=False, fq='pre')
money = history(1, unit='30d', field='money', security_list=stocks, df=True, skip_paused=False, fq='pre')
mean_price = (money.T / volume.T)
mean_price.columns = ['X3']
data = pd.merge(data,mean_price,left_on='code',right_index=True)
# 获取总股本
q = query(
valuation.capitalization,
valuation.code)
ret = get_fundamentals(q, statDate='{}q3'.format(endDate.year)).rename(columns={'capitalization':'X4'})
data = pd.merge(data,ret,on='code')
# 获取平均上市时间
list_dates = get_all_securities(types=['stock'], date=endDate)[['start_date']]
list_dates['start_date'] = pd.to_datetime(list_dates['start_date'])
list_dates['X5'] = [i.days for i in (endDate - list_dates['start_date'])]
data = pd.merge(data,list_dates[['X5']],left_on='code',right_index=True)
# 获取是否次新股
data['X7'] = data['X5'].apply(if_cixin)
if endDate.year >= 2017:
data = data[(data['X4'] <= 20*1e8) & (data['X1'] >= 3) & (data['X3'] >= 30) & (data['X5'] <= 3*365) & (data['X2'] >= 35)]
data = data.dropna().reset_index(drop=True)
try:
X_ = enc.fit_transform(np.array(data[['X1','X2','X3','X4','X5','X7']]))
except:
X_ = np.zeros([0,0])
return data.code, X_
def if_cixin(days):
if days <= 365:
return 1
else:
return 0
def get_buylist(context):
today = context.current_dt
if today.month == 10:
stk_list, X_ = get_indicators(g.stocks,today)
if X_.shape[0] > 0:
sorted_stk_index = np.argsort(rf.predict_proba(X_)[np.argwhere(rf.predict(X_)==1).reshape(-1)][:,1]).tolist()[::-1]
if len(sorted_stk_index) > 50:
sorted_stk_index = sorted_stk_index[:50]
g.buylist = stk_list[sorted_stk_index].tolist()
else:
g.buylist = 0
def sell(context):
current_positions = context.portfolio.positions
today = context.current_dt.date()
# if a stock is being held for more than ten tradeDates, sell it
for stk in current_positions:
tranDate = current_positions[stk].transact_time.date()
avg_cost = current_positions[stk].avg_cost
price = current_positions[stk].price
#if (g.all_tradeDates.index(today) - g.all_tradeDates.index(tranDate) + 1 >= 40) | ((price-avg_cost)/avg_cost >= 0.1):
if (g.all_tradeDates.index(today) - g.all_tradeDates.index(tranDate) + 1 >= 45):
# 卖出所有股票,使这只股票的最终持有量为0
order_target(stk, 0)
# 记录这次卖出
log.info("Selling %s" % (stk))
def buy(context):
today = context.current_dt
if today.month == 10:
buylist = g.buylist
if buylist:
for stk in buylist:
order_value(stk, context.portfolio.cash/len(buylist))
# 记录这次买入
log.info("Buying %s" % (stk))
```
[1]: https://quant.gtja.com/img/e8b6ce953ff54181ac9d130b26669995
[2]: https://quant.gtja.com/img/254794fb40b49facc029e4b2b598b126