backtrader学习系列-基础知识:实战1-自动运行找相关性高的股票

在时间序列分析中,描述了如何通过协整的方式寻找可以配对交易的组合,本节在此基础上实现自动脚本运行,找到相关性高的股票。

import os
import requests,re
import akshare as ak
import statsmodels.api as sm
import pandas as pd
import statsmodels.tsa.stattools as ts
from queue import PriorityQueue
import seaborn as sns
import matplotlib.dates as mdates
from matplotlib import pyplot as plt
import numpy as np

1.如何获取股票集合

# 1.1通过三方网站,获取a股集合,股票很多,会有点慢
response = requests.get("http://www.shdjt.com/js/lib/astock.js")
astock = re.findall(r"~([a-z0-9]*)`", response.text)
print(len(astock))
print(astock[0:5])
7817
['000001', '000002', '000003', '000004', '000005']
# 1.2 通过akshare 获取成分股
## https://akshare.akfamily.xyz/data/index/index.html#id16
## 获取新浪沪深300 
index_stock_cons_df = ak.index_stock_cons(symbol="000300")
print("==========》沪深300")
print(index_stock_cons_df[0:3])
## 获取中证指数沪深300
print("==========》中证指数沪深300")
index_stock_cons_csindex_df = ak.index_stock_cons_csindex(symbol="000300")
#print(index_stock_cons_csindex_df[['成分券代码','成分券名称']])
## 获取成分权重
index_stock_cons_weight_csindex_df = ak.index_stock_cons_weight_csindex(symbol="000300")
print(index_stock_cons_weight_csindex_df[['成分券代码','成分券名称','权重']][0:3])
==========》沪深300
     品种代码  品种名称        纳入日期
0  601916  浙商银行  2023-12-11
1  301269  华大九天  2023-12-11
2  688256   寒武纪  2023-12-11
==========》中证指数沪深300
    成分券代码 成分券名称     权重
0  000001  平安银行  0.524
1  000002   万科A  0.410
2  000063  中兴通讯  0.486

2. 通过股票集合获取行情

## 2.1 以沪深300股票为例
##### 使用akshare 获取股票历史行情,adjust str 默认返回不复权的数据; qfq: 返回前复权后的数据; hfq: 返回后复权后的数据
stock_zh_a_hist_df = ak.stock_zh_a_hist(symbol="000001", period="daily", start_date="20230301", end_date='20230907', adjust="")
print(stock_zh_a_hist_df[:3])
           日期     开盘     收盘     最高     最低      成交量           成交额    振幅   涨跌幅  \
0  2023-03-01  13.80  14.17  14.19  13.74  1223452  1.719711e+09  3.27  2.83   
1  2023-03-02  14.13  14.24  14.44  14.06  1015877  1.447566e+09  2.68  0.49   
2  2023-03-03  14.35  14.29  14.37  14.14   690954  9.855521e+08  1.62  0.35   

    涨跌额   换手率  
0  0.39  0.63  
1  0.07  0.52  
2  0.05  0.36  
## 2.2 使这里使用停牌的股票测试一下,如果存在某个单一股票停牌,则删除对应的数据不做比较, 603023 20230711有停牌
astock = ak.stock_zh_a_hist(symbol='603023', period="daily", start_date="20220101", end_date='20231231', adjust="hfq")
astock.index = astock['日期']
bstock = ak.stock_zh_a_hist(symbol='601916', period="daily", start_date="20220101", end_date='20231231', adjust="hfq")
bstock.index = bstock['日期']
combination = pd.DataFrame()
combination['astock'] = astock['收盘']
combination['abtock'] = bstock['收盘']
print("603023的数据量:",len(astock))
print("601916的数据量:",len(bstock))
combination.dropna()
print("删除空值之后的数据量:",len(combination))
603023的数据量: 474
601916的数据量: 478
删除空值之后的数据量: 474
## 2.3 foreach stock to get quote, and set to quote dict, time will be a little long
quote_dict = {}
for stock in index_stock_cons_df['品种代码']:
    stock_zh_a_hist_df = ak.stock_zh_a_hist(symbol=stock, period="daily", start_date="20220101", end_date='20231231', adjust="hfq")
    stock_zh_a_hist_df.index = stock_zh_a_hist_df['日期']
    quote_dict[stock] = stock_zh_a_hist_df
## 2.4 look for relative stock pair, there are 300 stocks, each combination include 2 stock , 300*(300-1)/2 = 44850
### 2.4.1 构建所有的组合
#print(len(quote_dict))
all_combinations=[]
hs300 = index_stock_cons_df['品种代码']
print("沪深300股票的数据量",len(hs300))
for i in range(len(hs300) - 1):
    for j in range(i + 1,len(hs300)) :
        all_combinations.append(hs300[i]+','+hs300[j])
        #print(hs300[i],',',hs300[j])
print("沪深300每2个股票构成一个集合的数据量",len(all_combinations))
沪深300股票的数据量 300
沪深300每2个股票构成一个集合的数据量 44850

3. 计算CADF

# calculate cadf, get cadf val
# cadf 参数的含义见时间序列分析部分
def calculate_cadf(stock_combination):
    # 分离股票代码
    stock_vec = stock_combination.split(',')
    if len(stock_vec) != 2:
        print('unknown stock_combination', stock_combination)
        return
    stock1 = stock_vec[0]
    stock2 = stock_vec[1]
    # 构建新的组合
    stock_combination = pd.DataFrame()
    stock_combination[stock1] = quote_dict[stock1]['收盘']
    stock_combination[stock2] = quote_dict[stock2]['收盘']
    # 删除空值
    stock_combination = stock_combination.dropna()
    #print(stock_combination)
    ## 进行OLS 训练,寻找参数
    model = sm.OLS(stock_combination[stock1], stock_combination[stock2])
    res= model.fit()
#     print(res.params[0])
#     print(res.summary())
    stock_combination["res"] = stock_combination[stock1] - res.params[0] *stock_combination[stock2]
#     #print(df["res"])
    # 获取参数结果
    cadf = ts.adfuller(stock_combination["res"])
    if cadf[1] >= 0.001 or cadf[0] > cadf[4]['1%']:
        return False, 0, cadf[1]
#     print(cadf)
#     print(cadf[0])
#     print(cadf[1])
#     print(cadf[4]['1%'])
    return True, cadf[0],cadf[1]

calculate_cadf('601916,301269')
(False, 0, 0.01565804329401019)
# 获取计算结果,放在优先队列中
cadf_pq = PriorityQueue()
## 数据接近50000个组合,只运行10000个组合,看看结果
for combination in all_combinations[0:10000]:
#     print(combination)
    cadfPassed, tStat , pValue= calculate_cadf(combination)
    if cadfPassed:
        #print(tStat, combination)
        cadf_pq.put((tStat,pValue, combination))
/Users/neil/anaconda3/lib/python3.10/site-packages/statsmodels/regression/linear_model.py:940: RuntimeWarning: divide by zero encountered in log
  llf = -nobs2*np.log(2*np.pi) - nobs2*np.log(ssr / nobs) - nobs2
# 获取到前10组股票
for i in range(10):
    print(cadf_pq.get())
(-6.10279795196737, 9.743996556382118e-08, '600674,300433')
(-5.923899040209228, 2.469527406422219e-07, '601699,300433')
(-5.617043137434816, 1.1679665215196353e-06, '600023,300433')
(-5.314153220357734, 5.117872580870923e-06, '600803,600011')
(-5.139567355684046, 1.1665725327115197e-05, '000983,002475')
(-5.124443400485768, 1.2516222555184482e-05, '600039,300433')
(-5.086955736567507, 1.4890852108722185e-05, '600372,000661')
(-5.0737055834075315, 1.5830009880242826e-05, '600515,002603')
(-5.0499788575149696, 1.7656566807224052e-05, '600803,300413')
(-5.010266453862187, 2.11779429744831e-05, '002603,002352')
def plot_residuals(stock_combination):
    stock_vec = stock_combination.split(',')
    if len(stock_vec) != 2:
        print('unknown stock_combination', stock_combination)
        return
    stock1 = stock_vec[0]
    stock2 = stock_vec[1]
    stock_combination = pd.DataFrame()
    stock_combination[stock1] = quote_dict[stock1]['收盘']
    stock_combination[stock2] = quote_dict[stock2]['收盘']
    # 删除空值
    stock_combination = stock_combination.dropna()
    #print(stock_combination)
    ## 进行OLS 训练,寻找参数
    model = sm.OLS(stock_combination[stock1], stock_combination[stock2])
    res= model.fit()
    stock_combination["res"] = stock_combination[stock1] - res.params[0] *stock_combination[stock2]
    mean=np.mean(stock_combination["res"])
    std=np.std(stock_combination["res"])
    up=mean+1.5*std
    down=mean-1.5*std
    mean_line=pd.Series(mean,index=stock_combination.index)
    up_line=pd.Series(up,index=stock_combination.index)
    down_line=pd.Series(down,index=stock_combination.index)
    stock_combination["res"]
    months = mdates.MonthLocator() # every month 
    fig, ax = plt.subplots()
    ax.plot(stock_combination.index, stock_combination["res"], label="Residuals")
    ax.plot(stock_combination.index, down_line, label="Down")
    ax.plot(stock_combination.index, up_line, label="Up")
    ax.plot(stock_combination.index, mean_line, label="Mean")
    gap = 10
    plt.ylabel('Price')
    plt.xlabel('Date')
    plt.xticks(rotation=90, size=8)
    plt.title('Residual Plot')
    plt.legend()
    plt.plot(stock_combination["res"])
    plt.show()

plot_residuals('600803,600011')
v2-77290e09b5a9b115fe24e1d330885dd2_1440w.png

可以看到在1.5 个标准差的时候,是可以来回做套利的,价格在Up 和Down 之间振动,最后总是回到均值附近。

上面的过程存在哪些问题:

Question1: 有些股票,比如浙商银行是2023-12-11 才纳入的000300, 但用于统计相关性的数据起始时间是20220101,属于使用了未来函数,因为这里只是计算股票之间的相关性,和指数成分之间关系不大,但如果基于股票池的回测中使用了这组数据呢?
Question2: 股票价格是不一样的,两个股票分别应该交易的单位是多少才能构成配对?

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容