├── .gitignore ├── 01PM.py ├── 02LVA.py ├── 03PAIR.py ├── 04SMA.py ├── 05ALMA.py ├── 05TMA.py ├── 06SR.py ├── 07CHANNEL.py ├── README.md ├── cumreturn.csv ├── rsrs.py ├── run.py └── tradesys.py /.gitignore: -------------------------------------------------------------------------------- 1 | serverIP.txt 2 | .env 3 | output/*.jpg 4 | datas/*.csv 5 | -------------------------------------------------------------------------------- /01PM.py: -------------------------------------------------------------------------------- 1 | # 实现经典量化策略 2 | # 低波动异常策略 3 | # 参考 ZuraKakushadze,JuanAndrésSerur. 151 Trading Strategies. 4 | 5 | 6 | import tradesys as ts 7 | import run 8 | import sys 9 | import akshare as ak 10 | import efinance as ef 11 | import pandas as pd 12 | import numpy as np 13 | import os 14 | import datetime 15 | 16 | 17 | # 策略类 18 | class PMStrategy(ts.Strategy): 19 | """ 20 | N,交易股票只数 21 | period, 调仓周期 22 | bprint, 是否输出交易过程 23 | """ 24 | params = (("N", 10), 25 | ("period", 20), 26 | ("bprint", False),) 27 | def __init__(self, refresh = False): 28 | super(PMStrategy, self).__init__() 29 | datafile = "./datas/cumreturn.csv" 30 | self.cumreturns = pd.read_csv(datafile) 31 | self.cumreturns.日期 = pd.to_datetime(self.cumreturns.日期) 32 | self.cumreturns.set_index("日期", drop = True, inplace = True) 33 | self.bIn = False 34 | self.bstart = True 35 | self.days = 0 # 记录交易天数 36 | self.bookmarker = pd.DataFrame() 37 | 38 | # 数据转换 39 | def transform(self, date): 40 | stock_list = self.cumreturns.loc[str(date)] 41 | s = stock_list.values[0][1:-1] 42 | s = s.replace("'", "") 43 | s_list = s.split() 44 | return s_list 45 | 46 | # 交易数量取整 47 | def downcast(self, amount, lot): 48 | return abs(amount//lot*lot) 49 | 50 | def next(self): 51 | s_list = self.transform(self.datas[0].datetime.date(0)) 52 | if self.bIn == False: 53 | cash = self.broker.get_cash()/self.p.N 54 | # 如果是第一次交易,直接使用排序结果 55 | if self.bstart: 56 | self.bookmarker["股票代码"] = s_list 57 | self.bookmarker["买入价"] = 0.0 58 | self.bstart = False 59 | for stock in self.bookmarker["股票代码"].values: 60 | data = self.getdatabyname(stock) 61 | pos = self.getposition(data).size 62 | # 计算交易数量 63 | amount = self.downcast(cash*0.9/data.close[0], 100) 64 | if not pos: 65 | self.buy(data = data, size = amount) 66 | self.p.N -= 1 67 | 68 | self.bookmarker.买入价[self.bookmarker.股票代码 == stock] = data.close[0] 69 | if self.is_lastday(data = data): 70 | self.close(data = data) 71 | self.bIn = True 72 | # 到达交易天数 73 | elif self.days == self.p.period: 74 | self.bookmarker["现价"] = 0.0 75 | self.bookmarker["累积收益率"] = 0.0 76 | for code in self.bookmarker.股票代码.values: 77 | data = self.getdatabyname(code) 78 | self.bookmarker.现价[self.bookmarker.股票代码 == code] = data.close[0] 79 | self.bookmarker.累积收益率[self.bookmarker.股票代码 == code] = data.close[0]/self.bookmarker[self.bookmarker.股票代码 == code].买入价 - 1.0 80 | 81 | # 找出累积收益率最低的股票,卖出 82 | min_code = self.bookmarker[self.bookmarker.累积收益率 == self.bookmarker.min().累积收益率].股票代码.values[0] 83 | min_data = self.getdatabyname(min_code) 84 | self.close(data = min_data) 85 | self.bookmarker = self.bookmarker[self.bookmarker.股票代码 != min_code] 86 | self.p.N += 1 87 | self.bIn = False 88 | # 放入累积收益最高的股票 89 | self.bookmarker = self.bookmarker.append({"股票代码":s_list[0], "买入价":0.0, "现价":0.0, "累积收益率":0.0}, ignore_index = True) 90 | self.days = 0 91 | else: 92 | self.days += 1 93 | 94 | def is_lastday(self,data): 95 | try: 96 | next_next_close = data.close[2] 97 | except IndexError: 98 | return True 99 | except: 100 | print("发生其它错误") 101 | return False 102 | 103 | 104 | # 形成股票池 105 | @run.change_dir 106 | def make_pool(refresh = False): 107 | data = pd.DataFrame() 108 | path = "./datas/" 109 | stockfile = path + "stocks.csv" 110 | if os.path.exists(stockfile) and refresh == False: 111 | data = pd.read_csv(stockfile, dtype = {"code":str, "昨日收盘":np.float64}) 112 | else: 113 | stock_zh_a_spot_df = ak.stock_zh_a_spot() 114 | stock_zh_a_spot_df.to_csv(stockfile) 115 | data = stock_zh_a_spot_df 116 | codes = select(data) 117 | return codes 118 | 119 | 120 | # 对股票数据进行筛选 121 | def select(data, highprice = sys.float_info.max, lowprice = 0.0): 122 | # 对股价进行筛选 123 | smalldata = data[(data.最高 < highprice) & (data.最低 > lowprice)] 124 | # 排除ST个股 125 | smalldata = smalldata[~ smalldata.名称.str.contains("ST")] 126 | # 排除要退市个股 127 | smalldata = smalldata[~ smalldata.名称.str.contains("退")] 128 | 129 | codes = [] 130 | for code in smalldata.代码.values: 131 | codes.append(code[2:]) 132 | 133 | return codes 134 | 135 | 136 | # 下载数据并形成累积收益率 137 | def make_data(codes, start_date, end_date, refresh = False): 138 | cumret = pd.Series() 139 | n = len(codes) 140 | i = 0 141 | start = np.datetime64(datetime.datetime.strptime(start_date, "%Y%m%d")) 142 | end = np.datetime64(datetime.datetime.strptime(end_date, "%Y%m%d")) 143 | for code in codes: 144 | print("下载数据进度", i/n) 145 | i += 1 146 | stock_data = ts.get_data(code = code, 147 | start_date = start_date, 148 | end_date = end_date, 149 | adjust = "qfq", 150 | period = "daily", 151 | refresh = refresh) 152 | if len(stock_data) == 0: 153 | continue 154 | date = stock_data.日期.values 155 | start_gap = gap_days(start, date[0]) 156 | end_gap = gap_days(end, date[-1]) 157 | if start_gap == 0 and end_gap == 0: 158 | # 生成累积收益率数据 159 | stock_data["累积收益率"] = stock_data["收盘"]/stock_data["收盘"][0] - 1.0 160 | cumret[code] = stock_data["累积收益率"] 161 | return cumret 162 | 163 | 164 | # 计算每日累积收益率 165 | @run.change_dir 166 | def get_top10(cumret, retry = False): 167 | datafile = "./datas/cumreturn.csv" 168 | if os.path.exists(datafile) and retry == False: 169 | results = pd.read_csv(datafile) 170 | results.日期 = pd.to_datetime(results.日期) 171 | results.set_index("日期", drop = True, inplace = True) 172 | return results 173 | cumreturn = pd.DataFrame() 174 | temp = pd.Series() 175 | n = len(cumret) 176 | m = len(cumret[0].index) 177 | j = 0 178 | print(m, n) 179 | # input("按任意键继续") 180 | for date in cumret[0].index: 181 | # print(date) 182 | i = 0 183 | for stock in cumret: 184 | j += 1 185 | print("计算累积收益率进度:", j/(m*n)) 186 | temp["日期"] = date 187 | temp["股票代码"] = cumret.index[i] 188 | ret = stock[stock.index == date].values 189 | if len(ret) == 0: 190 | temp["累积收益率"] = np.NaN 191 | else: 192 | temp["累积收益率"] = stock[stock.index == date].values[0] 193 | # print(temp["累积收益率"]) 194 | cumreturn = cumreturn.append(temp, ignore_index = True) 195 | i += 1 196 | # print(cumreturn) 197 | results = pd.DataFrame() 198 | top10 = [] 199 | for date in cumret[0].index: 200 | temp = cumreturn[cumreturn.日期 == date] 201 | temp = temp.sort_values(by = "累积收益率", ascending = False) 202 | top10.append(temp.loc[:, ["股票代码"]].values[:10].T[0]) 203 | # results.append(temp) 204 | # results = results.append({"日期": date, "累积收益率": temp}, ignore_index = True) 205 | results["日期"] = cumret[0].index 206 | results["累积收益率"] = top10 207 | results.set_index("日期", drop = True, inplace = True) 208 | results.to_csv(datafile) 209 | print(results.info(), results.head()) 210 | return results 211 | 212 | 213 | # 两个日期之间相差的天数 214 | def gap_days(date1, date2): 215 | return (date1 - date2)/np.timedelta64(1, 'D') 216 | 217 | 218 | # 重新计算数据 219 | def init_data(start_date = "20100108", end_date = "20201231", retry = False): 220 | codes = make_pool() 221 | cumret = make_data(codes, start_date, end_date, refresh = retry) 222 | codes = cumret.index.values 223 | if retry == True: 224 | results = get_top10(cumret, retry = False) 225 | return codes 226 | 227 | 228 | @run.change_dir 229 | def pm(): 230 | ts.init_display() 231 | start_date = "20100108" 232 | end_date = "20201231" 233 | codes = init_data(start_date = start_date, end_date = end_date, retry = False) 234 | backtest = ts.BackTest( 235 | strategy = PMStrategy, 236 | codes = codes, 237 | bk_code = "000300", 238 | start_date = start_date, 239 | end_date = end_date, 240 | rf = 0.03, 241 | start_cash = 10000000, 242 | stamp_duty=0.005, 243 | commission=0.0001, 244 | adjust = "hfq", 245 | period = "daily", 246 | refresh = False, 247 | bprint = False, 248 | bdraw = False) 249 | results = backtest.run() 250 | print("回测结果", results[:-2]) 251 | 252 | 253 | # 测试日期索引 254 | @run.change_dir 255 | def test_index(): 256 | datafile = "./datas/cumreturn.csv" 257 | cumreturns = pd.read_csv(datafile) 258 | cumreturns.日期 = pd.to_datetime(cumreturns.日期) 259 | cumreturns.set_index("日期", drop = True, inplace = True) 260 | print(cumreturns.info()) 261 | print(cumreturns.head()) 262 | x = cumreturns.loc["2010-01-08"] 263 | date = datetime.date(2010, 1, 8) 264 | y = cumreturns.loc[str(date)] 265 | print(x) 266 | 267 | 268 | if __name__ == "__main__": 269 | pm() 270 | # test_index() 271 | -------------------------------------------------------------------------------- /02LVA.py: -------------------------------------------------------------------------------- 1 | # 实现经典量化策略 2 | # 低波动异常策略 Low-Volatility Anomaly 3 | # 参考 ZuraKakushadze,JuanAndrésSerur. 151 Trading Strategies. 4 | 5 | 6 | import tradesys as ts 7 | import run 8 | import sys 9 | import akshare as ak 10 | import efinance as ef 11 | import pandas as pd 12 | import numpy as np 13 | import os 14 | import datetime 15 | 16 | 17 | # 策略类 18 | class LVAStrategy(ts.Strategy): 19 | """ 20 | N,交易股票只数 21 | period, 调仓周期 22 | bprint, 是否输出交易过程 23 | """ 24 | params = (("N", 10), 25 | ("period", 20), 26 | ("bprint", False),) 27 | def __init__(self, refresh = False): 28 | super(LVAStrategy, self).__init__() 29 | datafile = "./datas/retvar.csv" 30 | self.retvar = pd.read_csv(datafile) 31 | self.retvar.日期 = pd.to_datetime(self.retvar.日期) 32 | self.retvar.set_index("日期", drop = True, inplace = True) 33 | self.bIn = False 34 | self.bstart = True 35 | self.days = 0 # 记录交易天数 36 | self.bookmarker = pd.DataFrame() 37 | 38 | # 数据转换 39 | def transform(self, date): 40 | stock_list = self.retvar.loc[str(date)] 41 | s = stock_list.values[0][1:-1] 42 | s = s.replace("'", "") 43 | s_list = s.split()[:19:2] 44 | return s_list 45 | 46 | # 交易数量取整 47 | def downcast(self, amount, lot): 48 | return abs(amount//lot*lot) 49 | 50 | def next(self): 51 | s_list = self.transform(self.datas[0].datetime.date(0)) 52 | if self.bIn == False: 53 | cash = self.broker.get_cash()/self.p.N 54 | # 如果是第一次交易,直接使用排序结果 55 | if self.bstart: 56 | self.bookmarker["股票代码"] = s_list 57 | self.bookmarker["买入价"] = 0.0 58 | self.bstart = False 59 | for stock in self.bookmarker["股票代码"].values: 60 | data = self.getdatabyname(stock) 61 | pos = self.getposition(data).size 62 | # 计算交易数量 63 | amount = self.downcast(cash*0.9/data.close[0], 100) 64 | if not pos: 65 | self.buy(data = data, size = amount) 66 | self.p.N -= 1 67 | 68 | self.bookmarker.买入价[self.bookmarker.股票代码 == stock] = data.close[0] 69 | if self.is_lastday(data = data): 70 | self.close(data = data) 71 | self.bIn = True 72 | # 到达交易天数 73 | elif self.days == self.p.period: 74 | self.bookmarker["现价"] = 0.0 75 | self.bookmarker["累积收益率"] = 0.0 76 | for code in self.bookmarker.股票代码.values: 77 | data = self.getdatabyname(code) 78 | self.bookmarker.现价[self.bookmarker.股票代码 == code] = data.close[0] 79 | self.bookmarker.累积收益率[self.bookmarker.股票代码 == code] = data.close[0]/self.bookmarker[self.bookmarker.股票代码 == code].买入价 - 1.0 80 | 81 | # 找出累积收益率最低的股票的股票,卖出 82 | min_code = self.bookmarker[self.bookmarker.累积收益率 == self.bookmarker.min().累积收益率].股票代码.values[0] 83 | min_data = self.getdatabyname(min_code) 84 | self.close(data = min_data) 85 | self.bookmarker = self.bookmarker[self.bookmarker.股票代码 != min_code] 86 | self.p.N += 1 87 | self.bIn = False 88 | # 放入收益方差最低的股票 89 | self.bookmarker = self.bookmarker.append({"股票代码":s_list[0], "买入价":0.0, "现价":0.0, "累积收益率":0.0}, ignore_index = True) 90 | self.days = 0 91 | else: 92 | self.days += 1 93 | 94 | def is_lastday(self,data): 95 | try: 96 | next_next_close = data.close[2] 97 | except IndexError: 98 | return True 99 | except: 100 | print("发生其它错误") 101 | return False 102 | 103 | 104 | # 形成股票池 105 | @run.change_dir 106 | def make_pool(refresh = False): 107 | data = pd.DataFrame() 108 | path = "./datas/" 109 | stockfile = path + "stocks.csv" 110 | if os.path.exists(stockfile) and refresh == False: 111 | data = pd.read_csv(stockfile, dtype = {"code":str, "昨日收盘":np.float64}) 112 | else: 113 | stock_zh_a_spot_df = ak.stock_zh_a_spot() 114 | stock_zh_a_spot_df.to_csv(stockfile) 115 | data = stock_zh_a_spot_df 116 | codes = select(data) 117 | return codes 118 | 119 | 120 | # 对股票数据进行筛选 121 | def select(data, highprice = sys.float_info.max, lowprice = 0.0): 122 | # 对股价进行筛选 123 | smalldata = data[(data.最高 < highprice) & (data.最低 > lowprice)] 124 | # 排除ST个股 125 | smalldata = smalldata[~ smalldata.名称.str.contains("ST")] 126 | # 排除要退市个股 127 | smalldata = smalldata[~ smalldata.名称.str.contains("退")] 128 | 129 | codes = [] 130 | for code in smalldata.代码.values: 131 | codes.append(code[2:]) 132 | 133 | return codes 134 | 135 | 136 | # 下载数据并计算收益方差 137 | def make_data(codes, start_date, end_date, refresh = False): 138 | rets = pd.Series() 139 | n = len(codes) 140 | i = 0 141 | start = np.datetime64(datetime.datetime.strptime(start_date, "%Y%m%d")) 142 | end = np.datetime64(datetime.datetime.strptime(end_date, "%Y%m%d")) 143 | for code in codes: 144 | print("下载数据进度", i/n) 145 | i += 1 146 | stock_data = ts.get_data(code = code, 147 | start_date = start_date, 148 | end_date = end_date, 149 | adjust = "qfq", 150 | period = "daily", 151 | refresh = refresh) 152 | if len(stock_data) == 0: 153 | continue 154 | date = stock_data.日期.values 155 | start_gap = gap_days(start, date[0]) 156 | end_gap = gap_days(end, date[-1]) 157 | if start_gap == 0 and end_gap == 0: 158 | # 生成累积收益率数据 159 | stock_data["每日收益"] = stock_data["收盘"] - stock_data["收盘"].shift(1) 160 | rets[code] = stock_data["每日收益"] 161 | return rets 162 | 163 | 164 | # 计算每日收益率序列方差 165 | @run.change_dir 166 | def get_top10(rets, retry = False): 167 | datafile = "./datas/retvar.csv" 168 | if os.path.exists(datafile) and retry == False: 169 | results = pd.read_csv(datafile) 170 | results.日期 = pd.to_datetime(results.日期) 171 | results.set_index("日期", drop = True, inplace = True) 172 | return results 173 | returns = pd.DataFrame() 174 | temp = pd.Series() 175 | n = len(rets) 176 | m = len(rets[0].index) 177 | j = 0 178 | # print(m, n) 179 | # input("按任意键继续") 180 | for date in rets[0].index: 181 | # print(date) 182 | i = 0 183 | temp["日期"] = date 184 | # temp["股票代码"] = rets.index[i] 185 | temp_data = pd.DataFrame() 186 | temp_code = [] 187 | temp_var = [] 188 | for stock in rets: 189 | j += 1 190 | print("计算收益方差序列进度:", j/(m*n)) 191 | 192 | ret = stock[stock.index < date].values 193 | if len(ret) == 0 or len(ret) == 1: 194 | temp_var.append(0.0) 195 | else: 196 | temp_var.append(np.var(ret[1:])) 197 | temp_code.append(rets.index[i]) 198 | i += 1 199 | # temp["收益方差"] = temp_var 200 | # temp["股票代码"] = temp_code 201 | temp_data["收益方差"] = temp_var 202 | temp_data["股票代码"] = temp_code 203 | # print("测试a", date, temp_data) 204 | temp["收益方差"] = temp_data 205 | returns = returns.append(temp, ignore_index = True) 206 | 207 | # 找到每个交易日收益波动最小的十只股票 208 | top10 = pd.DataFrame(columns = ["日期", "股票代码"]) 209 | dates = rets[0].index 210 | temp_data = [] 211 | m = len(returns) 212 | n = len(returns.iloc[i, :].values[0]) 213 | t = 0 214 | for i in range(len(returns)): 215 | retvar = returns.iloc[i, :].values[0] 216 | # print(dates[i], retvar, type(retvar)) 217 | topvar = [] 218 | topcode = [] 219 | min_index_list = [] 220 | min_index = -1 221 | for k in range(10): 222 | min_var = float("inf") 223 | for j in range(len(retvar)): 224 | t += 1 225 | print("准备数据进程", t/(m*n*10)) 226 | if j in min_index_list: 227 | continue 228 | var_value = retvar.iloc[j, :].收益方差 229 | if min_var > var_value: 230 | min_var = var_value 231 | min_index = j 232 | topvar.append(min_var) 233 | min_index_list.append(min_index) 234 | topcode.append(retvar.iloc[min_index, :].股票代码) 235 | data = pd.Series(topcode, name = dates[i]) 236 | temp_data.append(data) 237 | top10["日期"] = dates 238 | top10["股票代码"] = temp_data 239 | top10.set_index("日期", drop = True, inplace = True) 240 | 241 | top10.to_csv(datafile) 242 | 243 | 244 | # 两个日期之间相差的天数 245 | def gap_days(date1, date2): 246 | return (date1 - date2)/np.timedelta64(1, 'D') 247 | 248 | 249 | # 重新计算数据 250 | def init_data(start_date = "20100108", end_date = "20201231", retry = False): 251 | codes = make_pool() 252 | rets = make_data(codes, start_date, end_date, refresh = False) 253 | # print("测试", rets.head()) 254 | # print(rets.index) 255 | # print(rets[0].index) 256 | # input("按任意键继续") 257 | 258 | codes = rets.index.values 259 | if retry == True: 260 | get_top10(rets, retry = False) 261 | 262 | # test_read_data() 263 | return codes 264 | 265 | 266 | # 测试读取数据 267 | @run.change_dir 268 | def test_read_data(): 269 | print("测试读取数据") 270 | datafile = "./datas/retvar.csv" 271 | data = pd.read_csv(datafile) 272 | data.日期 = pd.to_datetime(data.日期) 273 | data.set_index("日期", drop = True, inplace = True) 274 | print(data.info(), data.index) 275 | print(data.iloc[100, :].values[0]) 276 | print(type(data.iloc[100, :].values[0])) 277 | 278 | 279 | @run.change_dir 280 | def lva(): 281 | ts.init_display() 282 | start_date = "20100108" 283 | end_date = "20201231" 284 | codes = init_data(start_date = start_date, end_date = end_date, retry = False) 285 | backtest = ts.BackTest( 286 | strategy = LVAStrategy, 287 | codes = codes, 288 | bk_code = "000300", 289 | start_date = start_date, 290 | end_date = end_date, 291 | rf = 0.03, 292 | start_cash = 10000000, 293 | stamp_duty=0.005, 294 | commission=0.0001, 295 | adjust = "hfq", 296 | period = "daily", 297 | refresh = False, 298 | bprint = False, 299 | bdraw = False) 300 | results = backtest.run() 301 | print("回测结果", results[:-2]) 302 | 303 | 304 | # 测试日期索引 305 | @run.change_dir 306 | def test_index(): 307 | datafile = "./datas/cumreturn.csv" 308 | cumreturns = pd.read_csv(datafile) 309 | cumreturns.日期 = pd.to_datetime(cumreturns.日期) 310 | cumreturns.set_index("日期", drop = True, inplace = True) 311 | print(cumreturns.info()) 312 | print(cumreturns.head()) 313 | x = cumreturns.loc["2010-01-08"] 314 | date = datetime.date(2010, 1, 8) 315 | y = cumreturns.loc[str(date)] 316 | print(x) 317 | 318 | 319 | if __name__ == "__main__": 320 | lva() 321 | # test_index() 322 | # init_data(retry = False) 323 | -------------------------------------------------------------------------------- /03PAIR.py: -------------------------------------------------------------------------------- 1 | # 实现经典量化策略 2 | # 配对交易 Pairs Trading 3 | # 参考 ZuraKakushadze,JuanAndrésSerur. 151 Trading Strategies. 4 | 5 | 6 | import tradesys as ts 7 | import run 8 | import sys 9 | import akshare as ak 10 | import efinance as ef 11 | import pandas as pd 12 | import numpy as np 13 | import os 14 | import datetime 15 | 16 | 17 | # 策略类 18 | class PAIRStrategy(ts.Strategy): 19 | """ 20 | N,交易股票只数 21 | period, 调仓周期 22 | bprint, 是否输出交易过程 23 | """ 24 | params = (("N", 1), 25 | ("bprint", False),) 26 | def __init__(self, refresh = False): 27 | super(PAIRStrategy, self).__init__() 28 | self.bIn = False 29 | self.begin = [] 30 | self.begin.append(self.datas[0].close[0]) 31 | self.begin.append(self.datas[1].close[0]) 32 | self.ret = [list(), list()] 33 | self.ret_sub = [] 34 | self.ret_sub_std = [] 35 | 36 | # 交易数量取整 37 | def downcast(self, amount, lot): 38 | return abs(amount//lot*lot) 39 | 40 | def next(self): 41 | # print("日期", self.datas[0].datetime.date(0)) 42 | for i, d in enumerate(self.datas): 43 | # print("股票", i, d.close[0]) 44 | temp_ret = d.close[0]/self.begin[i] - 1.0 45 | self.ret[i].append(temp_ret) 46 | self.ret_sub.append(self.ret[0][-1] - self.ret[1][-1]) 47 | self.ret_sub_std.append(np.std(list(map(abs, self.ret_sub)))) 48 | bBuy = self.ret_sub[-1] > self.p.N * self.ret_sub_std[-1] 49 | if self.bIn == False: 50 | # 判断买入条件 51 | if bBuy == True: 52 | i = -1 53 | if self.ret_sub[-1] > 0: 54 | i = 1 55 | else: 56 | i = 0 57 | # 计算买入数量 58 | cash = self.broker.get_cash() 59 | price = self.datas[i].close[0] 60 | amount = self.downcast(cash*0.9/price, 100) 61 | # print("现金", cash, "股价", price, "交易量", amount) 62 | self.buy(data = self.datas[i], size = amount) 63 | self.bIn = True 64 | else: 65 | # 判断卖出条件 66 | if bBuy == False: 67 | for i, d in enumerate(self.datas): 68 | data = self.datas[i] 69 | pos = self.getposition(data).size 70 | if pos != 0: 71 | # print("卖出", i, pos) 72 | self.close(data = data) 73 | self.bIn = False 74 | 75 | def is_lastday(self,data): 76 | try: 77 | next_next_close = data.close[2] 78 | except IndexError: 79 | return True 80 | except: 81 | print("发生其它错误") 82 | return False 83 | 84 | 85 | # 形成股票池 86 | @run.change_dir 87 | def make_pool(refresh = False): 88 | data = pd.DataFrame() 89 | path = "./datas/" 90 | stockfile = path + "stocks.csv" 91 | if os.path.exists(stockfile) and refresh == False: 92 | data = pd.read_csv(stockfile, dtype = {"code":str, "昨日收盘":np.float64}) 93 | else: 94 | stock_zh_a_spot_df = ak.stock_zh_a_spot() 95 | stock_zh_a_spot_df.to_csv(stockfile) 96 | data = stock_zh_a_spot_df 97 | codes = select(data) 98 | return codes 99 | 100 | 101 | # 对股票数据进行筛选 102 | def select(data, highprice = sys.float_info.max, lowprice = 0.0): 103 | # 对股价进行筛选 104 | smalldata = data[(data.最高 < highprice) & (data.最低 > lowprice)] 105 | # 排除ST个股 106 | smalldata = smalldata[~ smalldata.名称.str.contains("ST")] 107 | # 排除要退市个股 108 | smalldata = smalldata[~ smalldata.名称.str.contains("退")] 109 | 110 | codes = [] 111 | for code in smalldata.代码.values: 112 | codes.append(code[2:]) 113 | 114 | return codes 115 | 116 | 117 | # 下载数据并计算收益方差 118 | def make_data(codes, start_date, end_date, refresh = False): 119 | rets = pd.Series() 120 | n = len(codes) 121 | i = 0 122 | start = np.datetime64(datetime.datetime.strptime(start_date, "%Y%m%d")) 123 | end = np.datetime64(datetime.datetime.strptime(end_date, "%Y%m%d")) 124 | for code in codes: 125 | print("下载数据进度", i/n) 126 | i += 1 127 | stock_data = ts.get_data(code = code, 128 | start_date = start_date, 129 | end_date = end_date, 130 | adjust = "qfq", 131 | period = "daily", 132 | refresh = refresh) 133 | if len(stock_data) == 0: 134 | continue 135 | date = stock_data.日期.values 136 | start_gap = gap_days(start, date[0]) 137 | end_gap = gap_days(end, date[-1]) 138 | if start_gap == 0 and end_gap == 0: 139 | # 生成累积收益率数据 140 | stock_data["每日收益"] = stock_data["收盘"] - stock_data["收盘"].shift(1) 141 | rets[code] = stock_data["每日收益"] 142 | return rets 143 | 144 | 145 | # 计算每日收益率序列方差 146 | @run.change_dir 147 | def get_top10(rets, retry = False): 148 | datafile = "./datas/retvar.csv" 149 | if os.path.exists(datafile) and retry == False: 150 | results = pd.read_csv(datafile) 151 | results.日期 = pd.to_datetime(results.日期) 152 | results.set_index("日期", drop = True, inplace = True) 153 | return results 154 | returns = pd.DataFrame() 155 | temp = pd.Series() 156 | n = len(rets) 157 | m = len(rets[0].index) 158 | j = 0 159 | # print(m, n) 160 | # input("按任意键继续") 161 | for date in rets[0].index: 162 | # print(date) 163 | i = 0 164 | temp["日期"] = date 165 | # temp["股票代码"] = rets.index[i] 166 | temp_data = pd.DataFrame() 167 | temp_code = [] 168 | temp_var = [] 169 | for stock in rets: 170 | j += 1 171 | print("计算收益方差序列进度:", j/(m*n)) 172 | 173 | ret = stock[stock.index < date].values 174 | if len(ret) == 0 or len(ret) == 1: 175 | temp_var.append(0.0) 176 | else: 177 | temp_var.append(np.var(ret[1:])) 178 | temp_code.append(rets.index[i]) 179 | i += 1 180 | # temp["收益方差"] = temp_var 181 | # temp["股票代码"] = temp_code 182 | temp_data["收益方差"] = temp_var 183 | temp_data["股票代码"] = temp_code 184 | # print("测试a", date, temp_data) 185 | temp["收益方差"] = temp_data 186 | returns = returns.append(temp, ignore_index = True) 187 | 188 | # 找到每个交易日收益波动最小的十只股票 189 | top10 = pd.DataFrame(columns = ["日期", "股票代码"]) 190 | dates = rets[0].index 191 | temp_data = [] 192 | m = len(returns) 193 | n = len(returns.iloc[i, :].values[0]) 194 | t = 0 195 | for i in range(len(returns)): 196 | retvar = returns.iloc[i, :].values[0] 197 | # print(dates[i], retvar, type(retvar)) 198 | topvar = [] 199 | topcode = [] 200 | min_index_list = [] 201 | min_index = -1 202 | for k in range(10): 203 | min_var = float("inf") 204 | for j in range(len(retvar)): 205 | t += 1 206 | print("准备数据进程", t/(m*n*10)) 207 | if j in min_index_list: 208 | continue 209 | var_value = retvar.iloc[j, :].收益方差 210 | if min_var > var_value: 211 | min_var = var_value 212 | min_index = j 213 | topvar.append(min_var) 214 | min_index_list.append(min_index) 215 | topcode.append(retvar.iloc[min_index, :].股票代码) 216 | data = pd.Series(topcode, name = dates[i]) 217 | temp_data.append(data) 218 | top10["日期"] = dates 219 | top10["股票代码"] = temp_data 220 | top10.set_index("日期", drop = True, inplace = True) 221 | 222 | top10.to_csv(datafile) 223 | 224 | 225 | # 两个日期之间相差的天数 226 | def gap_days(date1, date2): 227 | return (date1 - date2)/np.timedelta64(1, 'D') 228 | 229 | 230 | # 寻找相关性最高的两只股票 231 | def find_cov(rets, start_date = "20100108", end_date = "20201231"): 232 | codes = rets.index.values 233 | n = len(codes) 234 | i = 0 235 | max_codes = None 236 | max_r = -100.0 237 | for codeA in codes: 238 | for codeB in codes: 239 | print("找股票进程", i/(n*n)) 240 | i += 1 241 | if codeA == codeB: 242 | continue 243 | r = cal_cov(rets[codeA], rets[codeB], start_date = start_date, end_date = "20151231") 244 | if r > max_r: 245 | max_r = r 246 | max_codes = (codeA, codeB) 247 | print("找到候选股票", max_codes, max_r) 248 | 249 | return max_codes, max_r 250 | 251 | 252 | # 计算相关系数 253 | def cal_cov(x_val, y_val, start_date = "20100108", end_date = "20201231"): 254 | x = [] 255 | y = [] 256 | for date in pd.date_range(start = start_date, end = end_date): 257 | try: 258 | a = x_val[date] 259 | except KeyError: 260 | continue 261 | try: 262 | b = y_val[date] 263 | except KeyError: 264 | continue 265 | if np.isnan(a) or np.isnan(b): 266 | continue 267 | x.append(a) 268 | y.append(b) 269 | return np.corrcoef(x, y)[0, 1] 270 | 271 | 272 | # 重新计算数据 273 | def init_data(start_date = "20100108", end_date = "20201231", retry = False): 274 | ts.init_display() 275 | codes = make_pool() 276 | rets = make_data(codes, start_date, end_date, refresh = retry) 277 | # print("测试", rets.head()) 278 | # print(rets.index) 279 | # print(rets[0].index) 280 | # input("按任意键继续") 281 | 282 | codes = rets.index.values 283 | codes, r = find_cov(rets) 284 | return codes 285 | 286 | 287 | # 测试读取数据 288 | @run.change_dir 289 | def test_read_data(): 290 | print("测试读取数据") 291 | datafile = "./datas/retvar.csv" 292 | data = pd.read_csv(datafile) 293 | data.日期 = pd.to_datetime(data.日期) 294 | data.set_index("日期", drop = True, inplace = True) 295 | print(data.info(), data.index) 296 | print(data.iloc[100, :].values[0]) 297 | print(type(data.iloc[100, :].values[0])) 298 | 299 | 300 | @run.change_dir 301 | def pair(): 302 | ts.init_display() 303 | start_date = "20160101" 304 | end_date = "20201231" 305 | codes = [ 306 | ["601186", "601390"], 307 | ["600837", "600030"], 308 | ["600029", "600115"], 309 | ["600000", "601166"], 310 | ["600000", "600036"], 311 | ["600000", "600015"] 312 | ] 313 | for code in codes: 314 | backtest = ts.BackTest( 315 | strategy = PAIRStrategy, 316 | codes = code, 317 | bk_code = "000300", 318 | start_date = start_date, 319 | end_date = end_date, 320 | rf = 0.03, 321 | start_cash = 10000000, 322 | stamp_duty=0.005, 323 | commission=0.0001, 324 | adjust = "hfq", 325 | period = "daily", 326 | refresh = False, 327 | bprint = False, 328 | bdraw = False) 329 | results = backtest.run() 330 | print(code, "回测结果\n", results.年化收益率) 331 | 332 | 333 | # 测试日期索引 334 | @run.change_dir 335 | def test_index(): 336 | datafile = "./datas/cumreturn.csv" 337 | cumreturns = pd.read_csv(datafile) 338 | cumreturns.日期 = pd.to_datetime(cumreturns.日期) 339 | cumreturns.set_index("日期", drop = True, inplace = True) 340 | print(cumreturns.info()) 341 | print(cumreturns.head()) 342 | x = cumreturns.loc["2010-01-08"] 343 | date = datetime.date(2010, 1, 8) 344 | y = cumreturns.loc[str(date)] 345 | print(x) 346 | 347 | 348 | if __name__ == "__main__": 349 | # test_index() 350 | # init_data(retry = False) 351 | pair() 352 | -------------------------------------------------------------------------------- /04SMA.py: -------------------------------------------------------------------------------- 1 | # 实现经典量化策略 2 | # 单移动均线 Signal Moving Average。 3 | # 参考 ZuraKakushadze,JuanAndrésSerur. 151 Trading Strategies. 4 | 5 | 6 | import tradesys as ts 7 | import run 8 | import sys 9 | import akshare as ak 10 | import efinance as ef 11 | import pandas as pd 12 | import numpy as np 13 | import os 14 | import datetime 15 | import backtrader as bt 16 | 17 | 18 | # 策略类 19 | class SMAStrategy(ts.Strategy): 20 | """ 21 | period, 调仓周期 22 | bprint, 是否输出交易过程 23 | """ 24 | params = (("maperiod", 20), 25 | ("bprint", False),) 26 | def __init__(self): 27 | self.close = self.datas[0].close 28 | self.sma = bt.indicators.SimpleMovingAverage(self.datas[0], period = self.p.maperiod) 29 | self.order = None 30 | 31 | def next(self): 32 | if self.order: 33 | return 34 | 35 | if not self.position: 36 | if self.close[0] > self.sma[0]: 37 | cash = self.broker.get_cash() 38 | amount = self.downcast(cash*0.9/self.close[0], 100) 39 | self.order = self.buy(size = amount) 40 | else: 41 | pos = self.getposition() 42 | if self.close[0] < self.sma[0]: 43 | self.order = self.sell(size = pos.size) 44 | if self.is_lastday(data = self.datas[0]): 45 | self.close() 46 | 47 | 48 | # 主函数 49 | @run.change_dir 50 | def sma(): 51 | ts.init_display() 52 | start_date = "20100108" 53 | end_date = "20201231" 54 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 55 | codes = ["000100"] 56 | backtest = ts.BackTest( 57 | strategy = SMAStrategy, 58 | codes = codes, 59 | bk_code = "000001", 60 | start_date = start_date, 61 | end_date = end_date, 62 | rf = 0.03, 63 | start_cash = 10000000, 64 | stamp_duty=0.005, 65 | commission=0.0001, 66 | adjust = "hfq", 67 | period = "daily", 68 | refresh = True, 69 | bprint = False, 70 | bdraw = True) 71 | results = backtest.run() 72 | print("回测结果", results[:-2]) 73 | 74 | 75 | # 调参试试 76 | @run.change_dir 77 | def opt_sma(): 78 | ts.init_display() 79 | start_date = "20100108" 80 | end_date = "20201231" 81 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 82 | codes = ["000100"] 83 | backtest = ts.OptStrategy( 84 | strategy = SMAStrategy, 85 | codes = codes, 86 | bk_code = "000001", 87 | start_date = start_date, 88 | end_date = end_date, 89 | rf = 0.03, 90 | start_cash = 10000000, 91 | stamp_duty=0.005, 92 | commission=0.0001, 93 | adjust = "hfq", 94 | period = "daily", 95 | refresh = False, 96 | bprint = False, 97 | bdraw = False, 98 | maperiod = range(10, 30)) 99 | results = backtest.run() 100 | print("回测结果", results.loc[:,["参数", "年化收益率"]]) 101 | 102 | 103 | # 对整个市场回测 104 | @run.change_dir 105 | def research_sma(): 106 | ts.init_display() 107 | start_date = "20100108" 108 | end_date = "20201231" 109 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 110 | backtest = ts.Research( 111 | strategy = SMAStrategy, 112 | bk_code = "000001", 113 | start_date = start_date, 114 | end_date = end_date, 115 | start_cash = 10000000, 116 | min_len = 2000, 117 | adjust = "hfq", 118 | period = "daily", 119 | refresh = True, 120 | bprint = False, 121 | retest = True, 122 | maperiod = 25) 123 | results = backtest.run() 124 | # print("测试3") 125 | # print(results.info()) 126 | results.sort_values(by = "年化收益率", inplace = True, ascending = False) 127 | 128 | print("回测结果", results.loc[:, ["年化收益率"]]) 129 | 130 | 131 | if __name__ == "__main__": 132 | # sma() 133 | # opt_sma() 134 | research_sma() -------------------------------------------------------------------------------- /05ALMA.py: -------------------------------------------------------------------------------- 1 | # 实现经典量化策略 2 | # 双移动均线 Two Moving Average。 3 | # 用ALMA版本 4 | # 参考 ZuraKakushadze,JuanAndrésSerur. 151 Trading Strategies. 5 | 6 | 7 | import tradesys as ts 8 | import run 9 | import sys 10 | import akshare as ak 11 | import efinance as ef 12 | import pandas as pd 13 | import numpy as np 14 | import os 15 | import datetime 16 | import backtrader as bt 17 | 18 | 19 | # 定义ALMA,参考https://community.backtrader.com/topic/3262/alma-arnaud-legoux-moving-average 20 | class ALMA(bt.Indicator): 21 | lines = ("alma", ) 22 | 23 | params = dict( 24 | period = 40, 25 | sigma = 6, 26 | offset = 1, 27 | ) 28 | 29 | def __init__(self): 30 | self.asize = self.p.period - 1 31 | self.m = self.p.offset * self.asize 32 | self.s = self.p.period / self.p.sigma 33 | self.dss = 2 * self.s * self.s 34 | 35 | def next(self): 36 | try: 37 | wtd_sum = 0 38 | self.l.alma[0] = 0 39 | if len(self) >= self.asize: 40 | for i in range(self.p.period): 41 | im = i - self.m 42 | wtd = np.exp( -(im * im) / self.dss) 43 | self.l.alma[0] += self.data[0 - self.p.period + i] * wtd 44 | wtd_sum += wtd 45 | self.l.alma[0] = self.l.alma[0] / wtd_sum 46 | # print(self.l.alma[0]) 47 | 48 | except TypeError: 49 | self.l.alma[0] = 0 50 | return 51 | 52 | 53 | 54 | # 策略类 55 | class SMAStrategy(ts.Strategy): 56 | """ 57 | T1, 短周期 58 | T2, 长周期 59 | stoprate, 止损位 60 | bprint, 是否输出交易过程 61 | """ 62 | params = (("T1", 10), 63 | ("T2", 20), 64 | ("stoprate", 0.05), 65 | ("bprint", False),) 66 | def __init__(self): 67 | self.close = self.datas[0].close 68 | # self.sma1 = bt.indicators.SimpleMovingAverage(self.datas[0], period = self.p.T1) 69 | # self.sma2 = bt.indicators.SimpleMovingAverage(self.datas[0], period = self.p.T2) 70 | self.sma1 = ALMA(self.close, period = self.p.T1) 71 | self.sma2 = ALMA(self.close, period = self.p.T2) 72 | self.order = None 73 | self.price = 0.0 74 | # 测试用 75 | # print("参数", self.p.T1, self.p.T2, self.p.stoprate) 76 | 77 | def next(self): 78 | if self.order: 79 | return 80 | 81 | print(self.sma1[0], self.sma2[0]) 82 | if not self.position: 83 | if self.sma1 > self.sma2: 84 | cash = self.broker.get_cash() 85 | amount = self.downcast(cash*0.9/self.close[0], 100) 86 | self.order = self.buy(size = amount) 87 | self.price = self.close[0] 88 | else: 89 | pos = self.getposition() 90 | if self.sma1 < self.sma2 or self.close[0] < self.price*(1-self.p.stoprate): 91 | self.order = self.sell(size = pos.size) 92 | self.price = 0.0 93 | if self.is_lastday(data = self.datas[0]): 94 | self.close() 95 | 96 | 97 | # 主函数 98 | @run.change_dir 99 | def tma(): 100 | ts.init_display() 101 | start_date = "20100108" 102 | end_date = "20201231" 103 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 104 | codes = ["000100"] 105 | backtest = ts.BackTest( 106 | strategy = SMAStrategy, 107 | codes = codes, 108 | bk_code = "000001", 109 | start_date = start_date, 110 | end_date = end_date, 111 | rf = 0.03, 112 | start_cash = 10000000, 113 | stamp_duty=0.005, 114 | commission=0.0001, 115 | adjust = "hfq", 116 | period = "daily", 117 | refresh = True, 118 | bprint = False, 119 | bdraw = True) 120 | results = backtest.run() 121 | print("回测结果", results[:-2]) 122 | 123 | 124 | # 调参试试 125 | @run.change_dir 126 | def opt_tma(): 127 | ts.init_display() 128 | start_date = "20100108" 129 | end_date = "20201231" 130 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 131 | codes = ["000100"] 132 | backtest = ts.OptStrategy( 133 | strategy = SMAStrategy, 134 | codes = codes, 135 | bk_code = "000001", 136 | start_date = start_date, 137 | end_date = end_date, 138 | rf = 0.03, 139 | start_cash = 10000000, 140 | stamp_duty=0.005, 141 | commission=0.0001, 142 | adjust = "hfq", 143 | period = "daily", 144 | refresh = False, 145 | bprint = False, 146 | bdraw = False, 147 | num_params = 3, 148 | T1 = range(10, 20), 149 | T2 = range(30, 60), 150 | stoprate = np.arange(0.01, 0.1, 0.01)) 151 | results = backtest.run() 152 | print("回测结果", results.loc[:,["参数", "年化收益率"]]) 153 | 154 | 155 | # 对整个市场回测 156 | @run.change_dir 157 | def research_tma(): 158 | ts.init_display() 159 | start_date = "20100108" 160 | end_date = "20201231" 161 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 162 | backtest = ts.Research( 163 | strategy = SMAStrategy, 164 | bk_code = "000001", 165 | start_date = start_date, 166 | end_date = end_date, 167 | start_cash = 10000000, 168 | min_len = 2000, 169 | adjust = "hfq", 170 | period = "daily", 171 | refresh = False, 172 | bprint = False, 173 | retest = True, 174 | T1 = 16, 175 | T2 = 55, 176 | stoprate = 0.08) 177 | results = backtest.run() 178 | # print("测试3") 179 | # print(results.info()) 180 | results.sort_values(by = "年化收益率", inplace = True, ascending = False) 181 | 182 | print("回测结果", results.loc[:, ["年化收益率"]]) 183 | 184 | 185 | if __name__ == "__main__": 186 | tma() 187 | # opt_tma() 188 | # research_tma() -------------------------------------------------------------------------------- /05TMA.py: -------------------------------------------------------------------------------- 1 | # 实现经典量化策略 2 | # 双移动均线 Two Moving Average。 3 | # 参考 ZuraKakushadze,JuanAndrésSerur. 151 Trading Strategies. 4 | 5 | 6 | import tradesys as ts 7 | import run 8 | import sys 9 | import akshare as ak 10 | import efinance as ef 11 | import pandas as pd 12 | import numpy as np 13 | import os 14 | import datetime 15 | import backtrader as bt 16 | 17 | 18 | # 策略类 19 | class SMAStrategy(ts.Strategy): 20 | """ 21 | T1, 短周期 22 | T2, 长周期 23 | stoprate, 止损位 24 | bprint, 是否输出交易过程 25 | """ 26 | params = (("T1", 10), 27 | ("T2", 20), 28 | ("stoprate", 0.05), 29 | ("bprint", False),) 30 | def __init__(self): 31 | self.close = self.datas[0].close 32 | # self.sma1 = bt.indicators.SimpleMovingAverage(self.datas[0], period = self.p.T1) 33 | # self.sma2 = bt.indicators.SimpleMovingAverage(self.datas[0], period = self.p.T2) 34 | self.sma1 = bt.talib.KAMA(self.close, timeperiod = self.p.T1) 35 | self.sma2 = bt.talib.KAMA(self.close, timeperiod = self.p.T2) 36 | self.order = None 37 | self.price = 0.0 38 | # 测试用 39 | # print("参数", self.p.T1, self.p.T2, self.p.stoprate) 40 | 41 | def next(self): 42 | if self.order: 43 | return 44 | 45 | print(self.sma1[0], self.sma2[0]) 46 | if not self.position: 47 | if self.sma1 > self.sma2: 48 | cash = self.broker.get_cash() 49 | amount = self.downcast(cash*0.9/self.close[0], 100) 50 | self.order = self.buy(size = amount) 51 | self.price = self.close[0] 52 | else: 53 | pos = self.getposition() 54 | if self.sma1 < self.sma2 or self.close[0] < self.price*(1-self.p.stoprate): 55 | self.order = self.sell(size = pos.size) 56 | self.price = 0.0 57 | if self.is_lastday(data = self.datas[0]): 58 | self.close() 59 | 60 | 61 | # 主函数 62 | @run.change_dir 63 | def tma(): 64 | ts.init_display() 65 | start_date = "20100108" 66 | end_date = "20201231" 67 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 68 | codes = ["000100"] 69 | backtest = ts.BackTest( 70 | strategy = SMAStrategy, 71 | codes = codes, 72 | bk_code = "000001", 73 | start_date = start_date, 74 | end_date = end_date, 75 | rf = 0.03, 76 | start_cash = 10000000, 77 | stamp_duty=0.005, 78 | commission=0.0001, 79 | adjust = "hfq", 80 | period = "daily", 81 | refresh = True, 82 | bprint = False, 83 | bdraw = True) 84 | results = backtest.run() 85 | print("回测结果", results[:-2]) 86 | 87 | 88 | # 调参试试 89 | @run.change_dir 90 | def opt_tma(): 91 | ts.init_display() 92 | start_date = "20100108" 93 | end_date = "20201231" 94 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 95 | codes = ["000100"] 96 | backtest = ts.OptStrategy( 97 | strategy = SMAStrategy, 98 | codes = codes, 99 | bk_code = "000001", 100 | start_date = start_date, 101 | end_date = end_date, 102 | rf = 0.03, 103 | start_cash = 10000000, 104 | stamp_duty=0.005, 105 | commission=0.0001, 106 | adjust = "hfq", 107 | period = "daily", 108 | refresh = False, 109 | bprint = False, 110 | bdraw = False, 111 | num_params = 3, 112 | T1 = range(10, 20), 113 | T2 = range(30, 60), 114 | stoprate = np.arange(0.01, 0.1, 0.01)) 115 | results = backtest.run() 116 | print("回测结果", results.loc[:,["参数", "年化收益率"]]) 117 | 118 | 119 | # 对整个市场回测 120 | @run.change_dir 121 | def research_tma(): 122 | ts.init_display() 123 | start_date = "20100108" 124 | end_date = "20201231" 125 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 126 | backtest = ts.Research( 127 | strategy = SMAStrategy, 128 | bk_code = "000001", 129 | start_date = start_date, 130 | end_date = end_date, 131 | start_cash = 10000000, 132 | min_len = 2000, 133 | adjust = "hfq", 134 | period = "daily", 135 | refresh = False, 136 | bprint = False, 137 | retest = True, 138 | T1 = 16, 139 | T2 = 55, 140 | stoprate = 0.08) 141 | results = backtest.run() 142 | # print("测试3") 143 | # print(results.info()) 144 | results.sort_values(by = "年化收益率", inplace = True, ascending = False) 145 | 146 | print("回测结果", results.loc[:, ["年化收益率"]]) 147 | 148 | 149 | if __name__ == "__main__": 150 | tma() 151 | # opt_tma() 152 | # research_tma() -------------------------------------------------------------------------------- /06SR.py: -------------------------------------------------------------------------------- 1 | # 实现经典量化策略 2 | # 支撑与阻挡策略 Support and Resistance。 3 | # 参考 ZuraKakushadze,JuanAndrésSerur. 151 Trading Strategies. 4 | 5 | 6 | import tradesys as ts 7 | import run 8 | import sys 9 | import akshare as ak 10 | import efinance as ef 11 | import pandas as pd 12 | import numpy as np 13 | import os 14 | import datetime 15 | import backtrader as bt 16 | 17 | 18 | # 策略类 19 | class SRStrategy(ts.Strategy): 20 | """ 21 | bprint, 是否输出交易过程 22 | """ 23 | params = ( 24 | ("bprint", False),) 25 | def __init__(self): 26 | self.close = self.datas[0].close 27 | self.H = self.datas[0].high 28 | self.L = self.datas[0].low 29 | self.C = (self.H + self.L + self.close)/3 30 | self.R = 2.0*self.C - self.L 31 | self.order = None 32 | self.price = 0.0 33 | # 测试用 34 | # print("参数", self.p.T1, self.p.T2, self.p.stoprate) 35 | 36 | def next(self): 37 | if self.order: 38 | return 39 | 40 | if not self.position: 41 | if self.close[0] > self.C[0]: 42 | cash = self.broker.get_cash() 43 | amount = self.downcast(cash*0.9/self.close[0], 100) 44 | self.order = self.buy(size = amount) 45 | self.price = self.close[0] 46 | else: 47 | pos = self.getposition() 48 | if self.H[0] >= self.R[0]: 49 | self.order = self.sell(size = pos.size) 50 | self.price = 0.0 51 | if self.is_lastday(data = self.datas[0]): 52 | self.close() 53 | 54 | 55 | # 主函数 56 | @run.change_dir 57 | def sr(): 58 | ts.init_display() 59 | start_date = "20100108" 60 | end_date = "20201231" 61 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 62 | codes = ["000100"] 63 | backtest = ts.BackTest( 64 | strategy = SRStrategy, 65 | codes = codes, 66 | bk_code = "000001", 67 | start_date = start_date, 68 | end_date = end_date, 69 | rf = 0.03, 70 | start_cash = 10000000, 71 | stamp_duty=0.005, 72 | commission=0.0001, 73 | adjust = "hfq", 74 | period = "daily", 75 | refresh = True, 76 | bprint = False, 77 | bdraw = True) 78 | results = backtest.run() 79 | print("回测结果", results[:-2]) 80 | 81 | 82 | # 对整个市场回测 83 | @run.change_dir 84 | def research_sr(): 85 | ts.init_display() 86 | start_date = "20100108" 87 | end_date = "20201231" 88 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 89 | backtest = ts.Research( 90 | strategy = SRStrategy, 91 | bk_code = "000001", 92 | start_date = start_date, 93 | end_date = end_date, 94 | start_cash = 10000000, 95 | min_len = 2000, 96 | adjust = "hfq", 97 | period = "daily", 98 | refresh = True, 99 | bprint = False, 100 | retest = True) 101 | results = backtest.run() 102 | # print("测试3") 103 | # print(results.info()) 104 | results.sort_values(by = "年化收益率", inplace = True, ascending = False) 105 | 106 | print("回测结果", results.loc[:, ["年化收益率"]]) 107 | 108 | 109 | if __name__ == "__main__": 110 | sr() 111 | # research_sr() -------------------------------------------------------------------------------- /07CHANNEL.py: -------------------------------------------------------------------------------- 1 | # 实现经典量化策略 2 | # 通道策略 Channel。 3 | # 参考 ZuraKakushadze,JuanAndrésSerur. 151 Trading Strategies. 4 | 5 | 6 | import tradesys as ts 7 | import run 8 | import sys 9 | import akshare as ak 10 | import efinance as ef 11 | import pandas as pd 12 | import numpy as np 13 | import os 14 | import datetime 15 | import backtrader as bt 16 | 17 | 18 | # 策略类 19 | class CHAStrategy(ts.Strategy): 20 | """ 21 | bprint, 是否输出交易过程 22 | """ 23 | params = ( 24 | ("bprint", False), 25 | ("T", 20), ) 26 | def __init__(self): 27 | self.close = self.datas[0].close 28 | self.H = bt.ind.Highest(self.datas[0].high, period = self.p.T) 29 | self.L = bt.ind.Highest(self.datas[0].high, period = self.p.T) 30 | self.order = None 31 | self.price = 0.0 32 | # 测试用 33 | # print("参数", self.p.T1, self.p.T2, self.p.stoprate) 34 | 35 | def next(self): 36 | if self.order: 37 | return 38 | 39 | if not self.position: 40 | if self.close[0] <= self.L[0]: 41 | cash = self.broker.get_cash() 42 | amount = self.downcast(cash*0.9/self.close[0], 100) 43 | self.order = self.buy(size = amount) 44 | self.price = self.close[0] 45 | else: 46 | pos = self.getposition() 47 | if self.close[0] >= self.H[0]: 48 | self.order = self.sell(size = pos.size) 49 | self.price = 0.0 50 | if self.is_lastday(data = self.datas[0]): 51 | self.close() 52 | 53 | 54 | # 主函数 55 | @run.change_dir 56 | def channel(): 57 | ts.init_display() 58 | start_date = "20100108" 59 | end_date = "20201231" 60 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 61 | codes = ["000100"] 62 | backtest = ts.BackTest( 63 | strategy = CHAStrategy, 64 | codes = codes, 65 | bk_code = "000001", 66 | start_date = start_date, 67 | end_date = end_date, 68 | rf = 0.03, 69 | start_cash = 10000000, 70 | stamp_duty=0.005, 71 | commission=0.0001, 72 | adjust = "hfq", 73 | period = "daily", 74 | refresh = True, 75 | bprint = True, 76 | bdraw = True, 77 | T = 20, ) 78 | results = backtest.run() 79 | print("回测结果", results[:-2]) 80 | 81 | 82 | # 调参试试 83 | @run.change_dir 84 | def opt_channel(): 85 | ts.init_display() 86 | start_date = "20100108" 87 | end_date = "20201231" 88 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 89 | codes = ["000100"] 90 | backtest = ts.OptStrategy( 91 | strategy = CHAStrategy, 92 | codes = codes, 93 | bk_code = "000001", 94 | start_date = start_date, 95 | end_date = end_date, 96 | rf = 0.03, 97 | start_cash = 10000000, 98 | stamp_duty=0.005, 99 | commission=0.0001, 100 | adjust = "hfq", 101 | period = "daily", 102 | refresh = False, 103 | bprint = False, 104 | bdraw = False, 105 | T = range(10, 60)) 106 | results = backtest.run() 107 | print("回测结果", results.loc[:,["参数", "年化收益率"]]) 108 | 109 | 110 | # 对整个市场回测 111 | @run.change_dir 112 | def research_channel(): 113 | ts.init_display() 114 | start_date = "20100108" 115 | end_date = "20201231" 116 | # codes = init_data(start_date = start_date, end_date = end_date, retry = False) 117 | backtest = ts.Research( 118 | strategy = CHAStrategy, 119 | bk_code = "000001", 120 | start_date = start_date, 121 | end_date = end_date, 122 | start_cash = 10000000, 123 | min_len = 2000, 124 | adjust = "hfq", 125 | period = "daily", 126 | refresh = True, 127 | bprint = False, 128 | retest = True, 129 | T = 17) 130 | results = backtest.run() 131 | # print("测试3") 132 | # print(results.info()) 133 | results.sort_values(by = "年化收益率", inplace = True, ascending = False) 134 | 135 | print("回测结果", results.loc[:, ["年化收益率"]]) 136 | 137 | 138 | if __name__ == "__main__": 139 | # channel() 140 | # opt_channel() 141 | # research_channel() 142 | pass -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trade_strategy项目 2 | 使用backtrader实现一些量化策略,对backtrader进行了一些封装。详细内容参见我的博客: https://zwdnet.github.io 3 | 4 | -------------------------------------------------------------------------------- /rsrs.py: -------------------------------------------------------------------------------- 1 | # 计算rsrs 2 | # 来自https://gitee.com/xinyangq/open_test/blob/master/get_rsrs.py 3 | 4 | from scipy.stats import linregress 5 | import tradesys as ts 6 | 7 | # get slope by ordinary least squares(ols) 8 | # x, narray or series 9 | # y, narray or series 10 | # y, narray or series 11 | # slope, float 12 | def get_slope(ind, df): 13 | slope, *args = linregress(df.loc[ind, 'low'], df.loc[ind, 'high']) 14 | return slope 15 | 16 | # get z-score results 17 | # my_series, series, initial data 18 | # ser, float, single result 19 | def get_zscore(my_series): 20 | my_series = (my_series - my_series.mean()) / my_series.std() 21 | ser = my_series.iloc[-1] 22 | return ser 23 | 24 | # get rsrs indicator 25 | # df, DataFrame, including low and high data 26 | # window, int, the size of the window 27 | # rsrs, Series, the initial rsrs indicator 28 | def get_rsrs(df_ini, win_1, win_2): 29 | df=df_ini.copy() 30 | df['ind'] = df.index 31 | print("测试", df) 32 | # get initial rsrs indicator 33 | rsrs_ini = df['ind'].rolling(window=win_1).apply(get_slope, args=(df,)) 34 | # get rsrs indicator 35 | rsrs = rsrs_ini.rolling(window=win_2).apply(get_zscore) 36 | return rsrs 37 | 38 | 39 | if __name__ == '__main__': 40 | my_daily = ts.get_data('510300') 41 | my_slope = get_rsrs(df_ini=my_daily, win_1=18, win_2=25) 42 | print(my_slope) 43 | 44 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | # 将程序上传到服务器上执行 3 | import os 4 | import sys 5 | from functools import wraps 6 | import time 7 | 8 | 9 | # 两个常用的工具函数,装饰器 10 | # 工具函数,在上传到服务器上运行时改变当前目录 11 | def change_dir(func): 12 | @wraps(func) 13 | def change(*args, **kwargs): 14 | oldpath = os.getcwd() 15 | newpath = "/home/code/" 16 | os.chdir(newpath) 17 | r = func(*args, **kwargs) 18 | os.chdir(oldpath) 19 | return r 20 | return change 21 | 22 | 23 | # 工具函数,计算函数运行时间 24 | def timethis(func): 25 | @wraps(func) 26 | def wrapper(*args, **kwargs): 27 | start = time.perf_counter() 28 | r = func(*args, **kwargs) 29 | end = time.perf_counter() 30 | print('{}.{}的运行时间为 : {}秒'.format(func.__module__, func.__name__, end - start)) 31 | return r 32 | return wrapper 33 | 34 | 35 | # 运行代码前准备 36 | def before_run(user, server): 37 | # 创建输出目录 38 | s = "ssh " + user + "@" + server + " \"sudo mkdir ~/code/output\"" 39 | # print("测试4", s) 40 | os.system(s) 41 | # 创建数据目录 42 | s = "ssh " + user + "@" + server + " \"sudo mkdir ~/code/datas\"" 43 | # print("测试4", s) 44 | os.system(s) 45 | # 更改目录权限 46 | s = "ssh root@" + server + " -p 2222 \"chown -R 1000:1000 /home/code/output\"" 47 | os.system(s) 48 | s = "ssh root@" + server + " -p 2222 \"chown -R 1000:1000 /home/code/datas\"" 49 | os.system(s) 50 | # 将本地目录所有文件上传至容器 51 | s = "scp ./* " + user + "@" + server + ":~/code" 52 | # print("测试3", s) 53 | os.system(s) 54 | # 删除md文件,避免影响博客更新 55 | s = "ssh root@" + server + " -p 2222 \"rm /home/code/README.md\"" 56 | os.system(s) 57 | # 更改服务器容器里的当前目录 58 | s = "ssh root@" + server + " -p 2222 \"cd /home/code\"" 59 | os.system(s) 60 | 61 | 62 | # 运行完成后的后续操作 63 | def after_run(user, server): 64 | # 看本地是否有输出目录,没有则新建 65 | if os.path.exists("./output") == False: 66 | s = "mkdir ./output" 67 | os.system(s) 68 | # 看本地是否有数据目录,没有则新建 69 | if os.path.exists("./datas") == False: 70 | s = "mkdir ./datas" 71 | os.system(s) 72 | # 改变文件所有者 73 | s = "ssh root@" + server + " -p 2222 \"chown 1000:1000 -R /home/code/*" 74 | # 将代码目录里所有输出文件传回 75 | s = "scp -r " + user +"@" + server + ":~/code/output/* ./output/" 76 | os.system(s) 77 | # 将代码目录里所有数据文件传回 78 | # s = "scp -r " + user +"@" + server + ":~/code/datas/* ./datas/" 79 | # print("测试5", s) 80 | # os.system(s) 81 | 82 | 83 | # 上传代码至服务器并运行 84 | def run(gpus, user, server): 85 | # 上传本目录所有文件再执行指定文件 86 | if gpus == "all": 87 | before_run(user, server) 88 | # 运行指定代码 89 | s = "ssh root@" + server + " -p 2222 \"python -u /home/code/" + sys.argv[2] + "\"" 90 | # print("测试4", s) 91 | print("正在运行代码……\n") 92 | os.system(s) 93 | after_run(user, server) 94 | elif gpus == "copy": 95 | if sys.argv[2] == "up": 96 | before_run(user, server) 97 | elif sys.argv[2] == "down": 98 | after_run(user, server) 99 | elif sys.argv[2] == "up_data": 100 | s = "scp ./datas/* " + user + "@" + server + ":~/code/datas" 101 | os.system(s) 102 | elif sys.argv[2] == "down_data": 103 | s = "scp " + user + "@" + server + ":~/code/datas/* ./datas/" 104 | os.system(s) 105 | else: 106 | print("输入错误,copy后面要跟up、down、up_data或down_data。") 107 | # 用pytest执行单元测试 108 | elif gpus == "test": 109 | before_run(user, server) 110 | # 运行指定代码 111 | arg_len = len(sys.argv) 112 | if arg_len == 3: 113 | s = "ssh root@" + server + " -p 2222 \"/opt/conda/bin/pytest -v /home/code/" + sys.argv[2] + "\"" 114 | # elif arg_len == 2: 115 | # s = "ssh root@" + server + " -p 2222 \"/opt/conda/bin/pytest -v\"" 116 | else: 117 | print("输入有误!") 118 | return 119 | print("测试4", s) 120 | print("正在测试代码……\n") 121 | os.system(s) 122 | after_run(user, server) 123 | elif gpus == "clean": 124 | # 清除服务器代码目录所有文件 125 | s = "ssh root@" + server + " -p 2222 \"rm -rf /home/code/*\"" 126 | # print("测试1", s) 127 | print("清除服务器代码目录上的文件……\n") 128 | os.system(s) 129 | else: 130 | print("输入有误,格式: python run.py all/copy/test filename.py 其中filename.py为要运行/测试的源文件。") 131 | 132 | 133 | # 主函数 134 | def main(): 135 | gpus = sys.argv[1] 136 | # 读取服务器IP地址,自己编辑serverIP.txt去 137 | with open("serverIP.txt", "rt") as f: 138 | server_list = f.readlines() 139 | for s in server_list: 140 | s = s.replace('\n', '').replace('\r', '') 141 | # print(s) 142 | if s[0] != "#": 143 | res = s.split("@") 144 | username = res[0] 145 | server = res[1] 146 | # print("测试", username, server) 147 | # input("按任意键继续") 148 | run(gpus, username, server) 149 | return 150 | 151 | 152 | if __name__ == "__main__": 153 | main() 154 | 155 | 156 | -------------------------------------------------------------------------------- /tradesys.py: -------------------------------------------------------------------------------- 1 | # 交易系统,封装回测、优化基本过程 2 | 3 | 4 | import backtrader as bt 5 | import quantstats 6 | import akshare as ak 7 | import efinance as ef 8 | import pandas as pd 9 | import numpy as np 10 | import os 11 | import matplotlib.pyplot as plt 12 | import run 13 | import sys 14 | import math 15 | import imgkit 16 | from PIL import Image 17 | from scipy import stats 18 | import empyrical as ey 19 | import itertools 20 | import collections 21 | import datetime 22 | 23 | 24 | 25 | 26 | # 设置显示环境 27 | def init_display(): 28 | pd.set_option('display.max_rows', None) 29 | pd.set_option('display.max_columns', None) 30 | np.set_printoptions(threshold = sys.maxsize) 31 | plt.rcParams['font.sans-serif'] = ['SimHei'] 32 | 33 | 34 | # 获取数据 35 | @run.change_dir 36 | def get_data(code, start_date = "20000101", end_date = "20201231", adjust = "qfq", period = "daily", refresh = False): 37 | def download_data(code): 38 | try: 39 | data = ak.stock_zh_a_hist(symbol = code, start_date = start_date, end_date = end_date, adjust = adjust, period = period) 40 | except KeyError: 41 | if adjust == "qfq": 42 | fqt = 1 43 | elif adjust == "hfq": 44 | fqt = 2 45 | 46 | if period == "daily": 47 | klt = 101 48 | elif period == "weekly": 49 | klt = 102 50 | elif period == "monthly": 51 | klt = 103 52 | data = ef.stock.get_quote_history(code, beg = start_date, end = end_date, fqt = fqt, klt = klt) 53 | data.日期 = pd.to_datetime(data.日期) 54 | data.set_index("日期", drop = False, inplace = True) 55 | return data 56 | 57 | stockfile = "./datas/"+code+".csv" 58 | if os.path.exists(stockfile) and refresh == False: 59 | stock_data = pd.read_csv(stockfile) 60 | stock_data.日期 = pd.to_datetime(stock_data.日期) 61 | stock_data.set_index("日期", drop = False, inplace = True) 62 | # stock_data = stock_data.loc[start_date:datetime.datetime(end_date) + datetime.timedelta(days = 1), :] 63 | end_date = datetime.datetime.strptime(end_date, "%Y%m%d") + datetime.timedelta(days = 1) 64 | stock_data = stock_data.loc[start_date:end_date, :] 65 | else: 66 | stock_data = download_data(code) 67 | if os.path.exists(stockfile): 68 | os.system("rm " + stockfile) 69 | stock_data.to_csv(stockfile) 70 | 71 | return stock_data 72 | 73 | 74 | # A股的交易成本:买入交佣金,卖出交佣金和印花税 75 | class CNA_Commission(bt.CommInfoBase): 76 | params = (('stamp_duty', 0.005), # 印花税率 77 | ('commission', 0.0001), # 佣金率 78 | ('stocklike', True), ('commtype', bt.CommInfoBase.COMM_PERC),) 79 | 80 | def _getcommission(self, size, price, pseudoexec): 81 | if size > 0: 82 | return size * price * self.p.commission 83 | elif size < 0: 84 | return - size * price * (self.p.stamp_duty + self.p.commission) 85 | else: 86 | return 0 87 | 88 | 89 | # 自定义分析器,记录交易成本数据 90 | class CostAnalyzer(bt.Analyzer): 91 | def __init__(self): 92 | self._cost = [] 93 | self.ret = 0.0 94 | 95 | def notify_trade(self, trade): 96 | if trade.justopened or trade.status == trade.Closed: 97 | self._cost.append(trade.commission) 98 | 99 | def stop(self): 100 | super(CostAnalyzer, self).stop() 101 | self.ret = np.sum(self._cost) 102 | 103 | def get_analysis(self): 104 | return self.ret 105 | 106 | 107 | # 策略类基类 108 | class Strategy(bt.Strategy): 109 | def __init__(self): 110 | pass 111 | 112 | def log(self, txt, dt = None): 113 | if self.params.bprint: 114 | dt = dt or self.datas[0].datetime.date(0) 115 | print('%s, %s' % (dt.isoformat(), txt)) 116 | 117 | def notify_order(self, order): 118 | if order.status in [order.Submitted, order.Accepted]: 119 | return 120 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 121 | self.log("交易被拒绝/现金不足/取消") 122 | elif order.status in [order.Completed]: 123 | if order.isbuy(): 124 | self.log('买单执行,%s, %.2f, %i' % (order.data._name, order.executed.price, order.executed.size)) 125 | elif order.issell(): 126 | self.log('卖单执行, %s, %.2f, %i' % (order.data._name, order.executed.price, order.executed.size)) 127 | self.order = None 128 | 129 | def notify_trade(self, trade): 130 | if trade.isclosed: 131 | self.log('毛收益 %0.2f, 扣佣后收益 % 0.2f, 佣金 %.2f, 市值 %.2f, 现金 %.2f'%(trade.pnl, trade.pnlcomm, trade.commission, self.broker.getvalue(), self.broker.getcash())) 132 | 133 | def stop(self): 134 | for i, d in enumerate(self.datas): 135 | pos = self.getposition(d).size 136 | if pos != 0: 137 | # print("关闭", d._name) 138 | self.close() 139 | 140 | # 交易数量取整 141 | def downcast(self, amount, lot): 142 | return abs(amount//lot*lot) 143 | 144 | # 判断是否是最后的交易日 145 | def is_lastday(self,data): 146 | try: 147 | next_next_close = data.close[2] 148 | except IndexError: 149 | return True 150 | except: 151 | print("发生其它错误") 152 | return False 153 | 154 | 155 | # 回测类 156 | class BackTest(): 157 | """ 158 | A股股票策略回测类 159 | strategy 回测策略 160 | codes 回测股票代码列表 161 | start_date 回测开始日期 162 | end_date 回测结束日期 163 | bk_code 基准股票代码 164 | rf 无风险收益率 165 | start_cash 初始资金 166 | stamp_duty 印花税率,单向征收 167 | commission 佣金费率,双向征收 168 | adjust 股票数据复权方式,qfq或hfq 169 | period 股票数据周期(日周月) 170 | refresh 是否更新数据 171 | bprint 是否输出中间结果 172 | bdraw 是否作图 173 | **param 策略参数,用于调参 174 | """ 175 | def __init__(self, strategy, codes, start_date, end_date, bk_code = "000300", rf = 0.03, start_cash = 10000000, stamp_duty=0.005, commission=0.0001, adjust = "hfq", period = "daily", refresh = False, bprint = False, bdraw = False, **param): 176 | self._cerebro = bt.Cerebro() 177 | self._strategy = strategy 178 | self._codes = codes 179 | self._bk_code = bk_code 180 | self._start_date = start_date 181 | self._end_date = end_date 182 | # self._stock_data = stock_data 183 | # self._bk_data = bk_data 184 | self._rf = rf 185 | self._start_cash = start_cash 186 | self._comminfo = CNA_Commission(stamp_duty=0.005, commission=0.0001) 187 | self._adjust = adjust 188 | self._period = period 189 | self._refresh = refresh 190 | self._bprint = bprint 191 | self._bdraw = bdraw 192 | self._param = param 193 | 194 | # 回测前准备 195 | def _before_test(self): 196 | for code in self._codes: 197 | data = get_data(code = code, 198 | start_date = self._start_date, 199 | end_date = self._end_date,adjust = self._adjust, period = self._period, 200 | refresh = self._refresh) 201 | data = self._datatransform(data, code) 202 | self._cerebro.adddata(data, name = code) 203 | self._cerebro.addstrategy(self._strategy, bprint = self._bprint, **self._param) 204 | self._cerebro.broker.setcash(self._start_cash) 205 | self._cerebro.broker.addcommissioninfo(self._comminfo) 206 | 207 | # 数据转换 208 | def _datatransform(self, stock_data, code): 209 | # 生成datafeed 210 | data = bt.feeds.PandasData( 211 | dataname=stock_data, 212 | name=code, 213 | fromdate=stock_data.日期[0], 214 | todate=stock_data.日期[len(stock_data) - 1], 215 | datetime='日期', 216 | open='开盘', 217 | high='最高', 218 | low='最低', 219 | close='收盘', 220 | volume='成交量', 221 | openinterest=-1 222 | ) 223 | return data 224 | 225 | # 增加分析器 226 | def _add_analyzer(self): 227 | self._cerebro.addanalyzer(bt.analyzers.PyFolio, _name='PyFolio') 228 | self._cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name = "TA") 229 | self._cerebro.addanalyzer(bt.analyzers.TimeReturn, _name = "TR") 230 | self._cerebro.addanalyzer(bt.analyzers.SQN, _name = "SQN") 231 | self._cerebro.addanalyzer(bt.analyzers.Returns, _name = "Returns") 232 | self._cerebro.addanalyzer(bt.analyzers.TimeDrawDown, _name = "TimeDrawDown") 233 | self._cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='SharpeRatio', timeframe=bt.TimeFrame.Days, annualize=True, riskfreerate=self._rf) 234 | self._cerebro.addanalyzer(bt.analyzers.SharpeRatio_A, _name='SharpeRatio_A') 235 | self._cerebro.addanalyzer(CostAnalyzer, _name="Cost") 236 | 237 | # 运行回测 238 | def run(self): 239 | self._before_test() 240 | self._add_analyzer() 241 | self._results = self._cerebro.run() 242 | results = self._get_results() 243 | results = results[results.index != "股票代码"] 244 | return results 245 | 246 | # 获取回测结果 247 | def _get_results(self): 248 | # 计算基准策略收益率 249 | self._bk_data = get_data(code = self._bk_code, start_date = self._start_date, end_date = self._end_date, refresh = self._refresh) 250 | bk_ret = self._bk_data.收盘.pct_change() 251 | bk_ret.fillna(0.0, inplace = True) 252 | 253 | if self._bdraw: 254 | self._cerebro.plot(style = "candlestick") 255 | plt.savefig("./output/"+"backtest_result.jpg") 256 | 257 | testresults = self._backtest_result(self._results, bk_ret, rf = self._rf) 258 | end_value = self._cerebro.broker.getvalue() 259 | pnl = end_value - self._start_cash 260 | 261 | testresults["初始资金"] = self._start_cash 262 | testresults["回测开始日期"] = self._start_date 263 | testresults["回测结束日期"] = self._end_date 264 | testresults["期末净值"] = end_value 265 | testresults["净收益"] = pnl 266 | try: 267 | testresults["收益成本比"] = pnl/testresults["交易成本"] 268 | except ZeroDivisionError: 269 | pass 270 | testresults["股票代码"] = self._codes 271 | return testresults 272 | 273 | # 计算回测指标 274 | def _backtest_result(self, results, bk_ret, rf = 0.01): 275 | # 计算回测指标 276 | portfolio_stats = results[0].analyzers.getbyname('PyFolio') 277 | returns, positions, transactions, gross_lev = portfolio_stats.get_pf_items() 278 | returns.index = returns.index.tz_convert(None) 279 | totalTrade = results[0].analyzers.getbyname("TA").get_analysis() 280 | sqn = results[0].analyzers.SQN.get_analysis()["sqn"] 281 | Returns = results[0].analyzers.Returns.get_analysis() 282 | timedrawdown = results[0].analyzers.TimeDrawDown.get_analysis() 283 | sharpe = results[0].analyzers.SharpeRatio.get_analysis() 284 | sharpeA = results[0].analyzers.SharpeRatio_A.get_analysis() 285 | cost = results[0].analyzers.Cost.get_analysis() 286 | 287 | backtest_results = pd.Series() 288 | 289 | backtest_results["总收益率"] = Returns["rtot"] 290 | backtest_results["平均收益率"] = Returns["ravg"] 291 | backtest_results["年化收益率"] = Returns["rnorm"] 292 | backtest_results["交易成本"] = cost 293 | backtest_results["SQN"] = sqn 294 | try: 295 | backtest_results["交易总次数"] = totalTrade["total"]["total"] 296 | backtest_results["盈利交易次数"] = totalTrade["won"]["total"] 297 | backtest_results["盈利交易总盈利"] = totalTrade["won"]["pnl"]["total"] 298 | backtest_results["盈利交易平均盈利"] = totalTrade["won"]["pnl"]["average"] 299 | backtest_results["盈利交易最大盈利"] = totalTrade["won"]["pnl"]["max"] 300 | backtest_results["亏损交易次数"] = totalTrade["lost"]["total"] 301 | backtest_results["亏损交易总亏损"] = totalTrade["lost"]["pnl"]["total"] 302 | backtest_results["亏损交易平均亏损"] = totalTrade["lost"]["pnl"]["average"] 303 | backtest_results["亏损交易最大亏损"] = totalTrade["lost"]["pnl"]["max"] 304 | 305 | # 胜率就是成功率,例如投入十次,七次盈利,三次亏损,胜率就是70%。 306 | # 防止被零除 307 | if totalTrade["total"]["total"] == 0: 308 | backtest_results["胜率"] = np.NaN 309 | else: 310 | backtest_results["胜率"] = totalTrade["won"]["total"]/totalTrade["total"]["total"] 311 | # 赔率是指盈亏比,例如平均每次盈利30%,平均每次亏损10%,盈亏比就是3倍。 312 | # 防止被零除 313 | if totalTrade["lost"]["pnl"]["average"] == 0: 314 | backtest_results["赔率"] = np.NaN 315 | else: 316 | backtest_results["赔率"] = totalTrade["won"]["pnl"]["average"]/abs(totalTrade["lost"]["pnl"]["average"]) 317 | # 计算风险指标 318 | self._risk_analyze(backtest_results, returns, bk_ret, rf = rf) 319 | except KeyError: 320 | pass 321 | 322 | return backtest_results 323 | 324 | # 将风险分析和绘图部分提出来 325 | def _risk_analyze(self, backtest_results, returns, bk_ret, rf = 0.01): 326 | prepare_returns = False # 已经是收益率序列数据了,不用再转换了 327 | # 计算夏普比率 328 | if returns.std() == 0.0: 329 | sharpe = 0.0 330 | else: 331 | sharpe = quantstats.stats.sharpe(returns = returns, rf = rf) 332 | # 计算αβ值 333 | alphabeta = quantstats.stats.greeks(returns, bk_ret, prepare_returns = prepare_returns) 334 | # 计算信息比率 335 | info = quantstats.stats.information_ratio(returns, bk_ret, prepare_returns = prepare_returns) 336 | # 索提比率 337 | sortino = quantstats.stats.sortino(returns = returns, rf = rf) 338 | # 调整索提比率 339 | adjust_st = quantstats.stats.adjusted_sortino(returns = returns, rf = rf) 340 | # skew值 341 | skew = quantstats.stats.skew(returns = returns, prepare_returns = prepare_returns) 342 | # calmar值 343 | calmar = quantstats.stats.calmar(returns = returns, prepare_returns = prepare_returns) 344 | # r2值 345 | r2 = quantstats.stats.r_squared(returns, bk_ret, prepare_returns = prepare_returns) 346 | backtest_results["波动率"] = quantstats.stats.volatility(returns = returns, prepare_returns = prepare_returns) 347 | backtest_results["赢钱概率"] = quantstats.stats.win_rate(returns = returns, prepare_returns = prepare_returns) 348 | backtest_results["收益风险比"] = quantstats.stats.risk_return_ratio(returns = returns, prepare_returns = prepare_returns) 349 | backtest_results["夏普比率"] = sharpe 350 | backtest_results["α值"] = alphabeta.alpha 351 | backtest_results["β值"] = alphabeta.beta 352 | backtest_results["信息比例"] = info 353 | backtest_results["索提比例"] = sortino 354 | backtest_results["调整索提比例"] = adjust_st 355 | backtest_results["skew值"] = skew 356 | backtest_results["calmar值"] = calmar 357 | backtest_results["r2值"] = r2 358 | 359 | # 最大回撤 360 | md = quantstats.stats.max_drawdown(prices = returns) 361 | backtest_results["最大回撤"] = md 362 | 363 | # 生成回测报告 364 | if self._bdraw: 365 | # if True: 366 | self._make_report(returns = returns, bk_ret = bk_ret, rf = rf) 367 | 368 | # 回测报告 369 | def _make_report(self, returns, bk_ret, rf, filename = "report.jpg", title = "回测结果", prepare_returns = False): 370 | # filename = self._code + filename 371 | quantstats.reports.html(returns = returns, benchmark = bk_ret, rf = rf, output='./output/stats.html', title=title, prepare_returns = prepare_returns) 372 | imgkit.from_file("./output/stats.html", "./output/" + filename, options = {"xvfb": ""}) 373 | # 压缩图片文件 374 | im = Image.open("./output/" + filename) 375 | im.save("./output/" + filename) 376 | os.system("rm ./output/stats.html") 377 | 378 | 379 | # 对整个市场的股票进行回测 380 | class Research(): 381 | """ 382 | A股市场回测类 383 | strategy 回测策略 384 | bk_code 基准股票 385 | start_date 回测开始日期 386 | end_date 回测结束日期 387 | highprice 筛选股票池的最高股价 388 | lowprice 筛选股票池的最低股价 389 | min_len 股票数据最小大小(避免新股等) 390 | start_cash 初始资金大小 391 | adjust 数据复权方式 392 | period 数据周期 393 | retest 是否重新回测 394 | refresh 是否更新数据 395 | bprint 是否输出交易过程 396 | bdraw 是否作图 397 | **params 策略参数 398 | """ 399 | def __init__(self, strategy, bk_code, start_date, end_date, highprice = sys.float_info.max, lowprice = 0.0, min_len = 1, start_cash = 10000000, adjust = "hfq", 400 | period = "daily", retest = False, refresh = False, bprint = False, bdraw = True, **params): 401 | self._strategy = strategy 402 | self._start_date = start_date 403 | self._end_date = end_date 404 | self._bk_code = bk_code 405 | self._highprice = highprice 406 | self._lowprice = lowprice 407 | self._min_len = min_len 408 | self._start_cash = start_cash 409 | self._adjust = adjust 410 | self._period = period 411 | self._retest = retest 412 | self._refresh = refresh 413 | self._bprint = bprint 414 | self._bdraw = bdraw 415 | self._params = params 416 | 417 | # 调用接口 418 | def run(self): 419 | self._test() 420 | if self._bdraw: 421 | # print("测试", self._results.info()) 422 | self._draw(self._results) 423 | # print("测试2") 424 | # print(self._results.info()) 425 | return self._results 426 | 427 | # 对回测结果画图 428 | def _draw(self, results): 429 | results.set_index("股票代码", inplace = True) 430 | # 绘图 431 | plt.figure() 432 | results.loc[:, ["SQN", "α值", "β值", "交易总次数", "信息比例", "夏普比率", "年化收益率", "收益成本比", "最大回撤", "索提比例", "胜率", "赔率"]].hist(bins = 100, figsize = (40, 20)) 433 | plt.suptitle("对整个市场回测结果") 434 | plt.savefig("./output/market_test.jpg") 435 | 436 | # 执行回测 437 | def _test(self): 438 | result_path = "./datas/market_test.csv" 439 | if os.path.exists(result_path) and self._retest == False: 440 | self._results = pd.read_csv(result_path)#, dtype = {"股票代码":str}) 441 | return 442 | 443 | self._codes = self._make_pool(refresh = self._refresh) 444 | self._results = pd.DataFrame() 445 | n = len(self._codes) 446 | i = 0 447 | print("回测整个市场……") 448 | for code in self._codes: 449 | i += 1 450 | print("回测进度:", i/n) 451 | data = get_data(code = code, 452 | start_date = self._start_date, 453 | end_date = self._end_date, 454 | refresh = True) 455 | if len(data) <= self._min_len or (data.收盘 < 0.0).sum() > 0: 456 | continue 457 | backtest = BackTest(strategy = self._strategy, codes = [code], bk_code = self._bk_code, start_date = self._start_date, end_date = self._end_date, start_cash = self._start_cash, adjust = self._adjust, period = self._period, refresh = True, bprint = self._bprint, **self._params) 458 | res = backtest.run() 459 | res["股票代码"] = code 460 | self._results = self._results.append(res, ignore_index = True) 461 | # self._results.append(res, ignore_index = True) 462 | # print("测试2", res) 463 | self._results.to_csv(result_path) 464 | # print("测试1") 465 | # print(self._results.info()) 466 | return 467 | 468 | # 形成股票池 469 | def _make_pool(self, refresh = True): 470 | data = pd.DataFrame() 471 | path = "./datas/" 472 | stockfile = path + "stocks.csv" 473 | if os.path.exists(stockfile) and refresh == False: 474 | data = pd.read_csv(stockfile, dtype = {"code":str, "昨日收盘":np.float64}) 475 | else: 476 | stock_zh_a_spot_df = ak.stock_zh_a_spot() 477 | stock_zh_a_spot_df.to_csv(stockfile) 478 | data = stock_zh_a_spot_df 479 | codes = self._select(data) 480 | return codes 481 | 482 | # 对股票数据进行筛选 483 | def _select(self, data, highprice = sys.float_info.max, lowprice = 0.0): 484 | # 对股价进行筛选 485 | smalldata = data[(data.最高 < highprice) & (data.最低 > lowprice)] 486 | # 排除ST个股 487 | smalldata = smalldata[~ smalldata.名称.str.contains("ST")] 488 | # 排除要退市个股 489 | smalldata = smalldata[~ smalldata.名称.str.contains("退")] 490 | 491 | codes = [] 492 | for code in smalldata.代码.values: 493 | codes.append(code[2:]) 494 | 495 | return codes 496 | 497 | 498 | # 对策略进行参数优化 499 | class OptStrategy: 500 | """ 501 | 策略优化类 502 | codes 股票代码列表 503 | bk_code 基准股票代码 504 | strategy 回测策略 505 | start_date 回测开始日期 506 | end_date 回测结束日期 507 | highprice 筛选股票池的最高股价 508 | lowprice 筛选股票池的最低股价 509 | min_len 股票数据最小大小(避免新股等) 510 | start_cash 初始资金大小 511 | adjust 数据复权方式 512 | period 数据周期 513 | retest 是否重新回测 514 | refresh 是否更新数据 515 | bprint 是否输出交易过程 516 | bdraw 是否作图 517 | num_params 调参的参数个数 518 | **params 要调优的参数范围 519 | """ 520 | def __init__(self, codes, strategy, start_date, end_date, bk_code = "000300", min_len = 1, start_cash = 10000000, adjust = "hfq", period = "daily", retest = False, refresh = False, bprint = False, bdraw = True, num_params = 0, **params): 521 | self._codes = codes 522 | self._bk_code = bk_code 523 | self._strategy = strategy 524 | self._start_date = start_date 525 | self._end_date = end_date 526 | self._min_len = min_len 527 | self._start_cash = start_cash 528 | self._adjust = adjust 529 | self._period = period 530 | self._retest = retest 531 | self._refresh = refresh 532 | self._bprint = bprint 533 | self._bdraw = bdraw 534 | self._params = params 535 | self._num_params = num_params 536 | 537 | 538 | # 运行回测 539 | def run(self): 540 | self._results = pd.DataFrame() 541 | optparams = [] 542 | optkeys = list(self._params)[-1] 543 | # 遍历所有参数,初始化回测类,执行回测 544 | params = self._get_params() 545 | for param in params: 546 | backtest = BackTest( 547 | strategy = self._strategy, 548 | codes = self._codes, 549 | start_date = self._start_date, 550 | end_date = self._end_date, 551 | bk_code = self._bk_code, 552 | start_cash = self._start_cash, 553 | adjust = self._adjust, 554 | period = self._period, 555 | refresh = self._refresh, 556 | bprint = self._bprint, 557 | bdraw = self._bdraw, 558 | **param[0]) 559 | res = backtest.run() 560 | # print("测试参数", list(param[0])[-self._num_params:], param, params) 561 | # input("按任意键继续") 562 | self._results = self._results.append(res, ignore_index = True) 563 | #optparams.append(param[0][optkeys]) 564 | param_keys = list(param[0])[-self._num_params:] 565 | param_results = dict() 566 | for key in param_keys: 567 | param_results[key] = param[0][key] 568 | # print(param_results) 569 | # input("按任意键继续") 570 | optparams.append(param_results) 571 | 572 | self._results["参数"] = optparams 573 | self._results.sort_values(by = "年化收益率", inplace = True, ascending = False) 574 | self._draw(self._results) 575 | return self._results 576 | 577 | # 工具函数,提取参数要用,照Backtrader的optstrategy写的。 578 | @staticmethod 579 | def _iterize(iterable): 580 | niterable = list() 581 | for elem in iterable: 582 | if isinstance(elem, str): 583 | elem = (elem,) 584 | elif not isinstance(elem, collections.Iterable): 585 | elem = (elem,) 586 | niterable.append(elem) 587 | return niterable 588 | 589 | # 分析参数列表,提取参数 590 | def _get_params(self): 591 | params = self._params 592 | optkeys = list(params) 593 | vals = self._iterize(params.values()) 594 | optvals = itertools.product(*vals) 595 | okwargs1 = map(zip, itertools.repeat(optkeys), optvals) 596 | optkwargs = map(dict, okwargs1) 597 | it = itertools.product(optkwargs) 598 | return it 599 | 600 | # 对回测结果进行排序 601 | def sort_results(self, results, key, inplace = True, ascending = False): 602 | # print(results) 603 | results.sort_values(by = key, inplace = inplace, ascending = ascending) 604 | # print("测试", results) 605 | return results 606 | 607 | # 对回测结果画图 608 | def _draw(self, results): 609 | # print("测试", results.info()) 610 | # results.set_index("股票代码", inplace = True) 611 | # 绘图 612 | plt.figure() 613 | results.loc[:, ["SQN", "α值", "β值", "交易总次数", "信息比例", "夏普比率", "年化收益率", "收益成本比", "最大回撤", "索提比例", "胜率", "赔率"]].hist(bins = 100, figsize = (40, 20)) 614 | plt.suptitle("策略参数优化结果") 615 | plt.savefig("./output/params_optimize.jpg") 616 | 617 | 618 | if __name__ == "__main__": 619 | pass --------------------------------------------------------------------------------