前言:

      第一次写文章哈哈!分享我最近研究的回测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博客

[5]anaconda prompt快捷方式? - 知乎 

[6] Welcome to AKShare’s Online Documentation! — AKShare 1.11.21 文档

[7] Pandas 教程 | 菜鸟教程

[8]Python 基础教程 | 菜鸟教程

[9] python列表解析([ x for x in list])-CSDN博客

[10]python3 踩坑之:*操作符生成二维列表_[[a]*3]*3 python-CSDN博客 

[11] NumPy 教程 | 菜鸟教程

[12]Python 跳出多重循环总结_python退出多重循环-CSDN博客 

[13]功能强大的python包(十一):threading (多线程)_threading包使用方法-CSDN博客

Logo

更多推荐