前言:
第一次写文章哈哈!分享我最近研究的回测A股的脚本。我是一个完全没有任何编程基础的人,写代码都得是边写边查,浏览器经常开了很多标签,还有用来翻译异常问题的百度搜索。近来文字型AI发展迅猛,所以我还会打开AI对话的窗口,方便我问它要解决方法和代码,目前我最常用的国内大模型是科大讯飞的讯飞星火,准确率比较高,但有时也会回答一些错的,anyway,能借用的工具必须全部利用起来。在这个过程中,我花了大量的时间去写代码和debug,到后面一步一步优化代码以及优化过程,慢慢加入以前没接触过的功能,比如浮点数运算、多线程回测、数据保存成二进制文件或numpy文件,用到了很多新的库,学习到好多好多新的知识,CSDN这个网站也帮我解决了很多很多过程中遇到的难题,非常感谢在这里分享心得的博主。
大致步骤:
第一步:建立获取单个股票数据的脚本
第二步:编写想要回测的技术指标并封装成函数
第三步:遍历沪深京所有股票,调用指标函数回测,获取回测数据并保存
第四步:数据的统计
前期准备:
通过安装conda安装python,编译器用的是jupyter lab,真是太好用了,超级喜欢,没接触jupyter之前,我用的是Thonny,jupyter的笔记本用来写代码真是太棒了,占用空间也不大,运行完变量数据还在,还能单独运行单元格,非常适合初学者用于学习!
第一步:建立获取单个股票数据的脚本
这里比较简单,使用python的开源库akshare爬取国内财经网站的数据,完全免费,童叟无欺,比tushare简直好太多了。
函数可以在这里面查:Welcome to AKShare’s Online Documentation! — AKShare 1.11.31 文档
import akshare as ak
import pandas as pd
import sys
data = pd.DataFrame()
#symbol = sys.argv[1]
symbol = '000004'
print(f'{symbol}回测开始')
df=ak.stock_zh_a_hist(symbol = symbol,
period = 'daily',
start_date = '20000101',
end_date = '20500101',
adjust = 'qfq'
)
#df.to_excel(f'{symbol}.xlsx', index=False)
df
日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 | 振幅 | 涨跌幅 | 涨跌额 | 换手率 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2000-01-04 | 8.47 | 8.66 | 8.67 | 8.28 | 6577 | 5.628000e+06 | 4.63 | 2.73 | 0.23 | 1.58 |
1 | 2000-01-05 | 8.67 | 8.94 | 8.97 | 8.43 | 5707 | 5.044000e+06 | 6.24 | 3.23 | 0.28 | 1.37 |
2 | 2000-01-06 | 8.92 | 9.32 | 9.39 | 8.80 | 32360 | 3.039400e+07 | 6.60 | 4.25 | 0.38 | 7.77 |
3 | 2000-01-07 | 9.17 | 9.79 | 9.79 | 9.15 | 21303 | 2.088600e+07 | 6.87 | 5.04 | 0.47 | 5.11 |
4 | 2000-01-10 | 10.20 | 10.28 | 10.28 | 9.93 | 24623 | 2.540500e+07 | 3.58 | 5.01 | 0.49 | 5.91 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
5411 | 2023-10-09 | 16.04 | 15.76 | 16.34 | 15.71 | 84680 | 1.351435e+08 | 3.93 | -1.68 | -0.27 | 6.71 |
5412 | 2023-10-10 | 15.93 | 17.34 | 17.34 | 15.84 | 155377 | 2.674170e+08 | 9.52 | 10.03 | 1.58 | 12.30 |
5413 | 2023-10-11 | 18.23 | 17.56 | 18.50 | 17.48 | 374027 | 6.687257e+08 | 5.88 | 1.27 | 0.22 | 29.62 |
5414 | 2023-10-12 | 17.00 | 17.32 | 17.76 | 17.00 | 166427 | 2.884935e+08 | 4.33 | -1.37 | -0.24 | 13.18 |
5415 | 2023-10-13 | 17.10 | 18.33 | 18.39 | 17.08 | 131655 | 2.335914e+08 | 7.56 | 5.83 | 1.01 | 10.43 |
5416 rows × 11 columns
我的公式要用到月线,所以再爬
df_monthly=ak.stock_zh_a_hist(symbol = symbol,
period = 'monthly',
start_date = '20000101',
end_date = '20500101',
adjust = 'qfq'
)
mike(df_monthly, 'm')
df_monthly['日期'] = pd.to_datetime(df_monthly['日期'])
#df_monthly.to_excel(f'{symbol}.xlsx', index=False)
df_monthly
日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 | 振幅 | 涨跌幅 | 涨跌额 | 换手率 | WEKR | zfyq | 冲不冲 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2009-10-30 | 6.42 | 6.62 | 9.73 | 5.44 | 226207 | 2.373081e+09 | 122.57 | 89.14 | 3.12 | 89.48 | NaN | True | False |
1 | 2009-11-27 | 5.90 | 8.05 | 8.40 | 5.83 | 620152 | 6.386345e+09 | 38.82 | 21.60 | 1.43 | 245.31 | 9.086667 | True | False |
2 | 2009-12-31 | 8.70 | 6.78 | 9.17 | 6.58 | 387471 | 4.580679e+09 | 32.17 | -15.78 | -1.27 | 153.27 | 9.099865 | False | False |
3 | 2010-01-29 | 6.87 | 7.32 | 8.15 | 6.45 | 341343 | 3.827560e+09 | 25.07 | 7.96 | 0.54 | 135.02 | 9.127664 | True | False |
4 | 2010-02-26 | 6.75 | 9.49 | 9.78 | 6.75 | 331039 | 4.204930e+09 | 41.39 | 29.64 | 2.17 | 104.76 | 9.147199 | True | True |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
162 | 2023-06-30 | 13.34 | 14.00 | 16.30 | 12.30 | 35653893 | 4.927038e+10 | 29.54 | 3.40 | 0.46 | 195.99 | 8.001622 | False | False |
163 | 2023-07-31 | 13.96 | 10.37 | 13.96 | 9.78 | 22634508 | 2.544471e+10 | 29.86 | -25.93 | -3.63 | 124.42 | 9.405911 | False | False |
164 | 2023-08-31 | 10.35 | 10.87 | 11.22 | 9.13 | 23266718 | 2.388117e+10 | 20.15 | 4.82 | 0.50 | 127.90 | 10.640708 | False | True |
165 | 2023-09-28 | 10.87 | 10.06 | 10.93 | 8.69 | 14796886 | 1.446398e+10 | 20.61 | -7.45 | -0.81 | 81.34 | 11.656688 | False | False |
166 | 2023-10-12 | 9.95 | 10.17 | 10.66 | 9.77 | 3029435 | 3.121413e+09 | 8.85 | 1.09 | 0.11 | 16.65 | 12.434831 | False | False |
167 rows × 14 columns
第二步:编写技术指标
这是一个金融群分享的指标,名字叫mike
先看通达信的源码:
这是盘面
这是源码
HLC:=REF(EMA((HIGH+LOW+CLOSE)/3,10),1);
HV:=EMA(HHV(HIGH,10),3);
LV:=EMA(LLV(LOW,10),3);
WEKR:EMA(HLC*2-LV,8),LINETHICK5,COLORBLUE;
IF(C>=REF(WEKR,1),WEKR,DRAWNULL),LINETHICK5,COLORMAGENTA;
IF(C<=REF(WEKR,1),WEKR,DRAWNULL),LINETHICK5,COLORBLUE;
DRAWKLINE(HIGH,OPEN,LOW,CLOSE);
涨幅要求2:=(C/REF(C,1)-1)*100>7;
冲:=O<WEKR AND C>WEKR AND 涨幅要求2 ;
STICKLINE(冲,O,C,4,0),COLORYELLOW;
DRAWTEXT(冲,L*0.955, '冲'),COLORRED;
这里面有一个很重要的函数,就是EMA,在python要写个函数给它。
下面是转译成python的样子,输入的变量就是爬来的df表格。
import pandas as pd
import numpy as np
def ema(data,n):
#df[f'ema_{n}'] = np.nan
ema_values = []
for i in range(len(data)):
if i == 0 or pd.isnull(data[i-1]): #去掉第一个格子,或上一个格子是空白的格子
#df.loc[i, f'ema_{n}'] = df.loc[i, column]
ema_values.append(data[i]) #直接加入列表
else:
#df.loc[i, f'ema_{n}'] = (2 * df.loc[i, column] + (n-1) * df.loc[i-1, f'ema_{n}'])/(n+1)
ema_values.append((2*data[i] + (n-1)*ema_values[i-1])/(n+1)) # #EMA的基础公式:EMA = 前一日EMA x (N-1)/(N+1) + 当日收盘价 x 2/(N+1)
return ema_values
def mike(df, data_type = 'd'): #主函数,在DataFrame上以增加列的方式保存过程中间的变量
HIGH = df['最高']
LOW = df['最低']
CLOSE = df['收盘']
df['data1'] = (HIGH+LOW+CLOSE)/3
df['HLC'] = ema(df['data1'].values, 10)
df['HLC'] = df['HLC'].shift(1)
df.fillna(0)
df['HHV(HIGH,10)'] = HIGH.rolling(10, min_periods=1).max() #一段时间最高价翻译过来是这样子
df['LLV(LOW,10)'] = LOW.rolling(10, min_periods=1).min() #如上
df['HV'] = ema(df['HHV(HIGH,10)'].values,3)
df['LV'] = ema(df['LLV(LOW,10)'].values,3)
df['HLC*2-LV'] = df['HLC']*2-df['LV']
df['WEKR'] = ema(df['HLC*2-LV'].values, 8)
df['zfyq'] = df['涨跌幅']-7
df['zfyq'] = df['zfyq'].apply(lambda x: True if x >= 0 else False) #+-数值转成布林值
df['WEKR>O'] = df['WEKR']-df['开盘']
df['WEKR>O'] = df['WEKR>O'].apply(lambda x: True if x >= 0 else False)
df['C>WEKR'] = df['收盘']-df['WEKR']
#df.fillna(0)
df['C>WEKR'] = df['C>WEKR'].apply(lambda x: True if x >= 0 else False)
df['On-1<WEKR'] = df['WEKR'] - df['收盘'].shift(1)
df['On-1<WEKR'] = df['On-1<WEKR'].apply(lambda x: True if x >= 0 else False)
tj1 = df.filter(items=['WEKR>O', 'On-1<WEKR']).any(axis=1)
tj2 = df['zfyq']
tj3 = df['C>WEKR']
if data_type == 'd':
df['冲不冲'] = tj1 & tj2 & tj3
elif data_type == 'm':
df['冲不冲'] = tj1 & tj3
else:sys.exit()
df.drop(['data1', 'HLC', 'HHV(HIGH,10)', 'LLV(LOW,10)', 'HV', 'LV', 'HLC*2-LV', 'WEKR>O', 'C>WEKR', 'On-1<WEKR'], axis=1, inplace=True) #最后把没用的过程变量去掉
日线计算简单,直接mike(df,'d'),第二个参数是选日线还是月线
mike(df, 'd')
df
日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 | 振幅 | 涨跌幅 | 涨跌额 | 换手率 | WEKR | zfyq | 冲不冲 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2009-10-30 | 6.42 | 6.62 | 9.73 | 5.44 | 226207 | 2.373081e+09 | 122.57 | 89.14 | 3.12 | 89.48 | NaN | True | False |
1 | 2009-11-02 | 5.90 | 5.97 | 6.56 | 5.90 | 75328 | 7.190947e+08 | 9.97 | -9.82 | -0.65 | 29.80 | 9.086667 | False | False |
2 | 2009-11-03 | 6.14 | 6.15 | 6.31 | 5.99 | 41747 | 4.004070e+08 | 5.36 | 3.02 | 0.18 | 16.51 | 8.996162 | False | False |
3 | 2009-11-04 | 6.14 | 6.42 | 6.55 | 6.13 | 33682 | 3.357801e+08 | 6.83 | 4.39 | 0.27 | 13.32 | 8.852258 | False | False |
4 | 2009-11-05 | 6.48 | 6.64 | 6.85 | 6.42 | 32405 | 3.350685e+08 | 6.70 | 3.43 | 0.22 | 12.82 | 8.697696 | False | False |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
3264 | 2023-09-28 | 10.11 | 10.06 | 10.18 | 9.99 | 575699 | 5.789843e+08 | 1.89 | 0.10 | 0.01 | 3.16 | 10.233231 | False | False |
3265 | 2023-10-09 | 9.95 | 10.19 | 10.41 | 9.77 | 925070 | 9.380054e+08 | 6.36 | 1.29 | 0.13 | 5.09 | 10.321869 | False | False |
3266 | 2023-10-10 | 10.28 | 10.35 | 10.66 | 10.18 | 919924 | 9.616347e+08 | 4.71 | 1.57 | 0.16 | 5.06 | 10.428242 | False | False |
3267 | 2023-10-11 | 10.21 | 10.38 | 10.58 | 10.18 | 702760 | 7.296557e+08 | 3.86 | 0.29 | 0.03 | 3.86 | 10.563653 | False | False |
3268 | 2023-10-12 | 10.39 | 10.17 | 10.41 | 10.10 | 481682 | 4.921174e+08 | 2.99 | -2.02 | -0.21 | 2.65 | 10.710706 | False | False |
3269 rows × 14 columns
由于当日的月线是到算到当天的收盘为止,所以当月之前的数据用爬来的月线数据, 当月的用当月的日线数据手动算出。这个是后一步,先把符合条件的日子选出来
import sys
rows = df[df['冲不冲']==True].copy()
if rows.size == 0:
print(f'获得{symbol}回测数据完成,共出现{rows.shape[0]}个信号\n',end='')
sys.exit(0)
rows.reset_index(inplace=True)
rows
index | 日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 | 振幅 | 涨跌幅 | 涨跌额 | 换手率 | WEKR | zfyq | 冲不冲 | 日期范围 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 18 | 2009-11-25 | 6.67 | 7.39 | 7.39 | 6.67 | 33132 | 3.656763e+08 | 10.79 | 10.79 | 0.72 | 13.11 | 7.252044 | True | True | (2009-10-31, 2009-11-30] |
1 | 92 | 2010-03-22 | 10.64 | 11.19 | 11.40 | 10.51 | 15432 | 2.543407e+08 | 8.52 | 7.18 | 0.75 | 4.88 | 10.581057 | True | True | (2010-02-28, 2010-03-31] |
2 | 102 | 2010-04-06 | 12.73 | 13.74 | 13.75 | 12.72 | 20556 | 4.105921e+08 | 8.05 | 7.43 | 0.95 | 6.50 | 13.260945 | True | True | (2010-03-31, 2010-04-30] |
3 | 420 | 2011-08-01 | 5.57 | 6.00 | 6.13 | 5.57 | 80982 | 2.492260e+08 | 10.13 | 8.50 | 0.47 | 7.84 | 5.976972 | True | True | (2011-07-31, 2011-08-31] |
4 | 435 | 2011-08-23 | 6.40 | 6.88 | 6.91 | 6.34 | 84290 | 2.884709e+08 | 8.96 | 8.18 | 0.52 | 8.16 | 6.775105 | True | True | (2011-07-31, 2011-08-31] |
5 | 552 | 2012-02-21 | 3.86 | 4.13 | 4.17 | 3.75 | 75526 | 1.601069e+08 | 10.94 | 7.55 | 0.29 | 7.31 | 3.883402 | True | True | (2012-01-31, 2012-02-29] |
6 | 824 | 2013-04-08 | 3.28 | 3.72 | 3.72 | 3.25 | 104370 | 1.998146e+08 | 14.03 | 11.04 | 0.37 | 5.17 | 3.672354 | True | True | (2013-03-31, 2013-04-30] |
7 | 861 | 2013-06-05 | 4.45 | 4.76 | 4.91 | 4.43 | 272415 | 4.106805e+08 | 10.81 | 7.21 | 0.32 | 8.40 | 4.547126 | True | True | (2013-05-31, 2013-06-30] |
8 | 883 | 2013-08-23 | 6.78 | 6.78 | 6.78 | 6.78 | 2356 | 5.011339e+06 | 0.00 | 10.42 | 0.64 | 0.07 | 6.274634 | True | True | (2013-07-31, 2013-08-31] |
9 | 892 | 2013-09-05 | 9.47 | 10.28 | 10.43 | 9.39 | 247005 | 7.613480e+08 | 10.84 | 7.19 | 0.69 | 7.82 | 9.536524 | True | True | (2013-08-31, 2013-09-30] |
10 | 904 | 2013-09-25 | 10.21 | 11.12 | 11.28 | 10.15 | 175912 | 5.919190e+08 | 11.02 | 8.49 | 0.87 | 5.55 | 10.356636 | True | True | (2013-08-31, 2013-09-30] |
11 | 964 | 2013-12-25 | 8.63 | 9.26 | 9.37 | 8.55 | 165617 | 4.649416e+08 | 9.50 | 7.30 | 0.63 | 5.23 | 9.068894 | True | True | (2013-11-30, 2013-12-31] |
12 | 966 | 2013-12-27 | 8.86 | 9.75 | 9.75 | 8.84 | 168891 | 4.935936e+08 | 10.20 | 9.30 | 0.83 | 5.33 | 9.163578 | True | True | (2013-11-30, 2013-12-31] |
13 | 975 | 2014-01-20 | 11.11 | 11.11 | 11.11 | 10.72 | 291067 | 9.950094e+08 | 3.87 | 10.33 | 1.04 | 8.23 | 10.337553 | True | True | (2013-12-31, 2014-01-31] |
14 | 1056 | 2014-05-23 | 7.63 | 8.22 | 8.22 | 7.62 | 439044 | 5.420473e+08 | 8.04 | 10.19 | 0.76 | 6.07 | 8.042405 | True | True | (2014-04-30, 2014-05-31] |
15 | 1102 | 2014-10-30 | 10.06 | 10.06 | 10.06 | 10.06 | 5444 | 8.405736e+06 | 0.00 | 10.19 | 0.93 | 0.08 | 9.769167 | True | True | (2014-09-30, 2014-10-31] |
16 | 1123 | 2014-11-28 | 10.36 | 11.00 | 11.27 | 10.29 | 618805 | 1.029956e+09 | 9.58 | 7.53 | 0.77 | 8.52 | 10.378976 | True | True | (2014-10-31, 2014-11-30] |
17 | 1148 | 2015-01-06 | 10.93 | 12.07 | 12.07 | 10.77 | 459309 | 8.192253e+08 | 11.87 | 10.23 | 1.12 | 6.29 | 12.040501 | True | True | (2014-12-31, 2015-01-31] |
18 | 1156 | 2015-01-16 | 12.25 | 13.16 | 13.29 | 12.20 | 416274 | 8.174527e+08 | 8.99 | 8.49 | 1.03 | 5.70 | 13.106821 | True | True | (2014-12-31, 2015-01-31] |
19 | 1182 | 2015-03-02 | 14.60 | 15.95 | 15.97 | 14.60 | 623351 | 1.454102e+09 | 9.45 | 10.00 | 1.45 | 8.54 | 15.109282 | True | True | (2015-02-28, 2015-03-31] |
20 | 1199 | 2015-04-02 | 15.89 | 17.09 | 17.26 | 15.88 | 657733 | 1.651666e+09 | 8.75 | 8.30 | 1.31 | 9.01 | 16.850508 | True | True | (2015-03-31, 2015-04-30] |
21 | 1219 | 2015-05-04 | 22.48 | 24.72 | 24.72 | 22.28 | 329972 | 1.170745e+09 | 10.87 | 10.11 | 2.27 | 4.52 | 23.557275 | True | True | (2015-04-30, 2015-05-31] |
22 | 1235 | 2015-05-26 | 23.75 | 25.71 | 25.75 | 23.74 | 508543 | 1.919091e+09 | 8.46 | 8.21 | 1.95 | 6.80 | 25.080889 | True | True | (2015-04-30, 2015-05-31] |
23 | 1369 | 2015-12-17 | 12.46 | 13.61 | 13.61 | 12.37 | 1040420 | 1.386511e+09 | 10.03 | 10.11 | 1.25 | 9.11 | 13.276117 | True | True | (2015-11-30, 2015-12-31] |
24 | 1447 | 2016-04-14 | 9.02 | 9.71 | 9.90 | 9.00 | 1016533 | 9.929497e+08 | 10.01 | 8.01 | 0.72 | 8.84 | 9.212066 | True | True | (2016-03-31, 2016-04-30] |
25 | 1875 | 2018-01-11 | 6.25 | 6.77 | 6.77 | 6.24 | 512947 | 3.428190e+08 | 8.62 | 10.08 | 0.62 | 3.71 | 6.276158 | True | True | (2017-12-31, 2018-01-31] |
26 | 2162 | 2019-03-21 | 4.91 | 5.35 | 5.41 | 4.89 | 1234702 | 6.541604e+08 | 10.59 | 8.96 | 0.44 | 8.76 | 5.309214 | True | True | (2019-02-28, 2019-03-31] |
27 | 2223 | 2019-06-21 | 4.24 | 4.33 | 4.45 | 4.15 | 928968 | 4.099626e+08 | 7.43 | 7.18 | 0.29 | 6.59 | 4.156205 | True | True | (2019-05-31, 2019-06-30] |
28 | 2308 | 2019-10-28 | 3.80 | 3.80 | 3.80 | 3.80 | 397858 | 1.535731e+08 | 0.00 | 10.14 | 0.35 | 2.81 | 3.700198 | True | True | (2019-09-30, 2019-10-31] |
29 | 2393 | 2020-03-04 | 3.65 | 4.07 | 4.07 | 3.60 | 1468908 | 5.876640e+08 | 12.74 | 10.30 | 0.38 | 9.01 | 3.751133 | True | True | (2020-02-29, 2020-03-31] |
30 | 2400 | 2020-03-13 | 3.92 | 4.54 | 4.54 | 3.92 | 1501340 | 6.606580e+08 | 15.05 | 10.19 | 0.42 | 9.21 | 4.244405 | True | True | (2020-02-29, 2020-03-31] |
31 | 2417 | 2020-04-08 | 4.31 | 4.31 | 4.31 | 4.31 | 76435 | 3.340189e+07 | 0.00 | 10.23 | 0.40 | 0.47 | 4.264800 | True | True | (2020-03-31, 2020-04-30] |
32 | 2427 | 2020-04-22 | 5.74 | 6.35 | 6.35 | 5.72 | 2972948 | 1.848982e+09 | 10.92 | 10.05 | 0.58 | 18.24 | 6.329473 | True | True | (2020-03-31, 2020-04-30] |
33 | 2443 | 2020-05-19 | 6.04 | 6.55 | 6.55 | 5.97 | 2357484 | 1.513926e+09 | 9.75 | 10.08 | 0.60 | 14.46 | 6.298994 | True | True | (2020-04-30, 2020-05-31] |
34 | 2456 | 2020-06-05 | 6.15 | 6.87 | 6.87 | 6.07 | 1252051 | 8.465352e+08 | 12.82 | 10.10 | 0.63 | 7.68 | 6.507364 | True | True | (2020-05-31, 2020-06-30] |
35 | 2476 | 2020-07-07 | 6.48 | 7.03 | 7.15 | 6.44 | 2169895 | 1.513698e+09 | 10.94 | 8.32 | 0.54 | 13.32 | 6.985538 | True | True | (2020-06-30, 2020-07-31] |
36 | 2521 | 2020-09-08 | 6.93 | 7.43 | 7.44 | 6.84 | 1784993 | 1.300526e+09 | 8.66 | 7.22 | 0.50 | 10.96 | 7.133406 | True | True | (2020-08-31, 2020-09-30] |
37 | 2568 | 2020-11-20 | 5.67 | 6.06 | 6.24 | 5.67 | 1603040 | 9.721904e+08 | 10.40 | 10.58 | 0.58 | 9.84 | 5.833056 | True | True | (2020-10-31, 2020-11-30] |
38 | 2644 | 2021-03-16 | 4.51 | 4.81 | 5.00 | 4.50 | 1033546 | 5.019764e+08 | 11.36 | 9.32 | 0.41 | 6.34 | 4.757991 | True | True | (2021-02-28, 2021-03-31] |
39 | 2739 | 2021-08-03 | 4.59 | 5.52 | 5.52 | 4.55 | 2443362 | 1.321823e+09 | 21.13 | 20.26 | 0.93 | 14.54 | 5.031415 | True | True | (2021-07-31, 2021-08-31] |
40 | 2763 | 2021-09-06 | 5.27 | 5.86 | 6.22 | 5.27 | 2724360 | 1.618332e+09 | 18.27 | 12.69 | 0.66 | 16.21 | 5.387002 | True | True | (2021-08-31, 2021-09-30] |
41 | 2969 | 2022-07-18 | 3.92 | 4.14 | 4.15 | 3.89 | 808393 | 3.298014e+08 | 6.90 | 9.81 | 0.37 | 4.58 | 4.096755 | True | True | (2022-06-30, 2022-07-31] |
42 | 3119 | 2023-03-01 | 5.99 | 6.62 | 6.74 | 5.98 | 2105477 | 1.354974e+09 | 12.60 | 9.78 | 0.59 | 11.57 | 6.170033 | True | True | (2023-02-28, 2023-03-31] |
43 | 3132 | 2023-03-20 | 7.17 | 7.38 | 7.74 | 7.17 | 2932969 | 2.219677e+09 | 8.62 | 11.65 | 0.77 | 16.12 | 6.660683 | True | True | (2023-02-28, 2023-03-31] |
44 | 3141 | 2023-03-31 | 8.28 | 9.39 | 9.63 | 8.11 | 2215561 | 1.987753e+09 | 18.40 | 13.68 | 1.13 | 12.18 | 8.896722 | True | True | (2023-02-28, 2023-03-31] |
45 | 3154 | 2023-04-20 | 11.27 | 13.12 | 13.49 | 11.26 | 3027320 | 3.711599e+09 | 19.49 | 14.69 | 1.68 | 16.64 | 12.197207 | True | True | (2023-03-31, 2023-04-30] |
46 | 3160 | 2023-04-28 | 12.25 | 13.70 | 14.28 | 12.25 | 3021591 | 4.088951e+09 | 16.48 | 11.20 | 1.38 | 16.61 | 13.535083 | True | True | (2023-03-31, 2023-04-30] |
47 | 3194 | 2023-06-20 | 14.35 | 15.48 | 16.30 | 14.23 | 2709833 | 4.162040e+09 | 14.43 | 7.87 | 1.13 | 14.90 | 14.631142 | True | True | (2023-05-31, 2023-06-30] |
48 | 3242 | 2023-08-29 | 9.73 | 10.70 | 10.96 | 9.70 | 2119703 | 2.208865e+09 | 13.21 | 12.16 | 1.16 | 11.65 | 10.649530 | True | True | (2023-07-31, 2023-08-31] |
在符合条件的日子中,挑选同时符合月线条件的日子。找到上面选出来符合日线条件的日子的月份,先取所在月份之前的所有月线数据。再计算当月当前日期之前的月线数据。最后合并在一起计算MIKE指标买入的True/False,并入上面的表格rows。
monthly_data = pd.DataFrame(index=range(len(rows)), columns=rows.columns)
monthly_data['日期'] = rows['日期']
monthly_data.drop(columns = ['index'], inplace=True)
#i=2
for i in range(len(rows)):
the_day = rows.loc[i]
group = df[df['日期范围']==the_day['日期范围']]
group = group[group['日期']<=the_day['日期']]
group.reset_index(inplace=True)
if group.size > 0:
monthly_data.loc[i, '开盘'] = group.head(1)['开盘'].values
monthly_data.loc[i, '收盘'] = group.tail(1)['收盘'].values
monthly_data.loc[i, '最高'] = group['最高'].max()
monthly_data.loc[i, '最低'] = group['最低'].min()
divisor = df_monthly[df_monthly['日期'] < the_day['日期']].tail(1)['收盘']
if divisor.size>0 and divisor.values!=0:
monthly_data.loc[i, '涨跌幅'] = (group.tail(1)['收盘'].values/divisor.values - 1)*100
monthly_data.loc[i, '涨跌幅'] = monthly_data.loc[i, '涨跌幅'].round(2)
#monthly_match.loc[i, '上月月线WEKR'] = df_monthly[df_monthly['日期'] < the_day['日期']].tail(1)['WEKR'].values
#计算月WEKR,需要将本月月线数据并入股票月线数据表并计算WEKR
#日期前所有月线
df_before_date = df_monthly[df_monthly['日期'] < the_day['日期']].copy()
df_insert = pd.DataFrame(monthly_data.loc[i]).transpose() #计算出的月线数据
df_insert.dropna(axis=1, inplace=True)
if df_insert.size > 0:
df_before_date = pd.concat([df_before_date, df_insert], axis=0) #并入本月以前的月线数据
mike(df_before_date)
rows.loc[i, '日月穿'] = df_before_date.tail(1)['冲不冲'].values
df_insert.dropna(axis=1)
再筛选出‘日月穿’列为True 的列
rows_ryc = rows[rows['日月穿']==True].copy()
if rows_ryc.size == 0:
print(f'获得{symbol}回测数据完成,共出现{rows_ryc.shape[0]}个信号\n',end='')
rows_ryc.size
index | 日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 | 振幅 | 涨跌幅 | 涨跌额 | 换手率 | WEKR | zfyq | 冲不冲 | 日期范围 | 日月穿 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7 | 861 | 2013-06-05 | 4.45 | 4.76 | 4.91 | 4.43 | 272415 | 4.106805e+08 | 10.81 | 7.21 | 0.32 | 8.40 | 4.547126 | True | True | (2013-05-31, 2013-06-30] | True |
17 | 1148 | 2015-01-06 | 10.93 | 12.07 | 12.07 | 10.77 | 459309 | 8.192253e+08 | 11.87 | 10.23 | 1.12 | 6.29 | 12.040501 | True | True | (2014-12-31, 2015-01-31] | True |
18 | 1156 | 2015-01-16 | 12.25 | 13.16 | 13.29 | 12.20 | 416274 | 8.174527e+08 | 8.99 | 8.49 | 1.03 | 5.70 | 13.106821 | True | True | (2014-12-31, 2015-01-31] | True |
32 | 2427 | 2020-04-22 | 5.74 | 6.35 | 6.35 | 5.72 | 2972948 | 1.848982e+09 | 10.92 | 10.05 | 0.58 | 18.24 | 6.329473 | True | True | (2020-03-31, 2020-04-30] | True |
最后选出来这4行就是同时符合日线和月线的买入信号。
最后我要找出这些日期中,1日后、3日后、5日后、10日后、20日后的涨幅
def zf_after_index(index, daynum):
try:
zf = df.loc[index+daynum, '收盘'] / df.loc[index, '收盘'] - 1
zf*=100
return zf.round(2)
except:pass
for i in rows_ryc.index:
#print(i)
for daynum in [1,3,5,10,20]:
zf = zf_after_index(rows_ryc.loc[i, 'index'], daynum)
rows_ryc.loc[i, f'{daynum}日后涨幅'] = zf
rows_ryc['symbol'] = str(symbol)
rows_ryc
index | 日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 | 振幅 | 涨跌幅 | ... | zfyq | 冲不冲 | 日期范围 | 日月穿 | 1日后涨幅 | 3日后涨幅 | 5日后涨幅 | 10日后涨幅 | 20日后涨幅 | symbol | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7 | 861 | 2013-06-05 | 4.45 | 4.76 | 4.91 | 4.43 | 272415 | 4.106805e+08 | 10.81 | 7.21 | ... | True | True | (2013-05-31, 2013-06-30] | True | -0.21 | 12.18 | 18.49 | -0.63 | 22.06 | 300002 |
17 | 1148 | 2015-01-06 | 10.93 | 12.07 | 12.07 | 10.77 | 459309 | 8.192253e+08 | 11.87 | 10.23 | ... | True | True | (2014-12-31, 2015-01-31] | True | -0.33 | 3.56 | 2.90 | 15.24 | 16.57 | 300002 |
18 | 1156 | 2015-01-16 | 12.25 | 13.16 | 13.29 | 12.20 | 416274 | 8.174527e+08 | 8.99 | 8.49 | ... | True | True | (2014-12-31, 2015-01-31] | True | 0.46 | 8.13 | 5.70 | 1.90 | 2.81 | 300002 |
32 | 2427 | 2020-04-22 | 5.74 | 6.35 | 6.35 | 5.72 | 2972948 | 1.848982e+09 | 10.92 | 10.05 | ... | True | True | (2020-03-31, 2020-04-30] | True | -6.46 | -12.76 | -21.42 | -8.03 | -12.60 | 300002 |
4 rows × 23 columns
到这里为止,单个股票的回测的代码已经完成,加下面一句,方便批量化的时候观察信息。
print(f'获得{symbol}回测数据完成,共出现{rows_ryc.shape[0]}个信号\n',end='')
然后得封装成函数,下面退格,开头加个def test(symbol):就行。
(不封装成函数的方法我也试过,方法是调用命令行传参,但命令行传参只能传字符串不能传别了,最后还是定义函数比较方便调用。
第三步:遍历沪深京所有股票,调用指标函数回测,获取回测数据并保存
首先就是要取所有股票的代码,同样通过akshare库,然后for循环计算,因为A股现在有5000多只股票,虽然可以一直让电脑运行到计算完毕,但这样效率低而且一出故障,前面做的所有工作就白费,而且不符合我学习的目的。所以我的选择是分批进行获取计算并储存在本地,储存已经遍历过的股票代码,以及最后的成果表格,当下一次打开时,可以直接调用,并继续工作。
最后回测出来的日期的数据,我用pandas保存成excel,已经算过的股票代码,我保存成二进制文件,我觉得这样读写快,因为在外面不需要打开,读取时也不用关系数据类型,比如我就直接将list写进文件,加载出来就是list,如果保存成txt,就还要做一些处理。保存二进制文件,用到的库是pickle库,使用pickle.save()和pickle.load()就行。
上面提到,遍历多个股票运算,最简单的方法就是for循环,但是这样效率太低太慢了,要等一个股票算完,才能算下一个股票。所以单线程肯定是不现实了,然后我又去学了多线程,使用的是threading库。
threading库的使用方法是,将一个函数,放进一个子线程:
threading.Thread(target=函数名,args=(函数的输入变量1,函数的输入变量2,……))
接着使用 Thread.start()让子线程开始运行 。
所以for循环计算单个股票,就变成for循环建立子线程并开始运算。
接着用到的功能就是Thread.join(),即在线程工作完之前让主线程暂停运行,避免子线程还没得出结果,主线程就先运行完了,一般来说是需要设置的,因为主线程在建立完子线程之后就没事干了,这里可以让主线程同时运行一些代码,把Thread.join()放更后面,这样就可以并行运算。
多线程方法介绍完了,接下来是我用到的另一个功能queue队列功能,它允许将函数运算得出的结果一个一个地塞进队列里去,相当于弹匣填充,然后在需要时一个一个取出来用,相当于将弹匣里面的子弹一个一个发射出去,每发射一次就失去一个子弹,同理,每提取一次变量,队列就失去一个变量。那么如果队列处于空状态呢,那么get函数会一直等待,直到又变量塞进来,就可以提取出去。使用也很简单,分别是queue.Queue()建立队列,queue.put()填充子弹,queque.get()发射子弹。
还有一个功能,就是通过函数名称调用函数,我做这个的目的是当我有多个指标回测时,方便我切换。
所有用到的功能都已经介绍完了,下面给出代码:
from formula_testV2 import test #这个是一开始封装的MIKE指标回测函数
from RSI import testrsi
import queue
import subprocess
import sys
import os
import threading
import pandas as pd
import pickle
from concurrent.futures import ThreadPoolExecutor #这个是线程池,试过一下,还不如不用。
import time
import numpy as np
start_time = time.time()
with open('list.bin', 'rb') as f:
a_list = pickle.load(f)
lock = threading.Lock() #原来加锁是为了让print的时候显示不要混乱,原先print的时候要两步,现在只要一步就行,就不用加锁了。
rows_rycs = []
q = queue.Queue()
'''
if os.path.isfile('history_test.xlsx'):
main_sheet = pd.read_excel('history_test.xlsx')
rows_rycs.append(main_sheet)
'''
formula = 'testrsi'
combiner = 'concat_np'
total = 100
def process(symbol):
global count_done
#print(f"{symbol} 在 线程{threading.current_thread().name} 中处理\n",end='')
print(f'加载任务 {symbol}\n',end ='')
try:
func = globals()[formula]
rows_ryc = func(symbol) #如果第一个没运行完,count_done是0,队列不会产生数据,已合并的数量也是0,如果设置大于等于就直接结束了
#rows_rycs.append(rows_ryc)
q.put(rows_ryc)
symbols.append(symbol)
with lock: #儿子拿到遥控器才能操作
count_done+=1 #成功了执行了test才能加,大于等于total才结束
except Exception as e:
error_type = type(e).__name__ # 获取异常类型
print(f"{symbol}发生错误:", error_type)
raise(e)
#print(f'处理{symbol}出错')
finally:
print(f"{symbol} 线程关闭,已完成{count_done}/{total},队列有{q.qsize()}\n",end='')
def concat():
print('合并启动')
if os.path.isfile('history_test.xlsx'):
main_sheet = pd.read_excel('history_test.xlsx')
else:main_sheet = pd.DataFrame()
num = 0
while True: #连接超时困在这个循环里
main_sheet = pd.concat([main_sheet, q.get()]) #当队列中没有数据时,会一直等,要设置timeout=5, 连接超时返回异常
num+=1
print(f'已合并{num}个')
if num%100 == 0:
main_sheet.to_excel('history_test.xlsx', index=False)
print('自动保存history_test.xlsx完成')
if num>=total: #合并数同样要大于等于total才结束
print('合并结束')
break
main_sheet.to_excel('history_test.xlsx', index=False)
print('最后保存history_test.xlsx完成')
def concat_np():
print('合并启动')
if os.path.isfile('rsi_result.npy'):
rsi = np.load('rsi_result.npy')
else:rsi = np.array([])
num = 0
while True: #连接超时困在这个循环里
try:
rsi = np.append(rsi, q.get(timeout=10)) #当队列中没有数据时,会一直等,要设置timeout=5, 连接超时返回异常
except Exception as e:
error_type = type(e).__name__ # 获取异常类型
print(f"{symbol}发生错误:", error_type)
num+=1
print(f'已合并{num}个')
if num%100 == 0:
np.save('rsi_result', rsi)
print('自动保存rsi_result.npy完成')
if num>=total: #合并数同样要大于等于total才结束
print('合并结束')
break
np.save('rsi_result', rsi)
print('最后保存rsi_result.npy完成')
threads = []
if os.path.isfile('saved_stocks.bin'):
with open('saved_stocks.bin','rb') as f:
symbols = pickle.load(f) #读已经完成计算的股票列表
else:
symbols = []
print(f'已完成 {len(symbols)} 个股票的处理,剩余{len(a_list) - len(symbols)}个')
count = 0
total = total
if total + len(symbols) > len(a_list):
total = len(a_list) - len(symbols)
count_done = count
if total == 0:
print('已全部完成')
sys.exit(0)
for symbol in a_list: #给儿子分派任务并开始
if symbol not in symbols:
thread = threading.Thread(target=process, args=(symbol, ))
threads.append(thread)
thread.start()
count+=1
if count>=total or len(symbols) >= len(a_list):
break
func_combine = globals()[combiner] #调用对应名称的函数
func_combine()
#concat_np() #父亲分派完任务后,接收儿子的成果开始合并
#而事实上,父亲合并完才能往下,所以下面两行可要可不要
for thread in threads: #告诉父亲,等所有儿子执行完任务才能做接下来的事
thread.join() #阻塞父亲继续行动
#要子线程完结才能执行下面
with open('saved_stocks.bin','wb') as f:
pickle.dump(symbols, f)
#print('合并结果') #如果使用QUEUE的话,可以一边产出,一边合并
#main_sheet = pd.concat(rows_rycs)
#main_sheet.to_excel('history_test.xlsx', index=False)
expand = time.time()-start_time
expand = round(expand, 2)
print(f'回测完成,用时{expand}s,平均每个{round(expand/total,2)}s')
回测完5288个股票后,得到一个 25229行的excel表格。
有了这个表格,你单纯滚着看它数据也行,接着用pandas做进一步的信息提取也行,比如几个重要的回测数据:总体胜率、买入后各时间段平均的涨幅和跌幅、胜率的时间相关性、出现高胜率的时间分布、买入后涨跌和版块/市值/股价/公告/当日的换手率/当日的市场情绪 等的相关性、各时间段涨跌的数学期望值 等等。
第四步:数据的统计
这部分很简单,了解一些pandas的使用就行了,废话不多说,直接上代码:
import pandas as pd
%matplotlib inline
data = pd.read_excel('history_testV2.xlsx', usecols=['index',
'日期',
'symbol',
'1日后涨幅',
'3日后涨幅',
'5日后涨幅',
'10日后涨幅',
'20日后涨幅',
])
data
得到
index | 日期 | 1日后涨幅 | 3日后涨幅 | 5日后涨幅 | 10日后涨幅 | 20日后涨幅 | symbol | |
---|---|---|---|---|---|---|---|---|
0 | 3086 | 2013-05-30 | 6.08 | 12.71 | 25.97 | 11.60 | 3.87 | 8 |
1 | 4977 | 2022-02-18 | 0.00 | 3.82 | -2.78 | -5.90 | -13.54 | 8 |
2 | 57 | 2000-04-06 | 4.00 | -1.89 | -1.05 | -3.16 | -11.79 | 5 |
3 | 3217 | 2014-09-25 | -4.86 | -8.56 | -7.64 | -7.18 | -11.34 | 5 |
4 | 3375 | 2015-10-22 | 10.04 | 3.31 | 2.37 | -6.72 | 0.57 | 5 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
25223 | 669 | 2020-10-19 | -2.69 | -14.95 | -14.50 | -8.52 | -4.04 | 831039 |
25224 | 697 | 2021-09-02 | 21.89 | 34.76 | 53.36 | 29.76 | 36.48 | 831039 |
25225 | 787 | 2022-01-18 | -6.11 | -7.44 | -14.00 | -13.11 | -17.89 | 831039 |
25226 | 206 | 2017-01-20 | -10.42 | 2.08 | -6.25 | 39.58 | 29.17 | 831445 |
25227 | 624 | 2019-03-22 | 7.50 | 14.50 | 9.00 | 13.00 | 40.00 | 831445 |
25228 rows × 8 columns
#每一列的涨跌比
rom = [[] for i in range(6)]
index = []
for daynum in [1,3,5,10,20]:
index.append(f'{daynum}日后')
p_up = data[data[f'{daynum}日后涨幅'] > 0].shape[0]/data.shape[0] * 100
up_average = data[data[f'{daynum}日后涨幅'] > 0][f'{daynum}日后涨幅'].mean()
down_average = data[data[f'{daynum}日后涨幅'] < 0][f'{daynum}日后涨幅'].mean()
up_max = data[data[f'{daynum}日后涨幅'] > 0][f'{daynum}日后涨幅'].max()
down_max = data[data[f'{daynum}日后涨幅'] < 0][f'{daynum}日后涨幅'].min()
p_math = ((1 + up_average * p_up/10000) * (1 + down_average * (1-p_up)/10000)-1)*100
'''
print(f'{daynum}日后上涨概率:{p_up:.2f}%')
print(f'平均涨幅:{up_average:.2f}%')
print(f'平均跌幅:{down_average:.2f}%')
print(f'最大涨幅:{up_max:.2f}%')
print(f'最大跌幅:{down_max:.2f}%')
print(f'数学期望:{p_math:.2f}%')
'''
rom[0].append(round(p_up, 2))
rom[1].append(round(up_average, 2))
rom[2].append(round(down_average, 2))
rom[3].append(round(up_max, 2))
rom[4].append(round(down_max, 2))
rom[5].append(round(p_math, 2))
df = {'上涨概率':rom[0],
'平均涨幅':rom[1],
'平均跌幅':rom[2],
'最大涨幅':rom[3],
'最大跌幅':rom[4],
'数学期望':rom[5], }
index = {i:x for i,x in enumerate(index)}
df = pd.DataFrame(df)
df = df.rename(index=index)
df
得到
上涨概率 | 平均涨幅 | 平均跌幅 | 最大涨幅 | 最大跌幅 | 数学期望 | |
---|---|---|---|---|---|---|
1日后 | 50.13 | 5.55 | -4.30 | 222.22 | -235.42 | 4.96 |
3日后 | 48.76 | 9.92 | -6.99 | 751.51 | -158.33 | 8.34 |
5日后 | 46.36 | 12.70 | -8.68 | 882.35 | -341.67 | 10.06 |
10日后 | 46.17 | 18.03 | -11.36 | 1247.06 | -252.94 | 13.88 |
20日后 | 45.44 | 25.67 | -14.65 | 5716.67 | -276.47 | 18.93 |
单从数学期望和平均涨跌幅来说还是不错的,涨的时候涨得多,跌的时候跌得少,不过这个胜率倒是一般般,只有1日后涨的概率过半,其他都不过半,用来高频量化交易不太妙。
参考文献:
[1]Anaconda-- conda 创建、激活、退出、删除虚拟环境_anaconda如何关闭虚拟环境-CSDN博客
[2]JupyterLab使用教程_jupiterlab-CSDN博客
[3]Miniconda — miniconda documentation
[4]命令行给python脚本传参数的几种方式_python cmd 参数_zhuifengxu的博客-CSDN博客
[6] Welcome to AKShare’s Online Documentation! — AKShare 1.11.21 文档
[7] Pandas 教程 | 菜鸟教程
[9] python列表解析([ x for x in list])-CSDN博客
[10]python3 踩坑之:*操作符生成二维列表_[[a]*3]*3 python-CSDN博客
[11] NumPy 教程 | 菜鸟教程