背景介绍
投资者通过实战经验,总结出了各种各样的股票价格走势形态以辅助投资决策。比如常见的头肩形、倒头肩形、三重顶、三重底、M头、W底等。

然而投资者的经验是有限的,特别是新进股市的投资者。进一步地,常用的股票形态已经为广大投资者熟知,一定程度上降低了这些形态的有效性。同时,一些新的形态或许隐藏在其中而没有被发现。本策略通过提取价格形态特征,采用聚类分析的方法,对其形态特征数据进行自动聚类分析,并根据聚类的结果计算每类股票未来持有期为一个月的平均收益率。然后将类平均收益率排名前5的股票记为+1类,其余股票记为-1类,作为训练样本,采用KNN模型进行训练。最后,利用训练好的KNN模型对新的股票价格形态特征数据进行预测。如果预测结果为+1,表示该只股票在未来一个月内会得到较好的投资回报率,即设计一个量化投资策略:以未来一个月为持有周期,以期初收盘价买入,期末收盘价卖出,计算其收益率。
特征工程
由于用于聚类的股票价格数据周期取三个月,其交易日期一般都在60个以上,如果以收盘价数据直接进行聚类,则数据维度较高。特别地如果周期取半年、一年或者更长,其维度更高,会严重影响聚类的效果,因此在进行聚类之前需要做降维处理。降维的方法则是取能代表股票价格走势的关键价格点,并基于提取的关键价格点计算股票价格走势形态特征,从而进行聚类。这里关键价格点的提取是关键,下面详细介绍关键价格点的概念、提取算法和形态特征的计算方法。
关键价格点
- 关键价格点的概念和提取方法
- 输入:原始价格序列x=(x1,x2,……,xp),提取关键点个数num
- 输出:关键价格点序列、对应下标序列
step1:对x2,……,x(p-1),按公式计算其与相邻两个价格点均值的绝对值大小,并按从大到小进行排序,取排名前num-2对应的价格点,记为L1,对应的下标序列记为S1.
step2:记x1,xp对应的价格点记为L2,对应的下标序列记为S2;
step3:记L=L1∪L2,S=S1∪S2,并按S进行从小到大进行排序,则L即为关键价格点序列,S即为对应的下标序列。
def getKeyData(price, num):
value = price.values
#计算x2,……,x(p-1)各点减去相邻两点平均值的绝对值
abs_value = abs(value[1:len(value)-1] - (value[0:len(value)-2]+value[2:len(value)])/2)
sd = pd.Series(abs_value, index=range(1, len(value)-1)).sort_values(ascending=False)
l1 = sd[:num-2]
l2 = pd.Series([value[0], value[-1]], index=[0, len(value)-1])
l = l1.append(l2)
keyData = price[l.index].sort_index()
return keyData
# 绘图测试
p = stock[63]['closePrice']
keyData = getKeyData(p, 20)
plt.plot(p)
plt.plot(keyData.index, keyData.values, '-')
plt.scatter(keyData.index, keyData.values)

特征映射
关键价格点的提取降低了维度,但是直接用价格点进行聚类还是存在较大的误差,因此我们需要对关键价格点的走势情况进行特征化表示,采用两个关键价格点之间连线的斜率来确定其涨跌情况,即特征化表示为两个关键点连线之间的夹角的tan值,其映射关系如下:
•上涨幅度大: tan值>0.5
•上涨幅度较大: tan值介于0.2~0.5之间
•上涨: tan值介于0.1~0.2之间
•平缓: tan值介于-0.1~0.1之间
•下跌: tan值介于-0.2~-0.1之间
•下跌幅度较大: tan值介于-0.5~-0.2之间
•下跌幅度大: tan值<-0.5
•分别记为: 7、6、5、4、3、2、1
def getFeature(keyData):
x1 = keyData.index[1:]
x2 = keyData.index[0:-1]
y1 = keyData.values[1:]
y2 = keyData.values[0:-1]
tan = list((y2-y1) / (x2-x1))
tan = np.array(tan)
T = np.where(tan>=0.5, 7
, np.where(tan>=0.2, 6
, np.where(tan>=0.1, 5
, np.where(tan>=-0.1, 4
, np.where(tan>=-0.2, 3
, np.where(tan>=-0.5, 2, 1))))))
return T
对聚类效果的可视化分析
def getKeyDataDF(stock, num):
'''
功能:用于生成训练聚类模型的特征矩阵。
Parameters
----------
stock : dict
{ticker:str : stockPrice:Series}.
num : int
聚类个数。
Returns
-------
keyData_df : DataFrame
列名为[ticker, f1, ..., fnum].
keyData_index : DataFrame
用于绘图.
'''
keyData_df = np.zeros((len(stock.keys()), num))
keyData_index = np.zeros((len(stock.keys()), num+1))
for i, ticker in enumerate(stock.keys()):
keyData = getKeyData(stock[ticker]['closePrice'], num)
T = getFeature(keyData)
keyData_df[i] = [ticker] + list(T)
keyData_index[i] = [ticker] + list(keyData.index)
columns1 = ['ticker'] + ['f' + str(x) for x in range(1, num)]
columns2 = ['ticker'] + ['f' + str(x) for x in range(1, num+1)]
keyData_df = pd.DataFrame(keyData_df, columns=columns1)
keyData_index = pd.DataFrame(keyData_index, columns=columns2)
return keyData_df, keyData_index
keyData_df, keyData_index = getKeyDataDF(stock, 10)
def clustering(keyData_df, keyData_index, n_clusters):
'''
功能:对股票进行聚类。
Parameters
----------
keyData_df : DataFrame
列名为[ticker, f1, ..., f_num].
keyData_index : DataFrame
用于绘图.
Returns
-------
keyData_df : DataFrame
列名为[ticker, f1, ..., f_num, clusters].
keyData_index : DataFrame
用于绘图.
'''
# MinMaxScaler
X = keyData_df.iloc[:, 1:]
scaler = MinMaxScaler().fit(X)
X = scaler.transform(X)
# 聚类
kmeans = KMeans(n_clusters=n_clusters, random_state=0).fit(X)
labels = pd.Series(kmeans.labels_, name='clusters')
keyData_df = pd.concat([keyData_df, labels], axis=1)
keyData_index = pd.concat([keyData_index, labels], axis=1)
return keyData_df, keyData_index
keyData_df, keyData_index = clustering(keyData_df, keyData_index, 10)
# 可视化
mask = keyData_df.clusters == 1
i_clusters = keyData_df.loc[mask, 'f1':'f9']
for i in range(i_clusters.shape[0]):
plt.plot(i_clusters.iloc[i, :])

从上图来看聚类效果还不错在一个簇中的股票价格走势大体相同。
量化策略构建
此量化策略运行在优矿平台上。
主代码
class StockClustering:
'''
此类通过训练集来训练一个KNN分类器,并通过此分类器给测试集打上0,1标签。
一般来说只需调用fit和transform方法,其中fit方法用于训练KNN模型,transform方法用于给测试集打标签。
若要查看中间生成结果可调用featureTransformer方法,以上提到的三个函数的参数类型一致都为dict。
'''
def __init__(self, num, n_clusters):
'''
初始化
Parameters
----------
num : int
提取关键点的个数。
n_clusters : int
聚类个数。
'''
self.num = num
self.n_clusters = n_clusters
def fit(self, stock_dict, stock_oot):
'''
功能:通过训练集stock_dict来训练模型。
Parameters
----------
stock_dict : dict
{stock:str : closePrice:Series}.
stock_oot : dict
{stock:str : closePrice:Series}.
相比stock_dict中的时间往后一个月的数据。
Returns
-------
knn : KNNmodel
训练好的KNN分类器.
'''
keyData_df, keyData_index = self.featureTransformer(stock_dict, stock_oot)
self.knn = self.trainKNNModel(keyData_df)
return self
def transform(self, stock_dict):
'''
Parameters
----------
stock_dict : dict
{stock:str : closePrice:Series}.
Returns
-------
keyData_df : DataFrame
列名为[ticker, f1, ..., fnum].
'''
keyData_df, keyData_index = self.getKeyDataDF(stock_dict)
X = keyData_df.iloc[:, 1:]
y = self.knn.predict(X)
keyData_df = pd.concat([keyData_df, pd.Series(y, name='label')], axis=1)
return keyData_df
def getKeyData(self, price):
'''
功能:计算股票序列的关键价格点
Parameters
----------
price: Series
股票价格序列。
Returns
-------
keyData: Series
关键价格点数据,索引为该数据在price中对应的索引
'''
value = price.values
abs_value = abs(value[1:len(value)-1] - (value[0:len(value)-2]+value[2:len(value)])/2)
sd = pd.Series(abs_value, index=range(1, len(value)-1)).sort_values(ascending=False)
l1 = sd[:self.num-2]
l2 = pd.Series([value[0], value[-1]], index=[0, len(value)-1])
l = l1.append(l2)
keyData = price[l.index].sort_index()
return keyData
def getFeature(self, keyData):
'''
功能:根据规则将关键价格点数据映射为取值1-7的特征
Parameters
----------
keyData: Series
关键价格点数据,索引为该数据在price中对应的索引
Returns
-------
T: Series
关键价格点数据的特征映射,索引为keyData的索引
'''
x1 = keyData.index[1:]
x2 = keyData.index[0:-1]
y1 = keyData.values[1:]
y2 = keyData.values[0:-1]
tan = list((y2-y1) / (x2-x1))
tan = np.array(tan)
T = np.where(tan>=0.5, 7
, np.where(tan>=0.2, 6
, np.where(tan>=0.1, 5
, np.where(tan>=-0.1, 4
, np.where(tan>=-0.2, 3
, np.where(tan>=-0.5, 2, 1))))))
return T
def getKeyDataDF(self, stock):
'''
功能:用于生成训练聚类模型的特征矩阵。
Parameters
----------
stock : dict
{ticker:str : stockPrice:Series}.
Returns
-------
keyData_df : DataFrame
列名为[ticker, f1, ..., fnum].
keyData_index : DataFrame
用于绘图.
'''
keyData_df = np.zeros((len(stock.keys()), self.num-1))
keyData_index = np.zeros((len(stock.keys()), self.num))
for i, ticker in enumerate(stock.keys()):
keyData = self.getKeyData(stock[ticker]['closePrice'])
T = self.getFeature(keyData)
keyData_df[i] = list(T)
keyData_index[i] = list(keyData.index)
columns1 = ['f' + str(x) for x in range(1, self.num)]
columns2 = ['f' + str(x) for x in range(1, self.num+1)]
keyData_df = pd.DataFrame(keyData_df, columns=columns1)
keyData_index = pd.DataFrame(keyData_index, columns=columns2)
keyData_df = pd.concat([pd.Series(stock.keys(), name='ticker'), keyData_df], axis=1)
keyData_index = pd.concat([pd.Series(stock.keys(), name='ticker'), keyData_index], axis=1)
return keyData_df, keyData_index
def clustering(self, keyData_df, keyData_index):
'''
功能:对股票进行聚类。
Parameters
----------
keyData_df : DataFrame
列名为[ticker, f1, ..., f_num].
keyData_index : DataFrame
用于绘图.
Returns
-------
keyData_df : DataFrame
列名为[ticker, f1, ..., f_num, clusters].
keyData_index : DataFrame
用于绘图.
'''
# MinMaxScaler
X = keyData_df.iloc[:, 1:]
scaler = MinMaxScaler().fit(X)
X = scaler.transform(X)
# 聚类
kmeans = KMeans(n_clusters=self.n_clusters, random_state=0).fit(X)
labels = pd.Series(kmeans.labels_, name='clusters')
keyData_df = pd.concat([keyData_df, labels], axis=1)
keyData_index = pd.concat([keyData_index, labels], axis=1)
return keyData_df, keyData_index
def revenueOfEachClusters(self, keyData_df, stock):
'''
功能:计算每一簇的平均收益率。
Parameters
----------
keyData_df : DataFrame
列名为[ticker, f1, ..., f_num, clusters]
stock : dict
{ticker:str : stockPrice:Series}.
此处的stock为fit时传入的stock_oot。
Returns
-------
cluseter_revenue : DataFrame
index为clusers的编号,列为revenue。
'''
# 计算每只股票的收益率
revenue = {}
for ticker in stock.keys():
closePrice = stock[ticker]['closePrice']
revenue[ticker] = (closePrice.iloc[-1] - closePrice.iloc[0]) / closePrice.iloc[0]
revenue = pd.DataFrame(revenue, index=['revenue']).T
# 并表
keyData_df = keyData_df.merge(revenue, left_on=keyData_df['ticker'], right_index=True)
# 计算每簇的平均收益
cluseter_revenue = keyData_df[['revenue', 'clusters']].groupby('clusters').mean()
return cluseter_revenue
def featureTransformer(self, stock, stock_oot):
'''
功能:训练集特征转化器,整合前面的特征构造函数,并给训练集打上标签。
Parameters
----------
stock : dict
{ticker:str : stockPrice:Series}.
Returns
-------
keyData_df : DataFrame
列名为[ticker, f1, ..., f_num, clusters, label].
keyData_index : DataFrame
用于绘图。
'''
keyData_df, keyData_index = self.getKeyDataDF(stock)
keyData_df, keyData_index = self.clustering(keyData_df, keyData_index)
cluster_revenue = self.revenueOfEachClusters(keyData_df, stock_oot)
# 选出平均收益率排名前5的簇
top5 = cluster_revenue.sort_values(by='revenue', ascending=False).index[:5].tolist()
# 在top5中的股票label=1否则为0
keyData_df['label'] = keyData_df['clusters'].apply(lambda x: x in top5).apply(int)
return keyData_df, keyData_index
def trainKNNModel(self, keyData_df):
'''
功能:训练KNN模型。
Parameters
----------
keyData_df : DataFrame
列名为[ticker, f1, ..., f_num, clusters, label].
Returns
-------
knn : KNNmodel
训练好的KNN分类器.
'''
X = keyData_df.iloc[:, 1:-2]
y = keyData_df['label']
knn = KNN().fit(X, y)
return knn
策略代码
start = '2020-01-01' # 回测起始时间
end = '2021-01-01' # 回测结束时间
universe = DynamicUniverse('HS300') # 证券池,支持股票、基金、期货、指数四种资产
benchmark = 'HS300' # 策略参考标准
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
refresh_rate = Monthly(1) # 调仓频率,表示执行handle_data的时间间隔,若freq = 'd'时间间隔的单位为交易日,若freq = 'm'时间间隔为分钟
# 配置账户信息,支持多资产多账户
accounts = {
'fantasy_account': AccountConfig(account_type='security', capital_base=10000000)
}
#
gap = 1
# 获取沪深300指数成分股的代码
ticker = DataAPI.IdxConsGet(secID=u"",ticker=u"000300",isNew=u"",intoDate=u"20200101"
,field=['consTickerSymbol'],pandas="1")['consTickerSymbol'].tolist()
def initialize(context):
pass
# 每个单位时间(如果按天回测,则每天调用一次,如果按分钟,则每分钟调用一次)调用一次
def handle_data(context):
# 当前账户
account = context.get_account('fantasy_account')
# 获取调仓当天的日期
current_date = context.current_date.strftime('%Y-%m-%d')
# 计算所需的各时间段
start_train, end_train, start_oot, end_oot, start_vali, end_vali = generateDate(current_date, gap)
# 取数
data = DataAPI.MktEqudGet(secID=u"",ticker=ticker
,beginDate=start_train,endDate=end_vali,isOpen=""
,field=['ticker', 'tradeDate', 'closePrice'],pandas="1")
# 取出各时间段所需数据
train = processData(data, start_train, end_train)
oot = processData(data, start_oot, end_oot)
vali = processData(data, start_vali, end_vali)
# 训练模型
model = StockClustering(5, 20).fit(train, oot)
result = model.transform(vali)
# 给出买入名单
buy_list = list(result.loc[result['label']==1, 'ticker'])
buy_list = [normalize_code(tick, target_type='secID') for tick in buy_list]
# 获取当前持仓
positions = account.get_positions().keys()
#print('positions: ', positions)
# 先执行卖出策略,若当前持仓不在买入名单中则卖出
for stock in positions:
if stock not in buy_list:
account.close_all_positions(stock)
#print('stock_to_sell: ', account.get_position(stock))
else:
pass
# 再执行买入策略,若股票在buy_list中且不在持仓中则买入
stock_to_buy = []
for stock in buy_list:
if stock not in positions:
stock_to_buy.append(stock)
#print('stock_to_buy: ', stock_to_buy)
# 获取当前可用资金
cash = account.cash
for stock in stock_to_buy:
account.order_pct_to(stock, 1.0/len(stock_to_buy))
# 查看下单明细
#print("查看下单明细:", account.get_orders())
策略回测结果
回测区间为2020.1.1——2020.12.31

从结果可以看出相对于基准HS300来说是有一定的alpha收益的。
总结
本文的重点放在了特征工程上,重要的是关键价格点的提取和之后的特征映射,有重要影响的超参数参为聚类个数这直接影响了后续的持仓规模,后面可通过轮廓系数进一步调优,此外提取关键价格点的时间长度也是影响结果的重要变量;另外针对策略部分调整空间更大比如仓位控制上本文采用了等权的方式但进一步可以根据概率来设计仓位大小等等。
股票价格形态聚类分析与量化策略
本文探讨了如何通过聚类分析股票价格形态,提取关键价格点并进行特征映射,降低数据维度。使用KNN模型进行训练,根据聚类结果设计量化投资策略,实现在优矿平台上的回测显示,相对于基准HS300存在alpha收益。

2278





