之前写的一些策略主要是基于时间序列下的个股交易方式(网格交易策略、动量策略等),这些策略不属于目前的主流策略,在现今只是起收益增厚的辅助作用。今天讲一个量化机构都在用的多因子选股模型,正巧见过一个券商研报用CNN来做,自己尝试做一下,熟悉一下架构。
模型涉及到多期回测,有点点复杂,可以看看下图,我尽量解释清楚各个模块。
话不多说上代码
导入必要库:
import akshare as ak
import pandas as pd
import numpy as np
from pylab import plt, mpl
from datetime import datetime, timedelta
from sklearn.model_selection import train_test_split
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense
from sklearn import preprocessing
查询投资范围的股票代码以及基本信息
# 查询股票代码
stock_list = ak.index_stock_cons_weight_csindex(symbol="000300")
noa = 300
# 查询行业 + 重构表格
data = []
for i in range(noa):
所属行业 = ak.stock_individual_info_em(symbol=stock_list['成分券代码'][i])['value'][2]
data.append([stock_list['成分券代码'][i], stock_list['权重'][i], 所属行业])
data = pd.DataFrame(data, columns= ['代码','权重','行业'])
第一个模块,上图所示黄色部分,定义函数计算单期因子以及对应涨跌幅。
def data_collection(T0):
variables = []
for i in range(noa):
time_f=(datetime.strptime(T0,'%Y%m%d')+timedelta(days =10)).strftime('%Y%m%d')
time_30 = (datetime.strptime(T0,'%Y%m%d')-timedelta(days =30)).strftime('%Y%m%d')
s_code = stock_list['成分券代码'][i]
data = ak.stock_zh_a_hist(symbol=s_code, period="daily", start_date=time_30, end_date=T0, adjust="qfq")
MA_5 = data['收盘'][-6:-1].mean()
MA_10 = data['收盘'][-11:-1].mean()
换手率_10 = data['换手率'][-11:-1].mean()
换手率_1 = data['换手率'][-2:-1].mean()
涨跌幅_10 = data['涨跌幅'][-11:-1].mean()
涨跌幅_1 = data['涨跌幅'][-2:-1].mean()
振幅_10 = data['振幅'][-11:-1].mean()
振幅_1 = data['振幅'][-2:-1].mean()
涨跌幅_f = ak.stock_zh_a_hist(symbol=s_code, period="daily",
start_date=T0,
end_date=time_f,
adjust="qfq")['涨跌幅'].mean()
variables.append([MA_5, MA_10, 换手率_10, 换手率_1, 涨跌幅_10, 涨跌幅_1,振幅_10, 振幅_1, 涨跌幅_f])
return variables
第二个模块,上图粉色部分,在当前阶段前移5次10天读取历史数据。
def train_data(T0):
current_time = (datetime.strptime(T0,'%Y%m%d')-timedelta(days =10)).strftime('%Y%m%d')
variable_set = pd.DataFrame()
for i in range(5):
variables = data_collection(current_time)
variables = pd.DataFrame(variables)
current_time = (datetime.strptime(current_time,'%Y%m%d')-timedelta(days =10)).strftime('%Y%m%d')
variable_set =pd.concat([pd.DataFrame(variable_set), variables])
variable_set = pd.DataFrame(preprocessing.MinMaxScaler().fit_transform(variable_set))
return variable_set
第三块,上图绿色部分,适用数据训练出模型。
def model_training(variable_set):
# 准备数据
X = variable_set.drop(8, axis=1).values
y = variable_set[8].values
# 构建CNN模型
model = Sequential()
model.add(Conv1D(filters=32, kernel_size=3, activation='relu', input_shape=(X.shape[1], 1)))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(64, activation='relu'))
model.add(Dense(1, activation='linear'))
# 编译模型
model.compile(loss='mean_squared_error', optimizer='adam')
# 模型训练
model.fit(X, y, epochs=10, batch_size=32, validation_data=(X, y))
# 模型预测
return model
第四个模块,上图蓝色部分,整体代码跑10次,得到10期的预测结果。
# 从23年开始,做10期
T0 = '20230101'
for i in range(10):
# 1.获取训练数据
variable_set = train_data(T0)
# 2.训练
model = model_training(variable_set)
# 3.获取回测数据
T_f10 = (datetime.strptime(T0,'%Y%m%d')+timedelta(days=10)).strftime('%Y%m%d')
test_set = pd.DataFrame(data_collection(T_f10))
# 4.获取预测
X = test_set.drop(8, axis=1).values
data[T0] = model.predict(X)
data[T0] = data[T0].rank(ascending = True)
# 5.重置时间线
T0 = T_f10
先读取一下期间行情价格,用于后续计算
s_close = pd.DataFrame()
s_close.index = ak.stock_zh_a_hist(symbol='000001', period="daily", start_date="20230101", end_date='20230401', adjust="qfq")['日期']
for i in range(noa):
历史行情 = ak.stock_zh_a_hist(symbol=stock_list['成分券代码'][i], period="daily", start_date="20230101", end_date='20231230', adjust="qfq")[['日期','收盘']]
历史行情.columns = ['date',str(stock_list['成分券代码'][i])]
历史行情.index = 历史行情['date']
s_close = s_close.join(历史行情[str(stock_list['成分券代码'][i])],how ='left')
# 计算日回报率
s_close.index = pd.to_datetime(s_close.index)
s_rets = np.log(s_close / s_close.shift(1))
计算部分,以及绘图部分看看结果
df = pd.DataFrame()
df = data.drop(['代码', '权重','行业'],axis=1)
df.columns = pd.to_datetime(df.columns)
df.index = data['代码']
# 期初资金1亿,不考虑手续费,不考虑行业偏离问题,就买分数大于100的,一个买500万的
portfolio = pd.DataFrame()
balance = []
init_c = 100000000
stock_num = 100
for i in range(len(df.columns)-1):
buy_stock_price = s_close[(s_close.index>=df.columns[i])].iloc[0]
buy_list = df[df.columns[i]].map(lambda x:1 if x>(300-stock_num) else None)
time_ = s_close[(s_close.index>=df.columns[i])].index[0].strftime('%Y%m%d')
portfolio[time_]=(init_c/stock_num)//(buy_list* buy_stock_price)
cash = init_c-(portfolio[time_]*buy_stock_price).sum()
balance.append(cash)
if i<(len(df.columns)-2):
sell_stock_price = s_close[(s_close.index>=df.columns[i+1])].iloc[0]
init_c = (portfolio[time_] * sell_stock_price).sum() + cash
else:
break
portfolio = portfolio.transpose().fillna(0)
portfolio['cash'] = balance
portfolio.index = pd.to_datetime(portfolio.index)
values = pd.DataFrame()
s_close['cash'] = 1
values.index = s_close.index
values = (values.join(portfolio,how = 'left').ffill() *s_close).sum(axis = 1)
plt.plot((s_rets*np.array(data['权重']/100)).sum(axis = 1).cumsum(),'r')
plt.plot(np.log(values / values.shift(1)).cumsum(),'g')
plt.show()
因子是随便写的,算力和数据库有限,没有进行降维/重要性检验等过程,只做了归一化。
未考虑实际操作中的卖出手续费、卖出时间、账户金额限制问题,移仓全都默认发生在T0时间收盘竞价时,且全部成功买入/卖出。