├── .gitignore ├── FreeBack ├── __init__.py ├── alpha.py ├── barbybar.py ├── debug.py ├── display.py ├── event.py ├── my_pd.py ├── opt.py ├── post.py ├── signal.py └── strat.py ├── LICENSE ├── README.md ├── example ├── alpha.ipynb ├── data.csv.xz ├── data.ipynb └── output │ ├── alpha-Portfolio-Bar.png │ └── alpha-Portfolio-HoldReturn.png ├── setup.py └── upload_pypi.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | *.pyc 4 | *.pyc 5 | *.pyc 6 | -------------------------------------------------------------------------------- /FreeBack/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | from FreeBack import signal, strat, alpha, barbybar,display,event,post,opt,my_pd,debug 3 | -------------------------------------------------------------------------------- /FreeBack/alpha.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | from scipy import stats 4 | import statsmodels.api as sm 5 | import FreeBack as FB 6 | import os 7 | 8 | # 结果文件保存于output中 9 | def check_output(): 10 | if 'output' in os.listdir(): 11 | pass 12 | else: 13 | os.mkdir('output') 14 | 15 | # 该模块包含: 16 | # 因子计算常用函数 17 | # 单因子与多因子检验模块(组合法、回归法) 18 | 19 | 20 | ####################################################################################################### 21 | ####################################### 因子计算常用函数 ################################################# 22 | ####################################################################################################### 23 | # 无特殊说明下 factor,price的格式为pd.Series,其中index的格式为multiindex (date code) 24 | # Rank 25 | # 每日全部index的因子值从小到大排序,均匀映射到(0,1) 26 | # Norm 27 | # 将每日因子值在截面上标准化到 \mu = 0 \sigma = 1 分布 28 | # scale 29 | # 使得截面上因子值满足sum(abs(x)) = 1(a) 30 | # Gauss 31 | # 将每日的因子值转化为正态分布 32 | # resample_fill/select 33 | # 因子降频(日频因子换月频/周频) 34 | # QQ 绘制因子分布的QQ图,观察是否符合正态分布 35 | 36 | 37 | def Rank(factor, norm=False): 38 | # 因子排名 39 | rank = factor.groupby('date').rank() 40 | if norm: 41 | return 2*3**0.5*(rank/(rank.groupby('date').max()+1)-0.5) 42 | return rank/(rank.groupby('date').max()+1) 43 | def Norm(factor): 44 | return (factor - factor.groupby('date').mean())/factor.groupby('date').std() 45 | 46 | def scale(factor, a=1): 47 | return a*factor/factor.groupby('date').apply(lambda x:abs(x).sum()) 48 | # 通过正态分布累计概率函数的逆函数将[p,1-p]的均匀分布转化为正态分布,转换为正态分布后默认产生3sigma内的样本,99.7% p=0.003 49 | def Gauss(factor, p=0.003, slice=False): 50 | # 开启slice选项时为仅转化一个截面启代表仅有一个截面数据 51 | if not slice: 52 | rank = factor.groupby('date').rank() 53 | continuous = p/2+(1-p)*(rank-1)/(rank.groupby('date').max()-1) 54 | def func(ser): 55 | return ser.map(lambda x: stats.norm.ppf(x)) 56 | result = FB.my_pd.parallel(continuous, func) 57 | # 如果所有值相同则替换为0 58 | if_same = result.groupby('date').apply(lambda x: (~x.duplicated()).sum()) 59 | result.loc[if_same[if_same==1].index] = 0 60 | return result 61 | else: 62 | rank = factor.rank() 63 | continuous = p/2+(1-p)*(rank-1)/(rank.max()-1) 64 | return continuous.map(lambda x: stats.norm.ppf(x)) 65 | # 每月、每周内的因子值替换为月初、周初因子值 66 | def resample_fill(factor, freq='month'): 67 | factor.name=0 68 | df = pd.DataFrame(factor) 69 | df['th'] = df.index.map(lambda x:getattr(x[0], freq)) 70 | df['yesterday_th'] = df['th'].groupby('code').shift() 71 | df = df.fillna(getattr(df.index[0][0], freq)) 72 | df['after'] = df.apply(lambda x: x[0] if x.th!=x.yesterday_th else np.nan, axis=1) 73 | df = df.groupby('code').fillna(method='ffill') 74 | df = df.groupby('code').fillna(method='bfill') 75 | return df['after'] 76 | # 直接只选用原数据的周初、月初值 77 | def resample_select(market, freq='month'): 78 | if freq=='month': 79 | # 前一天月份和今天不同,后一天月份和今天相同 80 | market['th'] = market.index.get_level_values(0).map(lambda x: x.month) 81 | elif freq=='week': 82 | market['th'] = market.index.get_level_values(0).map(lambda x: x.week) 83 | return market[(~market['th'].groupby('code').shift().isna())&\ 84 | (market['th'].groupby('code').shift()!=market['th'])&\ 85 | (market['th'].groupby('code').shift(-1)==market['th'])].drop(columns='th') 86 | def QQ(factor, date=None): 87 | plt, fig, ax = FB.display.matplot(w=6, d=4) 88 | if date==None: 89 | norm_dis = pd.Series(np.random.randn(len(factor))).sort_values() 90 | ax.scatter(norm_dis, factor.sort_values()) 91 | else: 92 | norm_dis = pd.Series(np.random.randn(len(factor.loc[date]))).sort_values() 93 | ax.scatter(norm_dis, factor.loc[date].sort_values()) 94 | ax.plot(norm_dis, norm_dis, c='C3', ls='--') 95 | ax.set_title('Q-Q plot') 96 | ax.set_aspect('equal') 97 | plt.show() 98 | 99 | 100 | 101 | ####################################################################################################### 102 | ######################################### 单因子检测 #################################################### 103 | ####################################################################################################### 104 | # 1. 组合法, 计算各因子分层组合的收益及其他特征。 105 | # Porfolio, 因子投资组合表现 106 | # factorgroup 107 | # 2. 回归法, 假设截面上因子与未来收益率线性相关,计算该线性关系的一系列统计量。 108 | # Reg 109 | 110 | ########################################## 组合法 ####################################################### 111 | 112 | class Portfolio(): 113 | # factor 数据类型pd.Series, multiindex(date,code) T日因子值(可以在此排除涨停板) 114 | # price 数据类型pd.Series, multiindex(date,code) T日交易价格(最宽松情况可以使用当日收盘价,\ 115 | # 使用后一天开盘价或者VWAP等是接近实际情况的(全市场数据即可) 116 | # norm 是否按照因子值的排序来对投资组合分组,默认开启。否则使用原始因子值进行分组 117 | # divide 118 | # 数据类型为tuple时,给出全部阈值确定连续分组; 119 | # 数据类型为list, 给出每个分组的前后阈值,例:[(0,0.1),(0.9,1)]表示选取最小10%和最大10%构建组合,前开后闭。 120 | # norm开启时,元素<=1时表示百分比,大于1时表示绝对数量,如(0,0.2)表示持有钱20%,(0,10)表示持有前十只。 121 | # 否则表示因子值的阈值。 122 | # justdivide 是否仅计算给出分组的收益而不计算全市场组合收益,默认计算全市场组合收益(权重由holdweight确定) 123 | # periods (1, 5, 20)同时计算1天5天20天轮动的结果 124 | # holdweight pd.Series, multiindex(data,code) T日的投资组合权重,默认等权 125 | # comm 每次单边换手的交易成本,默认为0 126 | ## 当日收益率为当日收盘价相对昨日收盘价收益率*前一日持仓权重 127 | ## comm 不影响结果,仅仅在result中给出多头费后年化收益率 128 | def __init__(self, factor, price, norm=True, divide=(0, 0.2, 0.4, 0.6, 0.8, 1),\ 129 | justdivide=False, periods=(1, ), \ 130 | holdweight=None, comm=0, returns=None): 131 | self.comm = comm 132 | self.norm = norm 133 | self.justdivide=justdivide 134 | # 一个确定持有张数(不去除停牌),一个确定收益率(去除停牌) 135 | self.price = pd.DataFrame(price.rename('price')).pivot_table('price', 'date' ,'code') 136 | # 每日收益率(当日收盘相比上日收盘) 137 | if returns is None: 138 | returns = self.price/self.price.shift() - 1 139 | self.returns = returns.fillna(0) 140 | # # 先按照截面排序归一化 141 | if norm: 142 | self.factor = Rank(pd.DataFrame(factor.rename('factor'))) 143 | else: 144 | self.factor = pd.DataFrame(factor.rename('factor')) 145 | self.variable = factor 146 | # 组合权重 147 | if type(holdweight) != type(None): 148 | # 没有权重则按0计 149 | holdweight = holdweight.fillna(0) 150 | holdweight = pd.DataFrame(holdweight.rename('weight')).pivot_table('weight', 'date' ,'code') 151 | self.holdweight = holdweight.apply(lambda x: x/x.sum(), axis=1) 152 | else: 153 | self.holdweight = None 154 | # 结果dataframe 行:时间周期 列:IC、ICIR(分组计算的IC、ICIR(非rank))、 多空组合收益、多头收益、 等权收益、 155 | # 考虑换手率多空收益、 夏普、 多空平均换手率 156 | self.result = pd.DataFrame(columns=['group IC', 'group ICIR', 'L&S return', 'L return',\ 157 | 'market return', 'L&S sharpe', 'L sharpe', 'market sharpe', 'real return', \ 158 | 'real sharpe', 'turnover']) 159 | self.result.index.name = 'holding period' 160 | # 运行 161 | self.run(divide, periods) 162 | def run(self, divide, periods): 163 | self.divide = divide 164 | self.periods = periods 165 | # self.a_b表示对因子分隔的阈值,如果是list则直接为a_b 166 | if type(self.divide) == type(list()): 167 | self.a_b = self.divide 168 | # 如果是tuple则转化成list 169 | else: 170 | self.a_b = [(self.divide[i],self.divide[i+1]) for i in range(len(self.divide)-1)] 171 | # 如果justdivide为False,则增加一个可以选中全部标的的组合 172 | if not self.justdivide: 173 | if self.norm == True: 174 | # 按百分比划分 175 | if self.a_b[-1][1]<=1: 176 | self.a_b = self.a_b + [(0,1)] 177 | else: 178 | self.a_b = self.a_b + [(0, 99999)] 179 | else: 180 | self.a_b = self.a_b + [(self.factor.min().values[0], self.factor.max().values[0])] 181 | # 生成持仓表 -> 获得 df_contri(index date columns code) -> 获得净值每日对数收益率 -> 获得换手率 182 | # 全部为矩阵操作 183 | self.matrix_hold() 184 | self.matrix_contri() 185 | self.matrix_lr() 186 | self.matrix_holdn() 187 | self.matrix_turnover() 188 | if not self.justdivide: 189 | self.get_result() 190 | # mat[period][factor range] list[factor range] 191 | # 获得每个持仓周期 每个因子区间的虚拟持仓(只保证比例关系正确) 192 | # a_b factor range from a to b 区间内市值等权重 193 | def matrix_hold(self): 194 | # 每个bar按标的等权配置需要的持仓 195 | if self.norm: 196 | if self.a_b[-1][1]<=1: 197 | look_factor = self.factor.groupby('date').rank(pct=True) 198 | else: 199 | print('整数排序') 200 | look_factor = self.factor.groupby('date').rank() 201 | else: 202 | look_factor = self.factor 203 | look_factor = look_factor.reset_index() 204 | # 选取因子值 满足a_b list中全部条件的 放置于list_hold (前开后闭,与Rank函数返回的(0,1]对应) 205 | bar_hold = [look_factor[(i[0]0: 564 | return [1,1-(x-min_r)/max_r,1-(x-min_r)/max_r] 565 | elif x == 0: 566 | return [1,1,1] 567 | else: 568 | return [1+(x-min_r)/max_r,1,1+(x-min_r)/max_r] 569 | # 570 | plot = np.ones((len(level0s),len(level1s),3)) 571 | plt, fig, ax = FB.display.matplot() 572 | for level0 in range(len(level0s)): 573 | for level1 in range(len(level1s)): 574 | # 先列再行 575 | plot[level0][level1] = color_map(dict_returns[(level0s[level0], level1s[level1])], \ 576 | 0.9*min(dict_returns.values()), 1.1*max(dict_returns.values())) 577 | # 先行再列 578 | ax.text(level1, level0, 579 | round(dict_returns[(level0s[level0], level1s[level1])], 1), 580 | ha='center', va='center') 581 | ax.text(level1, level0, 582 | ' ' + str(int(dict_num[(level0s[level0], level1s[level1])])), 583 | ha='left', va='top', fontsize=10, color='C0') 584 | ax.imshow(plot, aspect='auto') 585 | ax.set(xticks=list(range(len(level1s)))) 586 | ax.set_xticklabels([i for i in level1s]) 587 | ax.set_xlabel(group1) 588 | ax.set(yticks=list(range(len(level0s)))) 589 | ax.set_yticklabels([i for i in level0s]) 590 | ax.set_ylabel(group0) 591 | ax.set_title('双因子分组') 592 | ax.grid(False) 593 | plt.savefig('MutiFactorGroup.png') 594 | 595 | 596 | 597 | ######################################################### 回归法 #################################################################### 598 | 599 | 600 | 601 | # 截面一元线性回归 602 | def cal_CrossReg(df, x_name, y_name, series=False): 603 | name = y_name + '-' + x_name + '--alpha' 604 | 605 | # 使用sm模块 606 | result = df.groupby('date', sort=False).apply(lambda d: sm.OLS(d[y_name], sm.add_constant(d[x_name])).fit()) 607 | 608 | # 如果d[x_name]中所有数相同为C且不为零,这时params中没有const,x_name为d[y_name].mean()/C 609 | # rsquared为0 610 | # 当d[x_name]全为0时,params['const']为0,params[x_name]为d[y_name].mean() 611 | # rsquared可能为极小的负数 612 | def func(x, name): 613 | try: 614 | return x.params[name] 615 | except: 616 | print('sm reg warning') 617 | return 0 618 | gamma = result.map(lambda x: func(x, 'const')) 619 | beta = result.map(lambda x: func(x,x_name)) 620 | r = result.map(lambda x: np.sign(func(x, x_name))*np.sqrt(abs(x.rsquared))) 621 | 622 | if series: 623 | return beta, gamma, r 624 | else: 625 | df[name] = df.groupby('date').apply(lambda x: x[y_name] - beta[x.index[0][0]]*x[x_name] - gamma[x.index[0][0]]).values 626 | return df 627 | 628 | # 回归法 629 | # 此处回归获得的因子收益率即为回归的斜率,对应的是等权做多/做空因子值(标准化后) 630 | # 大于+/小于-0.302或者因子值最大/最小38.13%(61.87%)的组合收益 631 | # 直接market_factor标准的market以及因子column名 632 | class Reg(): 633 | # factor_name为IC_series列名 634 | def __init__(self, factor, price, periods=(1, 5, 20), factor_name = 'alpha0', \ 635 | gauss=False, point=True): 636 | #import time 637 | #start = time.time() 638 | self.price = pd.DataFrame(price.rename('price')).pivot_table('price', 'date' ,'code') 639 | self.periods = periods 640 | self.point = point 641 | if gauss: 642 | factor = Gauss(factor) 643 | else: 644 | factor = Rank(factor, norm=True) 645 | self.factor = factor 646 | self.factor.name = factor_name 647 | factor = pd.DataFrame(factor.rename('factor')) 648 | # 输出结果 列:IC绝对值均值, IC均值, ICIR, 年化因子收益率, 年化夏普, 年化换手, 649 | # 交易成本万3\10\30 650 | # 行:时间周期 651 | result = pd.DataFrame(columns = ['absIC', 'IC', 'ICIR', 'annual return',\ 652 | 'sharpe', 'turnover',\ 653 | 'comm3_r', 'comm3_s', 'comm10_r', 'comm10_s']) 654 | result.index.name='period' 655 | # 多周期IC\因子收益率序列 656 | IC_dict = {} 657 | #rankIC_dict = {} 658 | fr_dict = {} 659 | # 每日回归截距 660 | gamma_dict = {} 661 | cross_dict = {} 662 | # 多空单位因子收益率组合平均换手率 663 | turnover_dict = {} 664 | #print(time.time()-start) 665 | #start = time.time() 666 | for period in self.periods: 667 | if point: # 预测因子出现之后间隔n期的收益率 668 | returns = ((self.price.shift(-1) - self.price)/self.price).shift(1-period) 669 | else: # 预测收益率 预测n期内收益率 670 | returns = (self.price.shift(-period) - self.price)/self.price 671 | returns = returns.reset_index().melt(id_vars=['date']).\ 672 | sort_values(by='date').set_index(['date','code']).dropna() 673 | # 合并df 674 | df_corr = pd.concat([factor, returns], axis=1).dropna() 675 | cross_dict[period] = df_corr 676 | #print(time.time()-start) 677 | #start = time.time() 678 | ## 计算IC序列 679 | beta, gamma, r = cal_CrossReg(df_corr, 'factor', 'value', True) 680 | gamma_dict[period] = gamma 681 | #print(time.time()-start) 682 | #start = time.time() 683 | # 因子指标 684 | #rankIC_dict[period] = df_corr.groupby('date').corr(method='spearman')['factor'].loc[:, 'value'] 685 | #rankIC = rankIC_dict[period].mean() 686 | IC_dict[period] = r 687 | IC = r.mean() 688 | ICIR = IC/r.std() 689 | absIC = (abs(r)).mean() 690 | # 因子收益率(单位预测周期 1day) 691 | if point: 692 | fr_dict[period] = beta 693 | fr = beta.mean() 694 | else: 695 | fr_dict[period] = beta/period 696 | fr = beta.mean()/period 697 | frIR = np.sqrt(period)*fr/beta.std() 698 | # 换手率 699 | # 多头组合成分 700 | factor_L = self.factor[self.factor>0.302].copy() 701 | name = factor_L.name 702 | factor_L = factor_L.reset_index() 703 | factor_L[name] = 1 704 | # 组合权重 705 | weight_L = factor_L.pivot_table(name, 'date', 'code') 706 | weight_L = weight_L.div(weight_L.sum(axis=1), axis='rows').fillna(0) 707 | # 如果未调整period日后的组合权重 708 | noadjust_weight = (weight_L.shift(period)*(self.price/self.price.shift(period)))[weight_L.columns] 709 | noadjust_weight = noadjust_weight.div(noadjust_weight.sum(axis=1), axis='rows').fillna(0) 710 | turnover_L = (abs(weight_L-noadjust_weight).sum(axis=1)).mean()/period 711 | # 空头组合成分 712 | factor_S = self.factor[self.factor<-0.302].copy() 713 | name = factor_S.name 714 | factor_S = factor_S.reset_index() 715 | factor_S[name] = 1 716 | # 组合权重 717 | weight_S = factor_S.pivot_table(name, 'date', 'code') 718 | weight_S = weight_S.div(weight_S.sum(axis=1), axis='rows').fillna(0) 719 | # 如果未调整period日后的组合权重 720 | noadjust_weight = (weight_S.shift(period)*(self.price/self.price.shift(period)))[weight_S.columns] 721 | noadjust_weight = noadjust_weight.div(noadjust_weight.sum(axis=1), axis='rows').fillna(0) 722 | turnover_S = (abs(weight_S-noadjust_weight).sum(axis=1)).mean()/period 723 | turnover = ((turnover_S+turnover_L)/2).mean()*250 724 | turnover_dict[period] = turnover 725 | # 费后收益及夏普 726 | comm3_return = ((1+250*fr)*(1-3/1e4)**turnover-1) 727 | comm3_sharpe = comm3_return/(np.sqrt(250)*beta.std()) 728 | comm10_return = ((1+250*fr)*(1-10/1e4)**turnover-1) 729 | comm10_sharpe = comm10_return/(np.sqrt(250)*beta.std()) 730 | #record = {'absIC':round(absIC*100,1), 'IC':round(IC*100,1), 'rankIC':round(100*rankIC,1),\ 731 | record = {'absIC':round(absIC*100,1), 'IC':round(IC*100,1),\ 732 | 'ICIR':round(10*ICIR,1), \ 733 | 'annual return':round(250*fr*100,1), \ 734 | 'sharpe':round(np.sqrt(250)*frIR,1), 'turnover':round(turnover,1),\ 735 | 'comm3_r':round(100*comm3_return,1), 'comm3_s':round(comm3_sharpe,1),\ 736 | 'comm10_r':round(100*comm10_return,1), 'comm10_s':round(comm10_sharpe,1)} 737 | result.loc[period] = record 738 | #print(time.time()-start) 739 | #start = time.time() 740 | self.IC_dict = IC_dict 741 | self.fr_dict = fr_dict 742 | self.cross_dict = cross_dict 743 | self.gamma_dict = gamma_dict 744 | self.result = result 745 | display(result) 746 | # 因子收益率 747 | def factor_return(self, period=1, rolling_period=20): 748 | plt, fig, ax = FB.display.matplot() 749 | cumsum_fr = 250*self.fr_dict[period].cumsum() 750 | ax.plot(cumsum_fr, label='累计因子收益率', c='C0') 751 | ax.plot(cumsum_fr.rolling(20).min(),\ 752 | alpha=0.5, c='C2') 753 | ax.plot(cumsum_fr.rolling(20).max(),\ 754 | alpha=0.5, c='C3') 755 | ax.legend(loc='lower left') 756 | ax.legend(bbox_to_anchor=(0.17, 1.06), loc=10, ncol=1) 757 | ax2 = ax.twinx() 758 | ax2.plot(250*self.fr_dict[period].rolling(rolling_period).mean(), label='滚动因子收益率(右)', c='C1') 759 | #ax2.legend(loc='lower right') 760 | ax2.legend(bbox_to_anchor=(0.78, 1.06), loc=10, ncol=1) 761 | ax.set_xlim(self.factor.index[0][0], self.factor.index[-1][0]) 762 | plt.show() 763 | # 累计因子收益率和累计的absIC 764 | def alphabeta(self, period=1): 765 | plt, fig, ax = FB.display.matplot() 766 | cumsum_fr = 250*self.fr_dict[period].cumsum() 767 | ax.plot(cumsum_fr, label='累计因子收益率', c='C0') 768 | ax.plot(cumsum_fr.rolling(20).min(),\ 769 | alpha=0.5, c='C2') 770 | ax.plot(cumsum_fr.rolling(20).max(),\ 771 | alpha=0.5, c='C3') 772 | ax.legend(loc='lower left') 773 | ax.legend(bbox_to_anchor=(0.17, 1.06), loc=10, ncol=1) 774 | ax2 = ax.twinx() 775 | ax2.plot(abs(self.IC_dict[period]).cumsum(), label='累计absIC(右)', c='C1') 776 | #ax2.legend(loc='lower right') 777 | ax2.legend(bbox_to_anchor=(0.78, 1.06), loc=10, ncol=1) 778 | ax.set_xlim(self.factor.index[0][0], self.factor.index[-1][0]) 779 | plt.show() 780 | # 截面因子与收益率(散点图) n为分级靠档组数 781 | def cross(self, date=None, period=1, n=100): 782 | plt, fig, ax = FB.display.matplot() 783 | df_corr = self.cross_dict[period].copy() 784 | if self.point: 785 | beta = self.fr_dict[period] 786 | else: 787 | beta = self.fr_dict[period]*period 788 | gamma = self.gamma_dict[period] 789 | r = self.IC_dict[period] 790 | if type(date)==type(None): 791 | # 如果因子值少于n个(因子值重复过多)则不需要分级靠档 792 | # 因子值按分位数分级靠档为n组 793 | if len(df_corr['factor'].unique()) 0: 308 | #self.log('buy '+ code + ' ' + str(deltaamount)) 309 | buy_vol[code] = deltaamount/self.cur_market[trader.price].loc[code] 310 | else: 311 | # 全部卖出 312 | if target_amount[code] == 0: 313 | #self.log('close '+ code) 314 | sell_vol[code] = self.cur_hold_vol[code] 315 | else: 316 | #self.log('sell '+ code + ' ' + str(deltaamount)) 317 | sell_vol[code] = -deltaamount/self.cur_market[trader.price].loc[code] 318 | for code in sell_vol.keys(): 319 | self.sell(code, sell_vol[code], trader.price) 320 | for code in buy_vol.keys(): 321 | self.buy(code, buy_vol[code], trader.price) 322 | # 等权/weight加权持有目标标的 normal 是否将权重归一化,如果归一化则无法空仓 323 | # ifall 是否认为这是全部权重(如果持仓中出现不在权重中的代码是否清仓) 324 | def trade_batch(self, weight, price='open', normal=False, ifall=True): 325 | trader = Trader('batch_trader', price) 326 | trader.ifall = ifall 327 | if normal: 328 | weight = weight/weight.sum() 329 | trader.weight = weight 330 | self.sub_trader(trader) 331 | def runtrade_batch(self, trader): 332 | # 按照执行价格计算的总资产 333 | net = (self.cur_hold_vol*self.cur_market[trader.price]).sum() + self.cur_cash 334 | ## 平均每标的的持有金额 335 | #amount = net/len(trader.code_list) 336 | # 提交订单 337 | if trader.ifall: 338 | # 不在code_list中的直接清仓 339 | for code in self.cur_hold_vol.index: 340 | if code not in trader.weight.index: 341 | self.sell(code, price=trader.price) 342 | # 在code_list中的补齐至amount 343 | sell_list = [] 344 | buy_list = [] 345 | for code, w in trader.weight.items(): 346 | amount = w*net 347 | # 当前权重与目标权重差距 348 | # 如果不在市场中,则卖出1e9 349 | try: 350 | delta = (amount - self.df_hold.loc[self.cur_bar][code]*self.cur_market.loc[code][trader.price])\ 351 | /self.cur_market.loc[code][trader.price] 352 | except: 353 | delta = -1e9 354 | if delta <= 0: 355 | sell_list.append((code, -delta)) 356 | else: 357 | buy_list.append((code, delta)) 358 | # 先卖后买 359 | for task in sell_list: 360 | self.sell(task[0], task[1], trader.price) 361 | for task in buy_list: 362 | self.buy(task[0], task[1], trader.price) 363 | # 替换现有持仓 364 | def trade_exchange(self, exchange, price='open', ifshowhand=True): 365 | trader = Trader('exchange_trader', price) 366 | trader.exchange = exchange 367 | trader.ifshowhand = ifshowhand 368 | self.sub_trader(trader) 369 | def runtrade_exchange(self, trader): 370 | cash = 0 371 | for code in trader.exchange[0]: 372 | self.sell(code, self.cur_hold_vol[code], trader.price) 373 | cash += self.cur_hold_vol[code]*self.cur_market[trader.price][code] 374 | if trader.ifshowhand: 375 | cash += self.cur_cash 376 | for code in trader.exchange[1]: 377 | self.buy(code, \ 378 | cash/len(trader.exchange[1])/self.cur_market[trader.price][code],\ 379 | trader.price) 380 | 381 | # 买入持有固定时间 382 | def trade_buyhold(self, code, vol, holdtime): 383 | trader = Trader('buyhold_trader') 384 | trader.code = code 385 | trader.vol = vol 386 | trader.holdtime =holdtime 387 | #self.queue_trader.put(trader) 388 | self.sub_trader(trader) 389 | def runtrade_buyhold(self, trader): 390 | # 如果还没有订单编号(未执行),则执行买单 391 | if trader.order_id == None: 392 | trader.order_id = self.unique 393 | self.buy(trader.code, trader.vol) 394 | return True 395 | else: 396 | excutelog = self.df_excute.loc[trader.order_id] 397 | dealdate = excutelog['date'] 398 | occurance_vol = excutelog['occurance_vol'] 399 | if (self.cur_bar-dealdate)>=trader.holdtime: 400 | self.sell(trader.code, occurance_vol) 401 | return False 402 | else: 403 | return True 404 | def trade_buystop(self, code, amount): 405 | pass 406 | 407 | # 交易员 408 | def runtrader(self): 409 | # 执行全部queue_trader中trader 410 | savetrader = [] 411 | while not self.queue_trader.empty(): 412 | trader = self.queue_trader.get() 413 | if trader.type == 'amount_trader': 414 | self.runtrade_amount(trader) 415 | elif trader.type == 'batch_trader': 416 | self.runtrade_batch(trader) 417 | elif trader.type == 'exchange_trader': 418 | self.runtrade_exchange(trader) 419 | elif trader.type == 'buyhold_trader': 420 | if self.runtrade_buyhold(trader): 421 | savetrader.append(trader) 422 | else: 423 | pass 424 | # 立即处理交易员订单 425 | while not self.queue_order.empty(): 426 | # 接收订单 427 | order = self.queue_order.get() 428 | self.excute(order) 429 | #for i in savetrader: 430 | ## self.queue_trader.put(i) 431 | ## 执行全部stack_trader中的trader 432 | #savetrader = [] 433 | #while len(self.stack_trader)!=0: 434 | # trader = self.stack_trader.pop() 435 | # if trader.type == 'amount_trader': 436 | # self.runtrade_amount(trader) 437 | # elif trader.type == 'batch_trader': 438 | # self.runtrade_batch(trader) 439 | # elif trader.type == 'buyhold_trader': 440 | # if self.runtrade_buyhold(trader): 441 | # savetrader.append(trader) 442 | # else: 443 | # pass 444 | for i in savetrader: 445 | self.stack_trader.append(i) 446 | # 执行订单部分 447 | # @staticmethod 448 | def rounding(self, vol, code): 449 | # 获取order.code 的类型 450 | try: 451 | code_type = self.type_dic[code] 452 | except: 453 | code_type = self.type_dic['all_code'] 454 | # 订单取整 455 | try: 456 | code_unit = self.unit_dic[code_type] 457 | except: 458 | code_unit = self.unit_dic['other'] 459 | print('注意!{}的合约乘数未知,默认是{}'.format((code, code_unit))) 460 | if code_unit < 1e-5: 461 | return vol 462 | else: 463 | return vol - vol%code_unit 464 | 465 | # 接收订单对象执行 466 | def excute(self, order): 467 | # 保证order.code在当前可交易code中 468 | inmarket = True 469 | try: 470 | # 停牌 471 | if self.cur_market['vol'].loc[order.code]==0: 472 | self.log_warning('try excute sus----code: %s, unique:%s'%(order.code, self.unique+1)) 473 | try: 474 | remain_vol = self.cur_hold_vol.loc[order.code] 475 | except: 476 | remain_vol = 0 477 | try: 478 | remain_amount = self.cur_hold_amount.loc[order.code] 479 | except: 480 | remain_amount = 0 481 | # 交割单 482 | excute_log = {'date':self.cur_bar, 'code':order.code, 'BuyOrSell':order.type, 483 | 'price':0, 'occurance_vol':0, 'occurance_amount':0, 484 | 'comm':0, 'remain_vol':remain_vol, 'remain_amount':remain_amount, 485 | 'remain_cash': self.cur_cash, 'stat':'sus code', 486 | 'orderprice':order.price, 'ordervol':order.vol} 487 | # update 488 | self.update_order(order.order_id, excute_log) 489 | inmarket = False 490 | except: 491 | # 没有找到code不发生交易 492 | self.log_error('excute 404----code: %s, unique:%s'%(order.code, self.unique+1)) 493 | try: 494 | remain_vol = self.cur_hold_vol.loc[order.code] 495 | except: 496 | remain_vol = 0 497 | try: 498 | remain_amount = self.cur_hold_amount.loc[order.code] 499 | except: 500 | remain_amount = 0 501 | # 交割单 502 | excute_log = {'date':self.cur_bar, 'code':order.code, 'BuyOrSell':order.type, 503 | 'price':0, 'occurance_vol':0, 'occurance_amount':0, 504 | 'comm':0, 'remain_vol':remain_vol, 'remain_amount':remain_amount, 505 | 'remain_cash': self.cur_cash, 'stat':'404 code', 506 | 'orderprice':order.price, 'ordervol':order.vol} 507 | self.update_order(order.order_id, excute_log) 508 | inmarket = False 509 | if inmarket: 510 | # price 订单执行价 511 | if order.price == 'split': 512 | # 平均价执行 513 | price = (self.cur_market.loc[order.code]['high'] + self.cur_market.loc[order.code]['low'])/2 514 | #elif order.price == 'open': 515 | # price = self.cur_market.loc[order.code]['open'] 516 | #elif order.price == 'close': 517 | # price = self.cur_market.loc[order.code]['close'] 518 | elif type(order.price)==type(""): 519 | price = self.cur_market.loc[order.code][order.price] 520 | else: 521 | # 限价单 522 | price = order.price 523 | 524 | # vol 成交量, stat 状态 525 | # 当前bar最大成交 526 | max_vol1 = self.max_vol_perbar * self.cur_market.loc[order.code]['vol'] 527 | # 买入并且最低价不高于执行价 528 | if order.type == 'Buy': 529 | # 当前现金cur最大买入量 530 | max_vol0 = self.cur_cash/price 531 | if self.cur_market.loc[order.code]['low'] <= price: 532 | # 现金限制最大成交量(做空也是一倍保证金限制) 533 | if max_vol0 <= max_vol1: 534 | if abs(order.vol) > max_vol0: 535 | if order.vol>0: 536 | vol = self.rounding(max_vol0, order.code) 537 | stat = 'not enough cash' 538 | else: 539 | vol = -self.rounding(max_vol0, order.code) 540 | stat = 'not enough deposit' 541 | else: 542 | vol = self.rounding(order.vol, order.code) 543 | stat = 'normal' 544 | # 当前bar最大成交量限制 545 | else: 546 | if order.vol > max_vol1: 547 | vol = self.rounding(max_vol1, order.code) 548 | stat = 'not enough vol' 549 | else: 550 | vol = self.rounding(order.vol, order.code) 551 | stat = 'normal' 552 | else: 553 | vol = 0 554 | stat = 'price lower than low' 555 | # 卖出并且最高价不低于执行价 556 | if order.type == 'Sell': 557 | # 持仓最大卖出量(可以做空则无此限制) 558 | try: 559 | max_vol2 = self.cur_hold_vol[order.code] 560 | except: 561 | max_vol2 = 0 562 | if self.cur_market.loc[order.code]['high'] >= price: 563 | # 当前持仓数量限制(当持仓为负时也使用此函数平仓卖出) 564 | if abs(max_vol2) <= max_vol1: 565 | if order.vol > abs(max_vol2): 566 | # 可以全部卖出 567 | vol = max_vol2 568 | stat = 'not enough hold' 569 | else: 570 | vol = self.rounding(order.vol, order.code) 571 | stat = 'normal' 572 | # 当前bar最大成交量限制 573 | else: 574 | if order.vol > max_vol1: 575 | vol = self.rounding(max_vol1, order.code) 576 | stat = 'not enough vol' 577 | else: 578 | vol = self.rounding(order.vol, order.code) 579 | stat = 'normal' 580 | else: 581 | vol = 0 582 | stat = 'price higher than high' 583 | 584 | # 执行交易 585 | try: 586 | code_type = self.type_dic[order.code] 587 | except: 588 | code_type = self.type_dic['all_code'] 589 | try: 590 | code_comm = self.comm_dic[code_type] 591 | except: 592 | code_comm = 0 593 | print('注意!未知类型{},手续费按照0处理'.format(code_type)) 594 | if order.type == 'Buy': 595 | # 交易处理完成后现金、持仓变化。 596 | cur_cash_ = self.cur_cash - vol*price 597 | final_vol = self.df_hold.iloc[-1][order.code] + vol 598 | final_amount = final_vol * self.cur_market.loc[order.code]['close'] 599 | if code_type.split('_')[-1] == 'option': 600 | comm_cost = abs(vol)*code_comm 601 | else: 602 | comm_cost = abs(vol*price)*code_comm 603 | cur_cash_ = cur_cash_ - comm_cost 604 | # 订单执行记录 605 | excute_log = {'date':self.cur_bar, 'code':order.code, 'BuyOrSell':order.type, 606 | 'price':price, 'occurance_vol':vol, 'occurance_amount':vol*price, 607 | 'comm':comm_cost, 'remain_vol':final_vol, 'remain_amount':final_amount, 608 | 'remain_cash': cur_cash_, 'stat':stat, 609 | 'orderprice':order.price, 'ordervol':order.vol} 610 | # order.type == ‘Sell' 611 | else: 612 | cur_cash_ = self.cur_cash + vol*price 613 | final_vol = self.df_hold.iloc[-1][order.code] - vol 614 | final_amount = final_vol * self.cur_market.loc[order.code]['close'] 615 | if code_type.split('_')[-1]== 'option' or code_type.split('_')[-1]== 'future': 616 | comm_cost = abs(vol)*code_comm 617 | else: 618 | comm_cost = abs(vol*price)*code_comm 619 | cur_cash_ = cur_cash_ - comm_cost 620 | excute_log = {'date':self.cur_bar, 'code':order.code, 'BuyOrSell':order.type, 621 | 'price':price, 'occurance_vol':vol, 'occurance_amount':vol*price, 622 | 'comm':comm_cost, 'remain_vol':final_vol, 'remain_amount':final_amount, 623 | 'remain_cash': cur_cash_, 'stat':stat, 624 | 'orderprice':order.price, 'ordervol':order.vol} 625 | # 更新World中信息 626 | self.update_order(order.order_id, excute_log) 627 | self.update_cash(cur_cash_) 628 | self.update_hold(order.code, final_vol) 629 | 630 | # XD 股息、债息以及送转股 631 | def dividend(self): 632 | pass 633 | 634 | # 初始化 635 | def init(self): 636 | pass 637 | # 策略 638 | def strategy(self): 639 | pass 640 | 641 | # 检查退市未来函数(未来10天中没有此code) 642 | def future_delist(self, code, n=10): 643 | i = 1 644 | future_date = self.barline[self.bar_n] 645 | while future_date < self.barline[-1] and i<=n: 646 | future_date = self.barline[self.bar_n + i] 647 | i += 1 648 | try: 649 | # 检查此合约 650 | self.market.loc[future_date, code] 651 | except: 652 | self.log_warning('future delist----delist date: %s, code: %s'%(future_date, code)) 653 | return True 654 | return False 655 | # 转债专用 转债退市前2-4周会有公告,所以当出现公告时检查退市没有用到未来数据 656 | def convertible_delist(self, code, n=10): 657 | i = 1 658 | future_date = self.barline[self.bar_n] 659 | while future_date < self.barline[-1] and i<=n: 660 | future_date = self.barline[self.bar_n + i] 661 | i += 1 662 | try: 663 | # 非查无此合约 664 | vol = self.market['vol'].loc[future_date, code] 665 | # 有强赎、退市、兑付公告,则成交量为0代表退市 666 | if vol == 0: 667 | if self.cur_market['announce'].loc[code] == 'Q1' or self.cur_market['announce'].loc[code] == 'F' or self.cur_market['announce'].loc[code] == 'T': 668 | return True 669 | except: 670 | # 当期未退市 671 | try: 672 | # 没有强赎、退市、兑付公告则使用未来函数 673 | if self.cur_market['announce'].loc[code] != 'Q1' and self.cur_market['announce'].loc[code] != 'F' and self.cur_market['announce'].loc[code] != 'T': 674 | self.log_warning('future delist----delist date: %s, code: %s'%(future_date, code)) 675 | return True 676 | except: 677 | return True 678 | return False 679 | 680 | # 回测运行 681 | def run(self): 682 | self.init() 683 | # 遍历每个bar 684 | for bar_ in range(len(self.barline)): 685 | # self.log('%s'%bar_) 686 | # regular 687 | # 当前bar 日期 688 | self.bar_n = bar_ 689 | self.cur_bar = self.barline[bar_] 690 | self.log('new bar') 691 | self.cur_market = self.market.loc[self.cur_bar] 692 | # 更新账户状态 (默认与之前相同,防止没有订单情况) 693 | self.df_hold.loc[self.cur_bar] = self.df_hold.iloc[-1] 694 | self.update_cash(self.cur_cash) 695 | self.update_net() 696 | 697 | # broker处理订单(第一个bar不会处理,此时cur_hold和cur_cash为初始值) 698 | self.log('excute yesterbar and trader order') 699 | while not self.queue_order.empty(): 700 | # 接收订单 701 | order = self.queue_order.get() 702 | self.excute(order) 703 | # 交易员下单并执行 704 | self.log('trader sub order') 705 | self.runtrader() 706 | # 处理分红、股息、送股 707 | #self.dividend() 708 | # 更新过hold之后再次更新净值 709 | self.update_net() 710 | 711 | self.log('run strategy') 712 | # 策略部分 713 | self.strategy() 714 | self.log('end bar') 715 | 716 | # log写入文件 717 | f = open('barbybar_log.txt', 'w') 718 | f.write('Regular log: \n\n\n') 719 | f.write(self.temp_log) 720 | f.write('\n\n\nError log: \n\n\n') 721 | f.write(self.error_log) 722 | f.write('\n\n\nWarning log: \n\n\n') 723 | f.write(self.warning_log) 724 | f.close() 725 | 726 | # 开启作弊 则用当前bar执行交易 727 | def cheat_run(self): 728 | self.init() 729 | # 遍历每个bar 730 | for bar_ in range(len(self.barline)): 731 | # regular 732 | self.bar_n = bar_ 733 | self.cur_bar = self.barline[bar_] 734 | self.log('new bar') 735 | self.cur_market = self.market.loc[self.cur_bar] 736 | # 更新账户状态 (默认与之前相同,防止没有订单情况) 737 | self.df_hold.loc[self.cur_bar] = self.df_hold.iloc[-1] 738 | self.update_cash(self.cur_cash) 739 | self.update_net() 740 | # 策略 741 | self.log('run strategy') 742 | self.strategy() 743 | # 直接处理订单 744 | self.log('excute thisbar order') 745 | # broker处理订单(第一个bar不会处理,此时cur_hold和cur_cash为初始值) 746 | while not self.queue_order.empty(): 747 | # 接收订单 748 | order = self.queue_order.get() 749 | self.excute(order) 750 | # 交易员下单 751 | self.runtrader() 752 | self.update_net() 753 | self.log('end bar') 754 | 755 | # log写入文件 756 | f = open('barbybar_log.txt', 'w') 757 | f.write('Regular log: \n\n\n') 758 | f.write(self.temp_log) 759 | f.write('\n\n\nError log: \n\n\n') 760 | f.write(self.error_log) 761 | f.write('\n\n\nWarning log: \n\n\n') 762 | f.write(self.warning_log) 763 | f.close() 764 | 765 | 766 | 767 | 768 | 769 | -------------------------------------------------------------------------------- /FreeBack/debug.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | 4 | # 函数运行时间装饰器 5 | def check_time(func): 6 | def wrapper(*args, **kwargs): 7 | start_time = time.perf_counter() 8 | result = func(*args, **kwargs) 9 | end_time = time.perf_counter() 10 | excution_time = end_time-start_time 11 | print('函数', func.__name__, '执行时间为:', excution_time) 12 | return result 13 | return wrapper 14 | 15 | # log 函数 16 | def log(*txt): 17 | f = open('log.txt','a+') 18 | write_str = ('\n'+' '*35).join([str(i) for i in txt]) 19 | f.write('%s, %s\n' % (datetime.datetime.now(), write_str)) 20 | f.close() 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FreeBack/display.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import pandas as pd 3 | from mpl_toolkits.axisartist.parasite_axes import HostAxes, ParasiteAxes 4 | import seaborn as sns 5 | from pyecharts import options as opts 6 | from pyecharts.charts import Kline,Bar,Grid,Line 7 | from pyecharts.commons.utils import JsCode 8 | import numpy as np 9 | import datetime, xlsxwriter 10 | 11 | ########################################################### 12 | ######################## 静态图 ########################### 13 | ########################################################### 14 | 15 | 16 | # matplot绘图 17 | def matplot(r=1, c=1, sharex=False, sharey=False, w=13, d=7, hspace=0.3, wspace=0.2): 18 | # don't use sns style 19 | sns.reset_orig() 20 | #plot 21 | #run configuration 22 | plt.rcParams['font.size']=14 23 | plt.rcParams['font.family'] = 'KaiTi' 24 | #plt.rcParams['font.family'] = 'Arial' 25 | plt.rcParams['font.sans-serif']=['Microsoft YaHei'] 26 | plt.rcParams["axes.unicode_minus"]=False #该语句解决图像中的“-”负号的乱码问题 27 | plt.rcParams['axes.linewidth']=1 28 | plt.rcParams['axes.grid']=True 29 | plt.rcParams['grid.linestyle']='--' 30 | plt.rcParams['grid.linewidth']=0.2 31 | plt.rcParams["savefig.transparent"]='True' 32 | plt.rcParams['lines.linewidth']=0.8 33 | plt.rcParams['lines.markersize'] = 1 34 | 35 | #保证图片完全展示 36 | plt.tight_layout() 37 | 38 | #subplot 39 | fig,ax = plt.subplots(r,c,sharex=sharex, sharey=sharey,figsize=(w,d)) 40 | plt.subplots_adjust(left=None, bottom=None, right=None, top=None, hspace = hspace, wspace=wspace) 41 | 42 | plt.gcf().autofmt_xdate() 43 | 44 | return plt, fig, ax 45 | 46 | # 获取月度统计量 47 | # method, 48 | # total_return 一般收益率总收益 49 | # total_lr 对数收益率的总收益 50 | # sum 求和 51 | # mul 乘积 52 | def get_month(ser, method='total_return'): 53 | if ser.name == None: 54 | name = 0 55 | else: 56 | name = ser.name 57 | df = ser.reset_index() 58 | # 筛出同月数据 59 | df['month'] = df['date'].apply(lambda x: x - datetime.timedelta(x.day-1)) 60 | df = df.set_index('month')[name] 61 | # 月度统计量 62 | if method=='total_return': 63 | return (((df+1).groupby('month')).prod()-1)*100 64 | elif method=='total_lr': 65 | return (np.exp(df.groupby('month').sum()) - 1)*100 66 | elif method=='sum': 67 | return df.groupby('month').sum() 68 | elif method=='mul': 69 | return df.groupby('month').prod() 70 | 71 | # 月度数据热力图 72 | # period_value 为pd.Series index month ‘2023-7-1’ value 0: *** 73 | # color_threshold 为红绿色分界点 74 | def month_thermal(period_value, color_threshold=0): 75 | # 数值>0红色,<0为绿色。转化为颜色[R,G,B] 76 | def color_map(x, max_r): 77 | if x>0: 78 | return [1,1-x/max_r,1-x/max_r] 79 | elif x == 0: 80 | return [1,1,1] 81 | else: 82 | return [1+x/max_r,1,1+x/max_r] 83 | # i 年份序号(纵坐标) j 月份序号(横坐标) calendar是值, plot是颜色 84 | def calendar_array(dates, data): 85 | # i, j = zip(*[d.isocalendar()[1:] for d in dates]) 86 | # 年份 月份 array 87 | i, j = zip(*[(d.year,d.month) for d in dates]) 88 | i = np.array(i) - min(i) 89 | j = np.array(j) - 1 90 | # 总年份 91 | ni = max(i) + 1 92 | # 12个月 93 | # 值 矩阵 94 | calendar = np.nan * np.zeros((ni, 12)) 95 | # 颜色值 矩阵 默认白色[1,1,1] 96 | plot = np.ones((ni, 12, 3)) 97 | calendar[i, j] = data 98 | # 绝对值最大为纯红(绿) 99 | max_r = np.abs(data).max().max() 100 | mat_color = [color_map(i-color_threshold,max_r) for i in data] 101 | plot[i,j] = np.array(mat_color) 102 | return i, j, plot, calendar 103 | 104 | # 纵坐标为年份,横坐标为月份, 填充值 105 | dates, data = list(period_value.index), period_value.values 106 | #period_value.iloc[:,0].values 107 | i, j, plot, calendar = calendar_array(dates, data) 108 | # 绘制热力图 109 | plt, fig, ax = matplot() 110 | ax.imshow(plot, aspect='auto') 111 | # 设置纵坐标 年份 112 | i = np.array(list(set(i))) 113 | i.sort() 114 | ax.set(yticks=i) 115 | # 年份 116 | years = sorted(list(set([i.year for i in period_value.index]))) 117 | ax.set_yticklabels(years) 118 | # 设置横坐标 月份 119 | ax.set(xticks=list(range(12))) 120 | ax.set_xticklabels(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 121 | 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']) 122 | # 关闭网格 123 | ax.grid(False) 124 | # 显示数值 125 | for i_ in i: 126 | for j_ in j: 127 | if not np.isnan(calendar[i_][j_]): 128 | ax.text(j_, i_, calendar[i_][j_].round(2), ha='center', va='center') 129 | return plt, fig, ax 130 | ''' 131 | # 月度收益热力图 输入对数收益率的series 132 | def plot_thermal(df_returns): 133 | # 先转化为对数收益率 134 | #df_lr = df_returns.apply(lambda x: np.log(x+1)) 135 | df_lr = df_returns.reset_index() 136 | # 筛出同月数据 137 | df_lr['month'] = df_lr['date'].apply(lambda x: x - datetime.timedelta(x.day-1)) 138 | df_lr = df_lr[['month', (lambda x: 0 if x==None else x)(df_returns.name)]] 139 | df_lr = df_lr.set_index('month') 140 | # 月度收益 % 141 | period_return = (np.exp(df_lr.groupby('month').sum()) - 1)*100 142 | # 收益率转化为颜色[R,G,B] 143 | def color_map(x,max_r): 144 | if x>0: 145 | return [1,1-x/max_r,1-x/max_r] 146 | elif x == 0: 147 | return [1,1,1] 148 | else: 149 | return [1+x/max_r,1,1+x/max_r] 150 | # i 年份序号(纵坐标) j 月份序号(横坐标) calendar是值, plot是颜色 151 | def calendar_array(dates, data): 152 | # i, j = zip(*[d.isocalendar()[1:] for d in dates]) 153 | # 年份 月份 array 154 | i, j = zip(*[(d.year,d.month) for d in dates]) 155 | i = np.array(i) - min(i) 156 | j = np.array(j) - 1 157 | # 总年份 158 | ni = max(i) + 1 159 | # 12个月 160 | # 收益率 矩阵 161 | calendar = np.nan * np.zeros((ni, 12)) 162 | # 颜色值 矩阵 默认白色[1,1,1] 163 | plot = np.ones((ni, 12, 3)) 164 | calendar[i, j] = data 165 | # 正最大收益为纯红 负最大收益为纯绿 166 | max_r = np.abs(data).max().max() 167 | mat_color = [color_map(i,max_r) for i in data] 168 | plot[i,j] = np.array(mat_color) 169 | return i, j, plot, calendar 170 | 171 | # 纵坐标为年份,横坐标为月份, 填充值 172 | dates, data = list(period_return.index), period_return.iloc[:,0].values 173 | i, j, plot, calendar = calendar_array(dates, data) 174 | # 绘制热力图 175 | plt, fig, ax = matplot() 176 | ax.imshow(plot, aspect='auto') 177 | # 设置纵坐标 年份 178 | i = np.array(list(set(i))) 179 | i.sort() 180 | ax.set(yticks=i) 181 | # 年份 182 | years = list(set([i.year for i in df_returns.index])) 183 | ax.set_yticklabels(years) 184 | # 设置横坐标 月份 185 | j = np.array(list(set(j))) 186 | j.sort() 187 | ax.set(xticks=j) 188 | ax.set_xticklabels(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 189 | 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']) 190 | # 关闭网格 191 | ax.grid(False) 192 | # 显示数值 193 | for i_ in i: 194 | for j_ in j: 195 | if not np.isnan(calendar[i_][j_]): 196 | ax.text(j_, i_, calendar[i_][j_].round(2), ha='center', va='center') 197 | 198 | return plt, fig, ax 199 | ''' 200 | 201 | 202 | # excel表 203 | def write_df(df, name, title=True, index=True, col_width={}, row_width={}): 204 | workbook = xlsxwriter.Workbook('%s.xlsx'%name) 205 | worksheet = workbook.add_worksheet() 206 | worksheet.freeze_panes(1, 1) # 冻结首行 207 | # 列宽设置 208 | for k,v in col_width.items(): 209 | worksheet.set_column('%s:%s'%(k,k), v) 210 | # 行高设置 211 | for k,v in row_width.items(): 212 | worksheet.set_row(k, v) 213 | # 格式 214 | general_prop = {'font_size':10, 'align':'center', 'valign':'vcenter', 'text_wrap':True} 215 | format_title = workbook.add_format(dict([(k,general_prop[k]) for k in general_prop]\ 216 | +[('font_size',14), ('bold',True),\ 217 | ('bg_color','#0066ff'), ('font_color','#ffffff')])) 218 | format_text = workbook.add_format(dict([(k,general_prop[k]) for k in general_prop]\ 219 | +[('num_format', '#,##0.0')])) 220 | format_date = workbook.add_format(dict([(k,general_prop[k]) for k in general_prop]\ 221 | +[('num_format', 'yyyy-mm-dd')])) 222 | format_time = workbook.add_format(dict([(k,general_prop[k]) for k in general_prop]\ 223 | +[('num_format', 'yyyy-mm-dd hh:MM')])) 224 | def judge_format(text): 225 | return format 226 | # 标题与序号 227 | if index: 228 | if (type(df.index[0])==type(datetime.date))|\ 229 | (type(df.index[0])==type(pd.to_datetime('2000'))): 230 | if (df.index[0].hour+df.index[0].minute)==0: 231 | # 日期格式 232 | worksheet.write_column("A%s"%(int(title)+1), list(df.index), format_date) 233 | else: 234 | # 精确到分钟 235 | worksheet.write_column("A%s"%(int(title)+1), list(df.index), format_time) 236 | else: 237 | worksheet.write_column("A%s"%(int(title)+1), list(df.index), format_text) 238 | if title: 239 | if df.index.name==None: 240 | worksheet.write(0, 0, '', format_title) 241 | else: 242 | worksheet.write(0, 0, df.index.name, format_title) 243 | worksheet.write_row("B1", list(df.columns), format_title) 244 | elif title: 245 | worksheet.write_row("A1", list(df.columns), format_title) 246 | # Iterate over the data and write it out row by row. 247 | row=int(title) 248 | col=int(index) 249 | for i, r in df.iterrows(): 250 | for j, v in r.items(): 251 | try: 252 | worksheet.write(row, col, v, format_text) 253 | except: 254 | worksheet.write(row, col, '') 255 | col += 1 256 | col = int(index) 257 | row += 1 258 | workbook.close() 259 | 260 | 261 | 262 | ########################################################### 263 | ######################## 动态交互图 ########################### 264 | ########################################################### 265 | 266 | 267 | 268 | 269 | 270 | # plot_data:df格式,index为datetime,必要列为close,high,low,open,vol 271 | # 其他列自由添加,默认绘制于主图 272 | # 其中vol绘制于副图 其他列为指标列,绘制于主图 273 | def plot_kbar(plot_data, title='个股行情'): 274 | # 画图大小 275 | big_width = 1400 276 | big_height = big_width*1000/1800 277 | 278 | # 主图,k线 279 | kbar_data = plot_data[['open', 'close', 'low', 'high']].values.tolist() 280 | kline = ( 281 | Kline() 282 | .add_xaxis([str(i.date()) for i in plot_data.index]) # 日期坐标 283 | .add_yaxis( # k线 284 | "kline", 285 | kbar_data, 286 | itemstyle_opts=opts.ItemStyleOpts( 287 | color="#ec0000", 288 | color0="#00da3c", 289 | border_color="#8A0000", 290 | border_color0="#008F28", 291 | ), 292 | ) 293 | .set_global_opts( 294 | xaxis_opts=opts.AxisOpts(is_scale=True), 295 | yaxis_opts=opts.AxisOpts( 296 | is_scale=True, 297 | splitarea_opts=opts.SplitAreaOpts( 298 | is_show=True, areastyle_opts=opts.AreaStyleOpts(opacity=1) 299 | ), 300 | ), 301 | ## 滑块控制主图和幅图,滑块位置, 滑块起始左边位置,起始右边位置 302 | # 隐藏幅图的滑轨 303 | datazoom_opts=[\ 304 | opts.DataZoomOpts(type_='inside', xaxis_index=[0,1],\ 305 | #pos_top='60%', pos_bottom='65%',\ 306 | range_start=80,\ 307 | range_end=100,)], 308 | ## 鼠标位于图中任意点展示详细信息 309 | tooltip_opts=opts.TooltipOpts( 310 | trigger="axis", 311 | axis_pointer_type="cross", 312 | background_color="rgba(245, 245, 245, 0.8)", 313 | border_width=1, 314 | border_color="#ccc", 315 | textstyle_opts=opts.TextStyleOpts(color="#000"), 316 | ), 317 | ## 318 | #visualmap_opts=opts.VisualMapOpts( 319 | # is_show=False, 320 | # dimension=2, 321 | # series_index=5, 322 | # is_piecewise=True, 323 | # pieces=[ 324 | # {"value": 1, "color": "#00da3c"}, 325 | # {"value": -1, "color": "#ec0000"}, 326 | # ], 327 | # ), 328 | # 在主图显示幅图详情 329 | axispointer_opts=opts.AxisPointerOpts( 330 | is_show=True, 331 | link=[{"xAxisIndex": "all"}], 332 | label=opts.LabelOpts(background_color="#777"), 333 | ), 334 | # 绘制阴影功能 335 | brush_opts=opts.BrushOpts( 336 | x_axis_index="all", 337 | brush_link="all", 338 | out_of_brush={"colorAlpha": 0.1}, 339 | brush_type="lineX", 340 | ), 341 | # 标题 342 | title_opts=opts.TitleOpts(title="%s"%title),) 343 | ) 344 | # 主图上绘制的其他数据 345 | kline_others = (set(plot_data.columns)-set(['close', 'high','low','open','vol'])) 346 | lines_list = [] 347 | for i in kline_others: 348 | lines_list.append((Line() 349 | .add_xaxis(xaxis_data=[str(i.date()) for i in plot_data.index]) 350 | .add_yaxis( 351 | series_name=i, 352 | y_axis=plot_data[i].tolist(), 353 | xaxis_index=1, 354 | yaxis_index=1, 355 | label_opts=opts.LabelOpts(is_show=False),) 356 | )) 357 | # 子图1,成交量 358 | bar = ( 359 | Bar() 360 | .add_xaxis(xaxis_data=[str(i.date()) for i in plot_data.index]) 361 | .add_yaxis( 362 | series_name="vol", 363 | y_axis=plot_data["vol"].tolist(), 364 | xaxis_index=1, 365 | yaxis_index=1, 366 | label_opts=opts.LabelOpts(is_show=False), 367 | itemstyle_opts=opts.ItemStyleOpts( 368 | # 跟随主图颜色 369 | color=JsCode( 370 | """ 371 | function(params) { 372 | var colorList; 373 | if (barData[params.dataIndex][1] > barData[params.dataIndex][0]) { 374 | colorList = '#ef232a'; 375 | } else { 376 | colorList = '#14b143'; 377 | } 378 | return colorList; 379 | } 380 | """ 381 | ) 382 | ), 383 | )\ 384 | .set_global_opts( 385 | xaxis_opts=opts.AxisOpts( 386 | type_="category", 387 | grid_index=1, 388 | axislabel_opts=opts.LabelOpts(is_show=False), 389 | ), 390 | legend_opts=opts.LegendOpts(is_show=False), 391 | )) 392 | # 总图 393 | grid_chart = Grid( 394 | init_opts=opts.InitOpts( 395 | width="%spx"%(big_width), 396 | height="%spx"%(big_height), 397 | animation_opts=opts.AnimationOpts(animation=False), 398 | ) 399 | ) 400 | ## 导入open、close数据到barData改变交易量每个bar的颜色 401 | grid_chart.add_js_funcs("var barData={}".format(plot_data[["open","close"]].values.tolist())) 402 | # 添加主图,副图 403 | for i in lines_list: 404 | overlap_kline = kline.overlap(i) 405 | grid_chart.add( 406 | overlap_kline, 407 | #kline, 408 | grid_opts=opts.GridOpts(pos_left="0%", pos_right="0%", height="60%"), 409 | ) 410 | grid_chart.add( 411 | bar, 412 | grid_opts=opts.GridOpts( 413 | pos_left="0%", pos_right="0%", pos_top="70%", height="20%" 414 | ), 415 | ) 416 | grid_chart.render("个股行情.html") 417 | -------------------------------------------------------------------------------- /FreeBack/event.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import FreeBack as FB 4 | 5 | class Event(): 6 | ''' 7 | 事件驱动测试 8 | 参数格式: 9 | multi——index: date, code 10 | signal: 入场信号,index格式 11 | price: series 12 | before: int, 事件前天数 13 | after: int, 事件后天数 14 | bench_type: zero:零, equal:等权 15 | ''' 16 | def __init__(self, signal, price, before=5, after=30, bench_type='zero',\ 17 | custom_bench=None, n_core=6, fast=True): 18 | self.signal = signal 19 | self.price = price 20 | self.before = before 21 | self.after = after 22 | self.n_core = n_core 23 | self.bench_type = bench_type 24 | self.custom_bench = custom_bench 25 | if fast: 26 | self.fast_init() 27 | else: 28 | self.init_param() 29 | 30 | def sr(self, x): 31 | sr = (x - x.shift(1))/x.shift(1) 32 | return sr 33 | 34 | def init_param(self): 35 | def fun1(x): 36 | r = pd.DataFrame() 37 | for i in cols: 38 | r.loc[:, i] = x.shift(-i) 39 | return r 40 | 41 | self.length = self.before + self.after 42 | self.sr = self.price.groupby('code').apply(lambda x: self.sr(x)).droplevel(0).sort_index(level=0) 43 | # 基准按照等权计算 44 | self.bench_sr = self.sr.groupby('date').mean() 45 | if self.bench_type == 'zero': 46 | self.bench_sr = pd.Series(index=self.bench_sr.index) 47 | self.bench_sr.fillna(0, inplace=True) 48 | elif self.bench_type == 'equal': 49 | self.bench_sr = self.bench_sr 50 | if type(self.custom_bench)==type(None): 51 | self.bench_sr = self.custom_bench.loc[self.bench_sr.index] 52 | self.sr = self.sr - self.bench_sr 53 | self.sr.name = 'sr' 54 | cols = [i-self.before+1 for i in range(self.length)] 55 | self.signal_sr_df = FB.my_pd.parallel_group(self.sr, fun1, n_core=self.n_core).loc[self.signal] 56 | self.number = self.signal_sr_df[0].groupby(level='date').count() 57 | self.bench_net = (self.bench_sr + 1).cumprod() 58 | self.net = (self.signal_sr_df+1).cumprod(axis=1) 59 | 60 | def fast_init(self): 61 | self.length = self.before + self.after 62 | self.sr = (self.price/self.price.groupby('code').shift() - 1).fillna(0) 63 | # 基准收益率,默认为0 64 | self.bench_sr = self.sr.groupby('date').mean() 65 | if self.bench_type == 'zero': 66 | self.bench_sr = pd.Series(index=self.bench_sr.index) 67 | self.bench_sr.fillna(0, inplace=True) 68 | elif self.bench_type == 'equal': 69 | self.bench_sr = self.bench_sr 70 | self.sr = self.sr - self.bench_sr 71 | self.sr.name = 'sr' 72 | # 前后观察收益率 73 | cols = [i-self.before+1 for i in range(self.length)] 74 | signal_sr_df = pd.concat([self.sr.groupby('code').shift(-i) for i in cols], axis=1) 75 | signal_sr_df.columns = cols 76 | self.signal_sr_df = signal_sr_df.loc[self.signal].copy() 77 | self.signal_sr_df.fillna(0, inplace=True) 78 | # 触发次数 79 | self.number = self.signal_sr_df[0].groupby(level='date').count() 80 | # 净值 81 | self.bench_net = (self.bench_sr + 1).cumprod() 82 | self.net = (self.signal_sr_df+1).cumprod(axis=1) 83 | 84 | # 每日触发信号数量, bench_type zero时没有bench 85 | def draw_turnover(self): 86 | plt0, fig0, ax0 = FB.display.matplot() 87 | ax1 = ax0.twinx() 88 | num = self.number 89 | ax1.plot(num.cumsum(), color='C2', label='累计样本量(右)') 90 | # 触发次数过多的截断 91 | index = num[num > (num.mean() + 5*num.std())].index 92 | num.loc[index] = num.mean() + 5*num.std() 93 | ax0.bar(num.index, num.values, color='grey', label='每日样本量') 94 | #if self.bench_type != 'zero': 95 | #ax1.plot(self.bench_net, color='steelblue', label='基准净值(右)') 96 | #fig0.legend(bbox_to_anchor=(0.5, 0), loc=10, ncol=2) 97 | fig0.legend(loc='lower center', ncol=2) 98 | plt0.show() 99 | 100 | # 每日超额, 事件净值(取均值) 101 | def draw_net(self): 102 | plt0, fig0, ax0 = FB.display.matplot() 103 | sr = self.signal_sr_df.mean(axis=0) 104 | ax0.bar(sr.index, sr.values, width=0.5, label='单日超额', color='darkgoldenrod') 105 | ax1 = ax0.twinx() 106 | net = self.net.mean() 107 | net = net/net.loc[1] 108 | ax1.plot(net, color='crimson', label='累计净值(右)', linewidth=2.0) 109 | ax1.hlines(1, sr.index[0], sr.index[-1], colors='k', linestyles='--') 110 | fig0.legend(loc='lower center', ncol=2) 111 | plt0.show() 112 | 113 | def draw_Kelly(self, direct='long'): 114 | # 信号触发后价格变化结果 115 | trade_result = (self.signal_sr_df.iloc[:, self.before+1:]+1).cumprod(axis=1)-1 116 | # 胜率 117 | winrate = (trade_result>0).sum()/len(trade_result.index) 118 | # 赔率 按最大值 119 | win = (trade_result*(trade_result>0)).replace(0, np.nan).max() 120 | loss = (abs(trade_result)*(trade_result<0)).replace(0, np.nan).max() 121 | odds = win/(loss+win) 122 | # 根据交易多空修改赔率胜率 123 | if direct=='short': 124 | win,loss = loss,win 125 | odds = win/(loss+win) 126 | winrate = 1-winrate 127 | ## Kelly公式确定仓位,负仓位为0 128 | ## 收益率分布 129 | ##plt, fig, ax = post.matplot() 130 | ##sns.histplot(trade_result[2]) 131 | ##plt.show() 132 | position = (winrate*win - (1-winrate)*loss)/(win*loss) 133 | position[position<0] = 0 134 | # 作图 135 | plt, fig, ax = FB.display.matplot() 136 | ax.plot(100*winrate, c='C2', label='胜率') 137 | ax.plot(100*odds, c='C0', label='赔率') 138 | ax1 = ax.twinx() 139 | ax1.plot(position, c='C3', label='最佳仓位') 140 | ax.set_ylabel('(%)') 141 | ax.set_xlabel('bar') 142 | fig.legend(loc='lower center', ncol=3) 143 | plt.show() 144 | 145 | 146 | # 净值累计加减一个方差 147 | def draw_std_net(self): 148 | plt1, fig1, ax1 = FB.display.matplot() 149 | net = self.net.loc[:, 1:] 150 | net_mean = net.mean() 151 | net_up = net_mean + self.net.std() 152 | net_low = net_mean - self.net.std() 153 | ax1.plot(net_mean, color='darkblue', linewidth=2.0, label='均值') 154 | ax1.plot(net_up, color='darkred', linewidth=2.0, label='均值+方差') 155 | ax1.plot(net_low, color='darkgreen', linewidth=2.0, label='均值-方差') 156 | ax1.legend(loc='upper left') 157 | plt1.show() 158 | 159 | # 净值累计最大值&净值最小值 160 | def draw_e_ratio(self): 161 | plt1, fig1, ax1 = FB.display.matplot() 162 | net = self.net.loc[:, 1:] 163 | net_max = net.cummax(axis=1).mean() 164 | net_min = net.cummin(axis=1).mean() 165 | e_ratio = net_max/net_min 166 | ax1.plot(e_ratio, color='darkred', linewidth=2.0) 167 | ax1.set_xlabel('set_xlabel') 168 | plt1.show() 169 | 170 | # 仅一个信号 171 | # i是siganl中第i个信号 172 | def draw_one_signal_net(self, date, code): 173 | plt0, fig0, ax0 = FB.display.matplot() 174 | sr = self.signal_sr_df.loc[date, code] 175 | ax0.bar(sr.index, sr.values, width=0.5, label='单日超额', color='darkgoldenrod') 176 | ax1 = ax0.twinx() 177 | net = self.net.loc[date, code] 178 | net = net/net.loc[1] 179 | ax1.plot(net, color='crimson', label='累计净值', linewidth=2.0) 180 | fig0.legend(loc='lower center') 181 | plt0.show() 182 | 183 | 184 | 185 | 186 | # # 在事件发生后,持有n日后(事件次bar open至+Tbar收盘价(T=0为次bar开盘至收盘的收益)),收益率 187 | # #dict_date: key thscode, value list of date(事件后可以买入的日期) 188 | # # hold_days = list(range(61)) 189 | # #df_price: date thscode open close 190 | # def event_return(dict_date, df_price, hold_days): 191 | # # 建立dataframe 前两列为合约与日期 192 | # columns = ['thscode', 'date'] 193 | # # return_1 代表持有1天,即当天(0)的收盘价与开盘价之比 194 | # for i in hold_days: 195 | # columns.append('return_%d'%(i+1)) 196 | # df_return = pd.DataFrame(columns = columns) 197 | # # 每个合约 198 | # for i in list(dict_date.keys()): 199 | # # 事件日期列表 200 | # list_date = dict_date[i] 201 | # # 如果在行情数据中没有,输出,跳过 202 | # if(i not in df_price.thscode.unique()): 203 | # print('not found',i) 204 | # continue 205 | # # 筛选出此合约行情 206 | # df_ = df_price[df_price.thscode == i] 207 | # # 按日期排序 208 | # df_ = df_.sort_values(by = 'date') 209 | # df_ = df_.reset_index(drop=True) 210 | # # 每一次事件 211 | # for start_date in list_date: 212 | # # 公告日期为实际发布公告日期后次日,在此时可以直接买入 213 | # # 为交易日则直接买入 214 | # if start_date in df_.date.values: 215 | # start = df_[df_.date == start_date] 216 | # # 如果不是交易日则后延 217 | # else: 218 | # # 最多尝试30天 219 | # try_num = 0 220 | # while try_num < 30: 221 | # try_num += 1 222 | # start_date += datetime.timedelta(1) 223 | # if start_date in df_.date.values: 224 | # start = df_[df_.date == start_date] 225 | # break 226 | # # 没有找到则下一个日期或转债 227 | # if(try_num==30): 228 | # print('fail: ', i, start_date) 229 | # continue 230 | # # 持有到end,需存在行情数据 231 | # dur = [start.index[0]+dur_i for dur_i in hold_days if (start.index[0]+dur_i) < len(df_.index)] 232 | # end = df_.loc[dur] 233 | # # 公告日开盘价到持有日收盘价 收益率 234 | # return_list = list((end.close/start.open.values[0]).apply(lambda x: math.log(x))) 235 | # # 字典 value 236 | # dict_values = [i,start_date] 237 | # dict_values.extend(return_list) 238 | # append_dict = dict(zip(columns, dict_values)) 239 | # df_return = df_return.append(append_dict, ignore_index=True) 240 | 241 | # return df_return -------------------------------------------------------------------------------- /FreeBack/my_pd.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import statsmodels.api as sm 4 | from numpy_ext import rolling_apply 5 | from joblib import Parallel, delayed 6 | import copy, math 7 | 8 | # 关于pandas经常使用到的一些操作 以及对现有函数的改进 9 | 10 | # dataframe, 查找的列, 为value时删除,包括np.nan 11 | # return 操作后df 与被删除的df 12 | def drop_row(df, col, value_list): 13 | drop_all = pd.DataFrame(columns = df.columns) 14 | for value in value_list: 15 | # nan特殊处理 16 | if type(value) == type(np.nan): 17 | if np.isnan(value): 18 | # 重新排序 19 | df = df.reset_index(drop = True) 20 | beishanchu = df[df[col].isnull() == True] 21 | df = df.drop(df.index[df[col].isnull() == True].values) 22 | df = df.reset_index(drop = True) 23 | else: 24 | df = df.reset_index(drop = True) 25 | beishanchu = df[df[col] == value] 26 | df = df.drop(df.index[df[col] == value].values) 27 | df = df.reset_index(drop = True) 28 | drop_all = pd.concat([drop_all, beishanchu], ignore_index=True) 29 | return df, drop_all 30 | 31 | # merge的改进, 避免列被重新命名(新加的为a_,并且保持on的列不变 32 | def nmerge(df, df_add, on): 33 | new_name = ['a_' + x for x in df_add.columns] 34 | new_name = dict(zip(df_add.columns,new_name)) 35 | df_add = df_add.rename(columns = new_name) 36 | # 将 on 列 改回 37 | new_name = ['a_' + x for x in on] 38 | new_name = dict(zip(new_name,on)) 39 | # on 40 | df_add = df_add.rename(columns = new_name) 41 | 42 | # merge 43 | return df.merge(df_add, on=on) 44 | 45 | # 将df中所有行按照年份划分,返回一个列表包含各年度的行,从最早开始 46 | def divide_row(df): 47 | first_year = df['date'].min().year 48 | last_year = df['date'].max().year 49 | result = [] 50 | for y in range(first_year, last_year+1): 51 | select = list(map(lambda x: x.year==y, df['date'])) 52 | df_ = df[select] 53 | result.append(df_) 54 | return result 55 | 56 | # 按sortby排序,使用unique_id列第一次出现的行组成新的df 57 | def extract_first(df, unique_id = 'thscode', sortby = 'date'): 58 | df_result = pd.DataFrame(columns = df.columns) 59 | df = df.sort_values(by = sortby) 60 | for i in df[unique_id].unique(): 61 | # 默认按时间第一行 62 | df_ = df[df[unique_id]==i].iloc[0] 63 | df_ = df_.to_frame().T 64 | df_result = pd.concat([df_result, df_], ignore_index = True) 65 | 66 | return df_result 67 | 68 | # 按unique_col将所有combine_col的值合并为所有values的list 69 | def combine_row(df, unique_col='date', combine_col='order_status'): 70 | df_result = pd.DataFrame(columns = list(df.columns)) 71 | for date in list(df[unique_col].unique()): 72 | list_status = list(df[df[unique_col] == date][combine_col].values) 73 | df_ = pd.DataFrame({unique_col:date, combine_col:[list_status]}) 74 | df_result = pd.concat([df_result, df_]) 75 | return df_result 76 | 77 | # x, y 自变量与因变量的列名(单自变量) 78 | # 返回DataFrame 0,1分别为回归系数与截距 79 | def rolling_reg(df, x_name, y_name, n): 80 | # 如果这一字段的df长度小于n,直接返回nan,对应index 81 | if df.shape[0]=tl.iloc[-1])*(x<=tr.iloc[-1])*y).sum() 249 | result = rolling_apply(func, n, x, tl, tr, y) 250 | # 添加 index 251 | result = pd.DataFrame(result, index=df.index) 252 | return result 253 | # 按代码并行 254 | df = copy.deepcopy(df) 255 | # inde必须为 'code'和'date',并且code内部的date排序 256 | df = df.reset_index() 257 | df = df.sort_values(by='code') 258 | df = df.set_index(['code','date']) 259 | df = df.sort_index(level=['code','date']) 260 | # 命名规则 261 | name_r = str(sum) + '_' + 'Sum' + str(n) + '-' + str(key) + ',' + str(threshold_left) + ';' + str(threshold_right) 262 | def func(df): 263 | return rolling_select_sum(df.reset_index('code'), key, threshold_left, threshold_right, sum, n) 264 | df[[name_r]] = parallel_group(df, func, n_core=n_core).values 265 | # 将index变回 date code 266 | df = df.reset_index() 267 | df = df.sort_values(by='date') 268 | df = df.set_index(['date','code']) 269 | df = df.sort_index(level=['date','code']) 270 | return df[name_r] 271 | 272 | 273 | # 获得df中x_name列为自变量 y_name列为因变量的线性回归结果 274 | def cal_reg(df, x_name, y_name, n, parallel=True, n_core=12): 275 | df = copy.deepcopy(df) 276 | # inde必须为 'code'和'date',并且code内部的date排序 277 | df = df.reset_index() 278 | df = df.sort_values(by='code') 279 | df = df.set_index(['code','date']) 280 | df = df.sort_index(level=['code','date']) 281 | # 命名规则 282 | name_beta = str(x_name) + '-' + str(y_name) + '--beta' +str(n) 283 | name_alpha = str(x_name) + '-' + str(y_name) + '--alpha'+str(n) 284 | name_r = str(x_name) + '-' + str(y_name) + '--r'+str(n) 285 | if parallel: 286 | def func(df): 287 | return rolling_reg(df.reset_index('code'), x_name, y_name, n) 288 | df[[name_beta, name_alpha, name_r]] = parallel_group(df, func, n_core=n_core).values 289 | else: 290 | # 回归 去掉二级index中的code 291 | df_reg = df.groupby('code', sort=False).apply(lambda df: rolling_reg(df.reset_index('code'), x_name, y_name, n)) 292 | df[[name_beta,name_alpha, name_r]] = df_reg 293 | # 将index变回 date code 294 | df = df.reset_index() 295 | df = df.sort_values(by='date') 296 | df = df.set_index(['date','code']) 297 | df = df.sort_index(level=['date','code']) 298 | return df 299 | 300 | def cal_corr(df, x_name, y_name, n, parallel=True, n_core=12): 301 | df = copy.deepcopy(df) 302 | # inde必须为 'code'和'date',并且code内部的date排序 303 | df = df.reset_index() 304 | df = df.sort_values(by='code') 305 | df = df.set_index(['code','date']) 306 | df = df.sort_index(level=['code','date']) 307 | # 命名规则 308 | name_r = str(x_name) + '-' + str(y_name) + '--r'+str(n) 309 | if parallel: 310 | def func(df): 311 | return rolling_corr(df.reset_index('code'), x_name, y_name, n) 312 | df[[name_r]] = parallel_group(df, func, n_core=n_core).values 313 | else: 314 | # 回归 去掉二级index中的code 315 | df_reg = df.groupby('code', sort=False).apply(lambda df: \ 316 | rolling_corr(df.reset_index('code'), x_name, y_name, n)) 317 | df[[name_r]] = df_reg 318 | # 将index变回 date code 319 | df = df.reset_index() 320 | df = df.sort_values(by='date') 321 | df = df.set_index(['date','code']) 322 | df = df.sort_index(level=['date','code']) 323 | return df 324 | 325 | 326 | # x_name list, y_nmae column 327 | # 截面多元线性回归 328 | def cal_CrossReg(df, x_name, y_name, residual=False): 329 | # 使用sm模块 330 | result = df.groupby('date', sort=False).apply(lambda d:\ 331 | sm.OLS(d[y_name], sm.add_constant(d[x_name])).fit()) 332 | 333 | # 如果d[x_name]中所有数相同为C且不为零,这时params中没有const,x_name为d[y_name].mean()/C 334 | # rsquared为0 335 | # 当d[x_name]全为0时,params['const']为0,params[x_name]为d[y_name].mean() 336 | # rsquared可能为极小的负数 337 | def func(x, name): 338 | try: 339 | return x.params[name] 340 | except: 341 | print('sm reg warning') 342 | return 0 343 | #if type(x_name)!=type([]): 344 | r = result.map(lambda x: np.sqrt(abs(x.rsquared))) 345 | 346 | data = [] 347 | index = [] 348 | for i in result.items(): 349 | data.append(dict(i[1].params)) 350 | index.append(i[0]) 351 | params = pd.DataFrame(data, index=index) 352 | 353 | if residual: 354 | return pd.Series(df.groupby('date').apply(lambda x: x[y_name] - \ 355 | (params.loc[x.index[0][0]][x_name]*x[x_name]).sum(axis=1) -\ 356 | params.loc[x.index[0][0]]['const']).values, index=df.index) 357 | else: 358 | return params, r 359 | -------------------------------------------------------------------------------- /FreeBack/opt.py: -------------------------------------------------------------------------------- 1 | import scipy.optimize as sco 2 | from itertools import product 3 | import FreeBack as FB 4 | import numpy as np 5 | import pandas as pd 6 | 7 | # 投资组合优化模块 8 | 9 | class Result(): 10 | # 组合优化结果(returns, weight) 11 | def __init__(self): 12 | self.store = {} 13 | class smallResult(): 14 | def __init__(self, df): 15 | self.df = df 16 | self.returns = (self.df['returns']*self.df['weight']).groupby('date').sum() 17 | def add(self, method, df): 18 | self.store[method] = self.smallResult(df) 19 | class Opt(): 20 | # 待优化组合, index:(date,code) values: 简单收益率序列 21 | def __init__(self, ser, interval=20, window=250): 22 | self.ser = ser 23 | self.interval = interval 24 | self.window = window 25 | # key为优化方法如'max_sharpe' 26 | self.result = Result() 27 | # 计算某段时期最优化权重 28 | def calculate_weights(self, ser, method): 29 | # 计算组合内股票的期望收益率和协方差矩阵 30 | pf = ser.unstack().fillna(0) 31 | # 简单平均/几何平均 32 | mean_return = [pf.mean()] 33 | #mean_return = [np.exp(np.log(pf+1).mean())-1] 34 | cov_matrix = pf.cov().values * 250 35 | # 计算给定权重下的投资组合表现 36 | def target(weights, method): 37 | weights = np.array(weights) 38 | pred_var = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights))) # 组合的期望波动 39 | pred_return = (np.prod(1+ mean_return*weights)-1)*250 # 组合的期望收益 40 | # 最小波动 41 | if method=='最小波动': 42 | return pred_var 43 | # 风险平价 44 | elif method=='风险平价': 45 | MRC = np.dot(cov_matrix, weights) 46 | TRC = weights*MRC 47 | return sum([(i[0]-i[1])**2 for i in list(product(TRC, repeat=2))]) 48 | # 最大收益 49 | elif method=='最大收益': 50 | return -pred_return 51 | # 最大夏普 52 | elif method=='最大夏普': 53 | return -pred_return/pred_var 54 | # 投资组合内股票数目 55 | num = pf.shape[1] 56 | if method != '等权': 57 | def min_func(weights): 58 | return target(weights, method) 59 | # 约束是所有参数(权重)的总和为1 60 | cons = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1}) 61 | # 参数值(权重)限制在0和1之间 62 | bnds = tuple((0, 1) for x in range(num)) 63 | # 调用最优化函数,对初始权重使用平均分布 64 | opt = sco.minimize(min_func, num * [1. / num], method='SLSQP', bounds=bnds, constraints=cons) 65 | w = opt['x'] 66 | else: 67 | w = num * [1. / num] 68 | return pd.Series(w, index=pf.columns) 69 | # 每隔inteval天优化持仓权重,当interval为'week','month'时为每周初、每月初优化持仓权重。 使用前window天数据 method 优化方法 70 | def process_method(self, method='等权'): 71 | if method=='all': 72 | for method in ['等权', '最小波动', '风险平价', '最大收益', '最大夏普']: 73 | self.process_method(method) 74 | else: 75 | df = self.ser.fillna(0) 76 | date_range = df.index.get_level_values(0).unique() 77 | weights = [] 78 | for i in range(self.window, len(date_range) - 1, self.interval): 79 | weight = self.calculate_weights(df.loc[date_range[i-self.window:i]], method=method) 80 | weight = pd.DataFrame(weight) 81 | weight['date'] = date_range[i-1] 82 | weight = weight.reset_index().set_index(['date', 'code'])[0] 83 | weights.append(weight) 84 | weights = pd.DataFrame(pd.concat(weights)).rename(columns={0:'weight'}) 85 | df = pd.DataFrame(df).join(weights).groupby('code').ffill().dropna() 86 | self.result.add(method, df) 87 | # 查看历史表现,比较对象其他方法或者子策略 88 | def pnl(self, method='等权', compare='sub'): 89 | if compare=='sub': 90 | benchmark = self.ser.unstack() 91 | elif compare=='method': 92 | benchmark = pd.DataFrame() 93 | for k,v in self.result.store.items(): 94 | if k!=method: 95 | benchmark[k] = v.returns 96 | post0 = FB.post.ReturnsPost(self.result.store[method].returns, benchmark=benchmark, stratname=method) 97 | post0.pnl() 98 | # 查看历史权重 99 | def weight(self, method='等权'): 100 | plt, fig, ax = FB.display.matplot() 101 | codes = self.result.store[method].df['weight'].index.get_level_values(1).unique() 102 | all_weights = [self.result.store[method].df['weight'].loc[:, i, :] for i in codes] 103 | plt.stackplot(self.result.store[method].df.index.get_level_values(0).unique(), all_weights,\ 104 | labels=codes) 105 | plt.legend(bbox_to_anchor=(0.5, -0.2), loc=8, ncol=5) 106 | FB.post.check_output() 107 | plt.savefig('./output/'+method+'-weight') 108 | plt.show() -------------------------------------------------------------------------------- /FreeBack/signal.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 基于信号的择时框架 3 | ''' 4 | 5 | import pandas as pd 6 | import numpy as np 7 | #import numba as nb 8 | import matplotlib.pyplot as plt 9 | from FreeBack.display import matplot 10 | from FreeBack.post import ReturnsPost 11 | from FreeBack.my_pd import parallel_group 12 | 13 | ''' 14 | 信号生成模块->根据因子生成信号 15 | 输入: market.index: date code; columns:open, low, high, close 16 | factor, Series, index: date code 17 | ''' 18 | class SignalGenerate(): 19 | def __init__(self, market, return_type='open'): 20 | self.market = market 21 | # 预期当天触发信号,次日所获得收益 22 | if return_type =='open': 23 | self.sr = market.groupby('code')['open'].apply(lambda x: x.shift(-2)/x.shift(-1) - 1).droplevel(0) 24 | elif return_type == 'close': 25 | self.sr = market.groupby('code')['close'].apply(lambda x: x.shift(-1)/x - 1).droplevel(0) 26 | elif return_type == 'overnight': 27 | self.sr = market.groupby('code').apply(lambda x: x['open'].shift(-1)/x['close'] - 1).droplevel(0) 28 | elif return_type == 'inday': 29 | self.sr = market.groupby('code').apply(lambda x: x['close'].shift(-1)/x['open'].shift(-1) - 1).droplevel(0) 30 | self.sr = self.sr.dropna() 31 | 32 | ### 快速计算并展示因子的择时效果 33 | def fast_post(self, factor, cal_type=0, cal_param=[0.3, 0.7, '9999d','365d'], \ 34 | cut_type=0, cut_param=None, n_core=10): 35 | io_df = self.get_oi_df(factor=factor, cal_type=cal_type, cal_param=cal_param) 36 | signals = self.get_signals(io_df=io_df, cut_type=cut_type, cut_param=cut_param, n_core=n_core) 37 | pos = SignalPost(signals=signals, sr=self.sr) 38 | pos.position_post(compose_type=0) 39 | 40 | 41 | ### 给出因子值,计算进出场信号 42 | # factor: 因子值,index:date 43 | # cal_type计算类型: 44 | # 0: 按照历史分位数, 参数: [0.1, 0.9, '9999d', '200d'], 最小10%做空, 90%后做多, 窗口9999d,最小窗口'200d' 45 | # 1: 绝对数值, 参数:[a, b, factor2, c. d], 小于a空开, 小于b多开, factor2平仓因子,None表示没有平仓因子 46 | def get_oi_df(self, factor, cal_type=0, cal_param=[0.3, 0.7, '9999d','365d']): 47 | def fun0(x): # 历史分位数类型 48 | x = x.set_index('date') 49 | min_date = x.index.min() + pd.Timedelta(cal_param[3]) 50 | x['short'] = x['mo11'].rolling(cal_param[2]).quantile(cal_param[0]) 51 | x['long'] = x['mo11'].rolling(cal_param[2]).quantile(cal_param[1]) 52 | x = x.loc[min_date: ].reset_index().set_index(['date', 'code']) 53 | return x 54 | 55 | factor = factor.dropna() 56 | fname = factor.name 57 | if cal_type == 0: 58 | factor = factor.dropna() 59 | f = factor.reset_index().groupby('code')[['date', 'code', fname]].apply(lambda x: fun0(x)).droplevel(0) 60 | io_df = pd.DataFrame(index=self.market.index, columns=['lo', 'so']) 61 | io_df['lo'] = f['long'] < f[fname] 62 | io_df['so'] = f['short'] > f[fname] 63 | elif cal_type == 1: 64 | lo = factor >= cal_param[1] 65 | so = factor <= cal_param[0] 66 | if cal_param[2] is None: 67 | io_df = pd.DataFrame(index=self.market.index, columns=['lo', 'so']) 68 | else: 69 | io_df = pd.DataFrame(index=self.market.index, columns=['lo', 'so', 'sc', 'lc']) 70 | factor2 = cal_param[2] 71 | io_df['sc'] = factor2 <= cal_param[3] 72 | io_df['lc'] = factor2 >= cal_param[4] 73 | io_df['so'] = so 74 | io_df['lo'] = lo 75 | else: 76 | print('cal_type格式错误') 77 | io_df = io_df.dropna() 78 | return io_df 79 | 80 | 81 | ### 给出进场出场条件,生成信号 82 | # 信号: 当天收盘后触发、次日执行 83 | # oi_df: index: date, columns:[lo, so, lc, sc](多开,空开,多平,空平) 84 | # cut_type止损类型: None:没有止损, fix: 持有固定天数 85 | def get_signals(self, io_df, cut_type=0, cut_param=None, n_core=10): 86 | def fun(x): 87 | return self.get_one_signal(io_df=x, cut_type=cut_type, cut_param=cut_param) 88 | return parallel_group(io_df, func=fun, n_core=n_core, sort_by='code') 89 | 90 | def get_one_signal(self, io_df, cut_type=0, cut_param=None): 91 | if cut_type == 0: 92 | if set(io_df.columns) == set(('lo', 'so')): # 默认按照因子值平仓 93 | io_df['lc'] = io_df['lo'] 94 | io_df['sc'] = io_df['lo'] 95 | signal = self.compose_io1(df=io_df) 96 | elif cut_type == 1: 97 | if set(io_df.columns) == set(('lo', 'so')): # 默认不主动平仓 98 | io_df['lc'] = False 99 | io_df['sc'] = False 100 | signal = self.compose_io2(df=io_df, n=cut_param) 101 | else: 102 | print('输入止损格式错误,程序终止') 103 | return None 104 | return signal 105 | 106 | 107 | # 合成信号方式一 108 | # 所有持仓均按照lc, sc执行平仓 109 | def compose_io1(self, df): 110 | last_signal = 0 111 | for t in df.index: 112 | if last_signal == 0: 113 | if df.loc[t, 'lo']: 114 | df.loc[t, 'signal'] = 1 115 | elif df.loc[t, 'so']: 116 | df.loc[t, 'signal'] = -1 117 | else: 118 | df.loc[t, 'signal'] = 0 119 | elif last_signal == 1: 120 | if df.loc[t, 'lc']: 121 | if df.loc[t, 'so']: 122 | df.loc[t, 'signal'] = -1 123 | else: 124 | df.loc[t, 'signal'] = 0 125 | else: 126 | df.loc[t, 'signal'] = 1 127 | elif last_signal == -1: 128 | if df.loc[t, 'sc']: 129 | if df.loc[t, 'lo']: 130 | df.loc[t, 'signal'] = 1 131 | else: 132 | df.loc[t, 'signal'] = 0 133 | else: 134 | df.loc[t, 'signal'] = -1 135 | last_signal = df.loc[t, 'signal'] 136 | return df['signal'] 137 | 138 | 139 | # 合成信号方式二 140 | # 只能仓位是1, -1, 0 141 | # 按照最大回撤止损, 止损后空仓3天 142 | def compose_io2(self, df, n): 143 | sr = self.sr 144 | last_signal = 0 145 | in_date = None 146 | cut_flag = 0 147 | for t in df.index: 148 | if last_signal == 0: # 上次空仓情况 149 | if cut_flag > 0: # 止损空仓期 150 | df.loc[t, 'signal'] = 0 151 | cut_flag += 1 152 | if cut_flag == 3: 153 | cut_flag = 0 154 | else: 155 | if df.loc[t, 'lo']: 156 | df.loc[t, 'signal'] = 1 157 | in_date = t 158 | elif df.loc[t, 'so']: 159 | df.loc[t, 'signal'] = -1 160 | in_date = t 161 | else: 162 | df.loc[t, 'signal'] = 0 163 | ## 在场情况 164 | else: # 上次有持仓 165 | ## 止损情况,空仓三天 166 | net = (1 + last_signal*sr.loc[in_date: t]).prod() 167 | if net < 1-n: 168 | df.loc[t, 'signal'] = 0 169 | in_date = None 170 | cut_flag = 1 171 | else: ## 未触发止损情况 172 | if last_signal == 1: 173 | if df.loc[t, 'lc']: 174 | if df.loc[t, 'so']: 175 | in_date = t 176 | df.loc[t, 'signal'] = -1 177 | else: 178 | df.loc[t, 'signal'] = 0 179 | else: 180 | df.loc[t, 'signal'] = 1 181 | elif last_signal == -1: 182 | if df.loc[t, 'sc']: 183 | if df.loc[t, 'lo']: 184 | in_date = t 185 | df.loc[t, 'signal'] = 1 186 | else: 187 | df.loc[t, 'signal'] = 0 188 | else: 189 | df.loc[t, 'signal'] = -1 190 | last_signal = df.loc[t, 'signal'] 191 | return df['signal'] 192 | 193 | 194 | 195 | # sr 次日可以获取的收益率: Series, index: date or index: date code 196 | # signals: 次日持仓信号 197 | class SignalPost(): 198 | def __init__(self, signals, sr, comm=2/1e4): 199 | self.signals = signals 200 | self.sr = sr 201 | self.comm = comm 202 | 203 | # 根据信号计算持仓df 204 | # compose_type信号组合方式 205 | # 0: 默认满仓等权 206 | def get_position(self, compose_type=0): 207 | if compose_type == 0: 208 | postion_df = self.signals.unstack().fillna(0) 209 | postion_df = postion_df.div(postion_df.abs().sum(axis=1), axis=0) 210 | else: 211 | print('输入compose_type错误') 212 | self.position_df = postion_df 213 | 214 | def position_post(self, compose_type=0): 215 | self.get_position(compose_type=compose_type) 216 | self.turnover = (self.position_df - self.position_df.shift(1)).fillna(0) 217 | sr_df = self.sr.unstack()*self.position_df - self.turnover*self.comm 218 | sr_compose = sr_df.loc[self.turnover.index].sum(axis=1) 219 | 220 | ReturnsPost(returns=sr_compose).pnl() 221 | 222 | 223 | """ 224 | 根据开仓信号和平仓类,分析开仓信号的优劣、得到持仓状态、展示择时效果。 225 | """ 226 | 227 | import FreeBack as FB 228 | from plottable import ColumnDefinition, ColDef, Table 229 | 230 | 231 | ATR_period = 20 232 | 233 | class Signal(): 234 | # 开仓信号坐标,开仓方向 235 | def __init__(self, market, oloc, trail, direct=1, benchmark=None): 236 | # 计算波动水平 237 | TR = pd.concat([market['high']-market['low'], \ 238 | abs(market['close'].groupby('code').shift()-market['high']),\ 239 | abs(market['close'].groupby('code').shift()-market['low'])],\ 240 | axis=1).max(axis=1) 241 | market['ATR'] = FB.my_pd.cal_ts(TR, 'MA', ATR_period) 242 | self.market = market 243 | self.oloc = oloc 244 | self.trail = trail 245 | self.direct = direct 246 | self.benchmark = benchmark 247 | # 信号分析(是否只分析通过trail结束的信号) 248 | def analysis(self, end_by_trail=False): 249 | if end_by_trail: 250 | result = self.result[self.result['end']!=self.market.index[-1][0]] 251 | result_hold = pd.concat([self.result_after[i]['stepr'] for i in result.index]) 252 | else: 253 | result = self.result 254 | result_hold = self.result_hold 255 | 256 | col0 = pd.DataFrame(columns=['col0']) 257 | col0.loc[0] = '开仓次数' 258 | col0.loc[1] = len(result) 259 | col0.loc[2] = '平均持有时长' 260 | col0.loc[3] = result['dur'].mean().round(1) 261 | col0.loc[4] = '持有至结束占比(%)' 262 | col0.loc[5] = round(100*(result['end']==self.market.index[-1][0]).mean(), 1) 263 | col1 = pd.DataFrame(columns=['col1']) 264 | col1.loc[0] = '空仓时间占比(%)' 265 | col1.loc[1] = round(100-100*len(result_hold.index.get_level_values(0).unique())/\ 266 | len(self.market.index.get_level_values(0).unique()), 1) 267 | col1.loc[2] = '最大持有只数' 268 | col1.loc[3] = result_hold.reset_index().groupby('date').count().max().values[0] 269 | col1.loc[4] = '平均持有只数' 270 | col1.loc[5] = result_hold.reset_index().groupby('date').count().mean()\ 271 | .round(1).values[0] 272 | col2 = pd.DataFrame(columns=['col2']) 273 | col2.loc[0] = '平均收益(万)' 274 | col2.loc[1] = result['returns'].mean().round(1) 275 | col2.loc[2] = '总收益(%)' 276 | col2.loc[3] = round((result_hold.groupby('date').mean()+1).prod()*1e2-1e2, 1) 277 | col2.loc[4] = '平均潜在收益(万)' 278 | col2.loc[5] = result['maxr'].mean().round(1) 279 | col3 = pd.DataFrame(columns=['col3']) 280 | col3.loc[0] = '平均超额收益(万)' 281 | if type(self.benchmark)!=type(None): 282 | col3.loc[1] = round(result['excess_returns'].mean(), 1) 283 | else: 284 | col3.loc[1] = 'nan' 285 | col4 = pd.DataFrame(columns=['col4']) 286 | col4.loc[0] = '平均最大回撤(万)' 287 | col4.loc[1] = result['maxd'].mean().round(1) 288 | col4.loc[2] = '平均负收益(万)' 289 | pmean = round((result[result['returns']>0]['returns']).mean(), 1) 290 | nmean = -round((result[result['returns']<0]['returns']).mean(), 1) 291 | col4.loc[3] = nmean 292 | col5 = pd.DataFrame(columns=['col5']) 293 | col5.loc[0] = '胜率(%)' 294 | col5.loc[1] = round(100*(result['returns']>0).mean(), 1) 295 | col5.loc[2] = '赔率' 296 | col5.loc[3] = round(pmean/nmean, 1) 297 | col5.loc[4] = 'E ratio' 298 | col5.loc[5] = round((self.result['maxr']/\ 299 | self.result['ATR'].replace(0, np.nan)).mean()/\ 300 | (self.result['maxd']/\ 301 | self.result['ATR'].replace(0, np.nan)).mean(),2) 302 | col6 = pd.DataFrame(columns=['col6']) 303 | col7 = pd.DataFrame(columns=['col7']) 304 | df_details = pd.concat([col0, col1, col2, col3, \ 305 | col4, col5, col6, col7], axis=1).fillna('') 306 | self.df_details = df_details 307 | plt, fig, ax = FB.display.matplot(w=22) 308 | column_definitions = [ColumnDefinition(name='col0', group="基本参数"), \ 309 | ColumnDefinition(name='col1', group="基本参数"), \ 310 | ColumnDefinition(name='col2', group='收益能力'), \ 311 | ColumnDefinition(name='col3', group='收益能力'), \ 312 | ColumnDefinition(name="col4", group='风险水品'), \ 313 | ColumnDefinition(name="col5", group='风险调整'), \ 314 | ColumnDefinition(name="col6", group='策略执行'), 315 | ColumnDefinition(name="col7", group='业绩持续性分析')] + \ 316 | [ColDef("index", title="", width=0, textprops={"ha":"right"})] 317 | tab = Table(self.df_details, row_dividers=False, col_label_divider=False, 318 | column_definitions=column_definitions, 319 | odd_row_color="#e0f6ff", even_row_color="#f0f0f0", 320 | textprops={"ha": "center"}) 321 | #ax.set_xlim(2,5) 322 | # 设置列标题文字和背景颜色(隐藏表头名) 323 | tab.col_label_row.set_facecolor("white") 324 | tab.col_label_row.set_fontcolor("white") 325 | # 设置行标题文字和背景颜色 326 | tab.columns["index"].set_facecolor("white") 327 | tab.columns["index"].set_fontcolor("white") 328 | tab.columns["index"].set_linewidth(0) 329 | FB.post.check_output() 330 | plt.savefig('./output/details.png') 331 | plt.show() 332 | # 从开仓信号得到信号强度(result)、持仓状态(result_hold)和跟踪指标(result_after) 333 | def run(self, comm=0, opentime='close'): 334 | result = pd.DataFrame(index=self.oloc) 335 | result_hold = pd.Series() 336 | result_after = {} 337 | for start in self.oloc: 338 | if start not in self.market.index: 339 | #print('开仓信号', start, '不在market,忽略。') 340 | continue 341 | #print('从', start, '开始') 342 | if start in result_hold.index: 343 | #print('开仓信号', start, '在持有状态,忽略。') 344 | continue 345 | # 信号触发后的market, copy后速度反而加快(41~50s -> 38s) 346 | after_market = self.market.loc[start[0]:, start[1], :].copy() 347 | after_market, r = self.trail(after_market, self.direct, comm, opentime).run() 348 | result.loc[start, ['end', 'returns', 'dur', 'maxr', 'maxd', 'ATR']] = r 349 | if result_hold.empty: 350 | result_hold = after_market['stepr'].iloc[1:] 351 | else: 352 | result_hold = pd.concat([result_hold, after_market['stepr'].iloc[1:]]) 353 | result_after[start] = after_market 354 | result = result.dropna() 355 | if type(self.benchmark)!=type(None): 356 | result['excess_returns'] = pd.Series(result.reset_index().\ 357 | apply(lambda x: 1e4*(1+x['returns']/1e4)/\ 358 | (self.benchmark.loc[x['date']:x['end']]+1).prod()-1e4, axis=1).values,\ 359 | index=result.index) 360 | self.result = result 361 | self.result_hold = result_hold 362 | self.result_after = result_after 363 | # 观察单标的择时情况,code可以输入代码或者整数当输入整数时展示单次最大收益的代码,\ 364 | # indicators after_market中指标 365 | def lookcode(self, code=0, indicators=[]): 366 | if type(code)==type(0): 367 | code = self.result.sort_values(by='returns', \ 368 | ascending=False).index.get_level_values(1)[code] 369 | daterange = self.market.loc[:, code, :].index.get_level_values(0).unique() 370 | datemap = pd.Series(range(len(daterange)), index=daterange) 371 | 372 | plt, fig, ax = FB.display.matplot() 373 | # 跟踪价格,收盘价 374 | l0, = ax.plot(datemap.values, self.market.loc[:, code, :]['close'].values) 375 | # 开仓信号 376 | l1 = ax.scatter(datemap[pd.DataFrame(index=self.oloc).loc[:, code, :].index].values, \ 377 | self.market.loc[:, code, :]['close'].loc[\ 378 | pd.DataFrame(index=self.oloc).loc[:, code, :].index].values,\ 379 | c='C3', s=10, marker='*', alpha=1) 380 | lines = [] 381 | ax1 = ax.twinx() 382 | for date in self.result.loc[:, code, :].index: 383 | l2 = ax.vlines(datemap[date], self.market.loc[:, code, :]['close'].min(),\ 384 | self.market.loc[:, code, :]['close'].max(), colors='C3', linestyle='--') 385 | l3 = ax.vlines(datemap[self.result_after[(date, code)].loc[:, code, :].index[-1]],\ 386 | self.market.loc[:, code, :]['close'].min(),\ 387 | self.market.loc[:, code, :]['close'].max(), colors='C2',\ 388 | linestyle='--') 389 | for indicator in indicators: 390 | l, = ax1.plot(datemap.loc[\ 391 | self.result_after[(date, code)].loc[:, code, :].index].values,\ 392 | self.result_after[(date, code)].loc[:, code, :][indicator].values,\ 393 | c='C1', alpha=0.5) 394 | lines.append(l) 395 | plt.legend([l0, l1, l2, l3, ]+lines, ['收盘价', '开仓信号', '开仓', '平仓']+indicators) 396 | plt.title(code) 397 | plt.show() 398 | # 信号的时序情况 399 | def plot_ts(self): 400 | plt, fig, ax = FB.display.matplot() 401 | # 有benchmark算超额,没有算绝对 402 | factor = 'returns' if type(self.benchmark)==type(None) else 'excess_returns' 403 | mean_returns = self.result.groupby('date')[factor].mean() 404 | #cum_mean_returns = (self.result.groupby('date')[factor].sum().cumsum()/\ 405 | # self.result.groupby('date')[factor].count().cumsum()) 406 | num_signals = self.result.groupby('date')[factor].count() 407 | cum_num_signals = self.result.groupby('date')[factor].count().cumsum()/\ 408 | (self.result.groupby('date')[factor].count()/\ 409 | self.result.groupby('date')[factor].count()).cumsum() 410 | l0 = ax.bar(mean_returns.index, mean_returns.values) 411 | # 方便显示,按十倍作图 412 | #l1, = ax.plot(10*cum_mean_returns, c='C0') 413 | ax1 = ax.twinx() 414 | l1 = ax1.bar(num_signals.index, num_signals.values, color='C7', alpha=0.3) 415 | l2, = ax1.plot(cum_num_signals, color='C7') 416 | 417 | lab = '平均收益' if type(self.benchmark)==type(None) else '平均超额' 418 | lab1 = '信号次数(右)' 419 | plt.legend([l0, l1, l2], ['bar'+lab, 'bar'+lab1, '累计平均'+lab1]) 420 | ax.set_ylabel('(万)') 421 | FB.post.check_output() 422 | plt.savefig('./output/ts.png') 423 | plt.show() 424 | def plot_net(self): 425 | plt, fig, ax = FB.display.matplot() 426 | l0, = ax.plot((self.result_hold.groupby('date').mean()+1).cumprod(), linewidth=2) 427 | if type(self.benchmark)!=type(None): 428 | ax.plot((self.benchmark[\ 429 | self.result_hold.groupby('date').count().index[0]:]+1).cumprod(), c='C7') 430 | ax1 = ax.twinx() 431 | l1, = ax1.plot(self.result_hold.groupby('date').count(), c='C1', alpha=0.3) 432 | plt.legend([l0, l1], ['等权持有净值', '持有标的个数(右)'], loc='lower left') 433 | FB.post.check_output() 434 | plt.savefig('./output/net.png') 435 | plt.show() 436 | 437 | # 跟踪类, 438 | # 输入: after_market multiindex 单一code 439 | # 输出:持仓坐标、带有指标的after_market、[收益,持有时间,最大收益,最大回撤] 440 | # opentime 'close':信号所在收盘价开仓, 'open':信号下一根bar开盘价格开仓 441 | class Trail(): 442 | def __init__(self, after_market, direct=1, comm=0, opentime='close'): 443 | self.after_market = after_market 444 | self.direct = direct 445 | self.comm = comm 446 | self.opentime = opentime 447 | # 游标 448 | self.indexrange = self.after_market.index 449 | self.i = 0 450 | # 获取after_market的指标 451 | def get_index(self, shift=0): 452 | if shift<0: 453 | return self.indexrange[0] 454 | try: 455 | return self.indexrange[self.i-shift] 456 | except: 457 | print(self.i, shift) 458 | print('最早取到0个日期') 459 | return self.indexrange[0] 460 | def get_ind(self, ind, shift=0): 461 | return self.after_market.loc[self.get_index(shift), ind] 462 | def set_ind(self, ind, value, shift=0): 463 | self.after_market.loc[self.get_index(shift), ind] = value 464 | def run(self): 465 | # 收盘开仓 466 | self.set_ind('cum_high', self.get_ind('close')) 467 | self.set_ind('cum_low', self.get_ind('close')) 468 | self.set_ind('stepr', 0) 469 | self.init() 470 | self.i += 1 471 | while self.i0: 524 | self.edge = self.get_ind(self.care) 525 | self.AF = min(self.AF+self.deltaAF, self.maxAF) 526 | self.set_ind('SAR', self.get_ind('SAR', 1)+self.AF*(self.edge-self.get_ind('SAR', 1))) 527 | # 价格低于(做多)SAR离场 528 | return self.direct*(self.get_ind('close')-self.get_ind('SAR'))<0 529 | # 持有固定时间 530 | class Trail_fixdur(Trail): 531 | hold_dur = 10 532 | def init(self): 533 | # 运行中全部记录指标 534 | self.set_ind('dur', 1) 535 | def check(self): 536 | self.set_ind('dur', 1+self.get_ind('dur', 1)) 537 | return (self.get_ind('dur')-1)==self.hold_dur 538 | # 固定止盈止损 539 | class Trail_stop(Trail): 540 | stop_profit = 10 541 | stop_loss = 0.02 542 | def init(self): 543 | self.set_ind('pnl', 0) 544 | def check(self): 545 | self.set_ind('pnl', self.get_ind('close')/self.get_ind('close', -1)-1) 546 | return (self.get_ind('pnl')>self.stop_profit)|(self.get_ind('pnl')<-self.stop_loss) 547 | # 动态止盈止损 548 | class Trail_trailstop(Trail): 549 | stop_profit = 10 550 | stop_loss = 0.02 551 | def init(self): 552 | self.set_ind('trailloss', 0) 553 | self.set_ind('trailprofit', 0) 554 | def check(self): 555 | self.set_ind('trailloss', 1-self.get_ind('close')/self.get_ind('cum_high')) 556 | self.set_ind('trailprofit', self.get_ind('close')/self.get_ind('cum_low')-1) 557 | return (self.get_ind('trailprofit')>self.stop_profit)|(self.get_ind('trailloss')>self.stop_loss) 558 | # 止盈止损+最大持有时间 559 | class Trail_stopdur(Trail): 560 | hold_dur = 100 561 | stop_profit = 0.02 562 | stop_loss = 0.02 563 | def init(self): 564 | self.set_ind('pnl', 0) 565 | self.set_ind('dur', 1) 566 | def check(self): 567 | self.set_ind('dur', 1+self.get_ind('dur', 1)) 568 | self.set_ind('pnl', self.get_ind('close')/self.get_ind('close', -1)-1) 569 | return (self.get_ind('pnl')>self.stop_profit)|(self.get_ind('pnl')<-self.stop_loss)|\ 570 | ((self.get_ind('dur')-1)==self.hold_dur) 571 | # 自定义卖出信号 572 | class Trail_cloc(Trail): 573 | cloc = None 574 | def check(self): 575 | return self.get_index(0) in self.cloc 576 | -------------------------------------------------------------------------------- /FreeBack/strat.py: -------------------------------------------------------------------------------- 1 | import FreeBack as FB 2 | import numpy as np 3 | import pandas as pd 4 | import time 5 | 6 | 7 | 8 | #################################################################### 9 | ########################## 常用策略框架 ############################## 10 | ################################################################### 11 | 12 | # 修正冻结交易日(停牌、涨跌停等)的returns,market 13 | def frozen_correct(code_returns, market, buy_frozen_days, sell_frozen_days=None): 14 | if code_returns.name: 15 | returns_name = code_returns.name 16 | else: 17 | returns_name = 0 18 | # code_returns 调整:连续冻结交易日(涨跌停/停牌)收益转移到第一个冻结交易日 19 | code_returns = code_returns.reindex(market.index).fillna(0) # 收益对齐至market 20 | if type(sell_frozen_days)==type(None): 21 | sell_frozen_days = buy_frozen_days 22 | from numba import njit 23 | @njit 24 | def compute_freeze_blocks(buy_arr, sell_arr): 25 | freeze = np.zeros(buy_arr.shape[0], dtype=np.int32) 26 | block = 0 27 | is_frozen = False 28 | for i in range(len(freeze)): 29 | # 如果当天买入信号为 True 或者前一天已冻结,则当天属于冻结状态 30 | if buy_arr[i] or is_frozen: 31 | if not is_frozen: 32 | block += 1 # 启动新冻结区间 33 | is_frozen = True 34 | freeze[i] = block 35 | else: 36 | freeze[i] = 0 37 | # 如果处于冻结状态,且当天卖出信号为 False,则冻结状态在当天结束(当天仍归入该区间),下一天不再冻结 38 | if is_frozen and (not sell_arr[i]): 39 | is_frozen = False 40 | return freeze 41 | def process_series(buy_series, sell_series): 42 | # 转为 numpy array 43 | res = compute_freeze_blocks(buy_series.values, sell_series.values) 44 | return pd.Series(res, index=buy_series.index) 45 | frozen_days_labels = [] 46 | for code in buy_frozen_days.index.get_level_values(1).unique(): 47 | res = process_series(buy_frozen_days.loc[:, code, :], sell_frozen_days.loc[:, code, :]) 48 | res = res.reset_index() 49 | res['code'] = code 50 | res = res.set_index(['date', 'code'])[0] 51 | frozen_days_labels.append(res) 52 | frozen_days_labels = pd.concat(frozen_days_labels).sort_index() 53 | frozen_days_start = (frozen_days_labels!=0)&(frozen_days_labels!=frozen_days_labels.groupby('code').shift()) 54 | frozen_days_start = frozen_days_start.map(lambda x: 1 if x else 0) 55 | frozen_days_labels = frozen_days_labels[frozen_days_labels>0] 56 | frozen_days_labels = pd.Series(frozen_days_labels.index.get_level_values(1), \ 57 | index=frozen_days_labels.index)+frozen_days_labels.astype(str) # 每一个冻结区块有唯一值 58 | code_returns_frozen = (1+code_returns).groupby(frozen_days_labels).prod()-1 # 计算冻结交易日的收益率 59 | code_returns_frozen = code_returns_frozen.reset_index().merge(\ 60 | frozen_days_labels.loc[frozen_days_start[frozen_days_start==1].index]\ 61 | .reset_index().rename(columns={0:'index'}), on='index')\ 62 | .set_index(['date', 'code'])[returns_name].sort_index() # 收益率对齐到冻结首日 63 | code_returns_frozen = code_returns_frozen.reindex(frozen_days_labels.index).fillna(0) # 其后冻结日收益为0 64 | code_returns.loc[code_returns_frozen.index] = code_returns_frozen # 修正code_returns 65 | return code_returns, market[~buy_frozen_days] 66 | 67 | 68 | # 择股策略、元策略 69 | class MetaStrat(): 70 | # 'inexclude':, 71 | # True, 不排除 72 | # 'include', 'include'列为bool值,为True为符合条件的证券 73 | # 'exclude' 'exclude列为bool值, 为True为排除的证券 74 | # 格式为 ['code0', 'code1', ] 时为等权持有固定证券组合,'cash'表示持有现金 75 | # 'score':float, 按score列由大到小选取证券,等权持有 76 | # 'hold_num':float, 取前hold_num(大于1表示数量,小于1小于百分比)只 77 | # market,pd.DataFrame, 需要包括策略需要调取的列,可以先不加 78 | # price,当前日期可以获得的价格数据,可以使用 'close'收盘价(有一点未来信息),或者下根bar开盘价/TWAP/VWAP 79 | # hold_weight 权重, code_returns 标的收益(T-1日持有标的的收益), MultiIndex, Seires 80 | def __init__(self, market, inexclude=None, score=None, hold_num=None,\ 81 | price='close', interval=1, direct=1, hold_weight=None, code_returns=None): 82 | self.inexclude = inexclude 83 | self.score = score 84 | self.hold_num = hold_num 85 | # 记录是否是上市最后一天 86 | #market['Z'] = market.index.get_level_values(1).duplicated(keep='last') 87 | #market['Z'] = ~market['Z'] 88 | market.loc[:, 'Z'] = ~market.index.get_level_values(1).duplicated(keep='last') 89 | market.loc[market.index[-1][0], 'Z'] = False 90 | self.market = market 91 | self.price = price 92 | self.interval = interval 93 | self.direct = direct 94 | self.hold_weight = hold_weight 95 | self.code_returns = code_returns.loc[market.index[0][0]:market.index[-1][0]] 96 | # 为market添加cash品种 97 | def add_cash(self): 98 | cash = pd.DataFrame(index=self.market.index.get_level_values(0).unique()) 99 | cash['code'] = 'cash' 100 | cash['name'] = '现金' 101 | cash[self.price] = 1 102 | cash = cash.reset_index().set_index(['date', 'code']) 103 | self.market = pd.concat([self.market, cash]).sort_index() 104 | cash['returns'] = 0 105 | if type(self.code_returns)!=type(None): 106 | self.code_returns = pd.concat([self.code_returns, cash['returns']]).sort_index() 107 | # 获得虚拟持仓表(价格每一时刻货值都为1的持仓张数) 108 | def get_hold(self): 109 | if type(self.inexclude)==list: # 按列表持股 110 | if 'cash' in (self.inexclude): 111 | self.add_cash() 112 | df_hold = self.market.loc[:, self.inexclude, :] 113 | self.keeppool_rank = pd.Series(index=df_hold.index) # 记录选中顺序 114 | #elif self.inexclude==None: # 持仓全部股票 115 | # df_hold = self.market 116 | # self.keeppool_rank = pd.Series(index=df_hold.index) 117 | else: # 避免持有退市前最后一天股票 118 | if not self.inexclude: 119 | keeppool_rank = self.market[self.score][~self.market['Z']] 120 | elif self.inexclude=='include': 121 | keeppool_rank = self.market[self.score][(~self.market['Z'])&self.market['include']] 122 | elif self.inexclude=='exclude': 123 | keeppool_rank = self.market[self.score][(~self.market['Z'])&(~self.market['exclude'])] 124 | #time0 = time.time() 125 | keeppool_rank = keeppool_rank.groupby('date').rank(ascending=False, \ 126 | pct=(self.hold_num<1), method='first') 127 | #print('按日期分组排名耗时', time.time()-time0) 128 | #time0 = time.time() 129 | self.keeppool_rank = keeppool_rank[keeppool_rank<=self.hold_num] 130 | df_hold = self.market[[self.price]].loc[self.keeppool_rank.index] #.copy() 131 | # 检查有无空仓情形,如果有的话就在空仓日添加现金 132 | if len(self.market.index.get_level_values(0).unique())!=\ 133 | len(df_hold.index.get_level_values(0).unique()): 134 | lost_bars = list(set(self.market.index.get_level_values(0))-\ 135 | set(df_hold.index.get_level_values(0))) 136 | self.add_cash() 137 | df_hold = pd.concat([self.market.loc[lost_bars, 'cash', :][[self.price]],\ 138 | df_hold]) 139 | # 赋权 140 | if type(self.hold_weight)!=type(None): 141 | w_ = self.hold_weight # 如果df_hold中有cash,权重也应该加入cash 142 | else: 143 | w_ = 1 144 | df_hold = ((w_/df_hold[self.price]).dropna().unstack()).fillna(0) 145 | # 总账户市值1块钱 146 | df_hold = df_hold.div((df_hold!=0).sum(axis=1), axis=0) 147 | self.df_hold = self.direct*df_hold 148 | # 调仓间隔不为1时,需考虑调仓问题 149 | def get_interval(self, df): 150 | if type(self.interval)!=int: 151 | # 选取调仓日 152 | take_df = [pd.Series(index=sorted(self.interval)).loc[:date].index[-1]\ 153 | for date in df.index] 154 | elif self.interval!=1: 155 | # 以interval为周期 获取df 156 | # 选取的index interval = 3 0,0,0,3,3,3,6... 157 | take_df = [df.index[int(i/self.interval)*self.interval]\ 158 | for i in range(len(df.index))] 159 | else: 160 | return df 161 | real_df = df.loc[take_df].copy() 162 | ## 提取的index非连续,复原到原来的连续交易日index 163 | real_df.index = df.index 164 | return real_df 165 | # 运行策略 166 | def run(self): 167 | #time0 = time.time() 168 | self.get_hold() 169 | #print('获取持仓矩阵耗时', time.time()-time0) 170 | #time0 = time.time() 171 | df_hold = self.get_interval(self.df_hold) 172 | keeppool_rank = self.get_interval(self.keeppool_rank.fillna(1).\ 173 | groupby('date').cumsum().unstack()).stack() 174 | self.keeppool_rank = keeppool_rank.reset_index().sort_values(by=['date',0]).\ 175 | set_index(['date', 'code'])[0] 176 | #print('获取持仓表耗时', time.time()-time0) 177 | #time0 = time.time() 178 | always_not_hold = (df_hold==0).all() # 去掉一直持仓为0的品种 179 | self.df_hold = df_hold[always_not_hold[~always_not_hold].index].copy() 180 | # 判断cash是否在持仓,如果在的话避免price没有cash列 181 | if ('cash' in self.df_hold.columns)&('cash' not in self.market.index.get_level_values(1)): 182 | self.add_cash() 183 | # 价格矩阵,去掉没有持仓过的标的,缺失价格数据(nan)的日期 184 | df_price = pd.DataFrame(self.market[self.price]).\ 185 | pivot_table(self.price, 'date' ,'code')[self.df_hold.columns] 186 | self.df_price = df_price[~df_price.isna().all(axis=1)].copy() 187 | # 虚拟货值矩阵 188 | self.df_amount = (self.df_hold*self.df_price).fillna(0) 189 | #print('获取虚拟货值矩阵耗时', time.time()-time0) 190 | #time0 = time.time() 191 | # 权重矩阵 192 | self.df_weight = (self.df_amount.apply(lambda x:\ 193 | (x/abs(x.sum())).fillna(0), axis=1)) 194 | #print('获取权重矩阵耗时', time.time()-time0) 195 | #time0 = time.time() 196 | # 净值贡献矩阵 197 | if type(self.code_returns)==type(None): 198 | self.code_returns = (self.df_price/self.df_price.shift() - 1).fillna(0) 199 | else: 200 | self.code_returns = self.code_returns.unstack().fillna(0)[self.df_price.columns] 201 | self.df_contri = (self.df_weight.shift()*self.code_returns).fillna(0) 202 | self.returns = self.df_contri.sum(axis=1) 203 | self.net = (self.returns+1).cumprod() 204 | #print('获取净值耗时', time.time()-time0) 205 | #time0 = time.time() 206 | # 为了准确计算换手率,需要获得真实持仓市值与持仓张数(净值需要interval) 207 | net = self.get_interval(self.net) 208 | self.df_amount = self.df_amount.mul(list(net.values), axis=0) 209 | self.df_hold = self.df_hold.mul(list(net.values), axis=0) 210 | # 交易金额(注意不能直接用市值相减) 211 | delta_hold = self.df_hold-self.df_hold.shift().fillna(0) 212 | self.delta_amount = (delta_hold*self.df_price).fillna(0) 213 | # cash的变化不会带来换手,可能没有‘cash'列 214 | self.df_turnover = abs(self.delta_amount.div(self.df_amount.sum(axis=1), axis=0)) 215 | if 'cash' in self.df_hold.columns: 216 | self.df_turnover['cash'] = 0 217 | self.turnover = self.df_turnover.sum(axis=1) 218 | #print('获取换手率耗时', time.time()-time0) 219 | 220 | 221 | 222 | # 根据择时条件选择陪着不同的择股策略 223 | # conds = [满足条件0的交易日(index或lsit),满足条件1的交易日, ..., 满足条件n的交易日] 224 | # strats =[条件0对应策略0(MetaStrat), 非条件0且条件1对应策略1, ... , 非条件0到条件n-1且条件n对应策略n, 剩余时间执行策略n+1] 225 | class ComboStrat(MetaStrat): 226 | def __init__(self, conds, strats, market, price='close', code_returns=None, interval=1): 227 | self.conds = conds 228 | self.strats = strats 229 | # 如果状态数和策略数相同,呢么默认空余状态使用最后一个策略 230 | if len(strats)==len(conds): 231 | self.strats.append(strats[-1]) 232 | self.market = market 233 | self.price = price 234 | self.code_returns = code_returns 235 | self.interval = interval 236 | def get_hold(self): 237 | # 策略择时模块,将全部交易日按择时条件划分 238 | # 满足条件0为 239 | all_days = self.market.index.get_level_values(0).unique() 240 | print('共:', len(all_days), 'bars') 241 | stat_days = [] 242 | left_days = all_days 243 | # 从第一个择时条件开始筛选 244 | for cond in self.conds: 245 | stati_days = [] 246 | # 新的剩余交易日 247 | left_days_ = [] 248 | for date in left_days: 249 | if date in cond: 250 | stati_days.append(date) 251 | else: 252 | left_days_.append(date) 253 | stat_days.append(stati_days) 254 | left_days = left_days_ 255 | stat_days.append(left_days) 256 | # 子策略模块 257 | # stati对应strati对应df_holdi 258 | df_holds = [] 259 | keeppool_rank = [] 260 | for i in range(len(self.strats)): 261 | strati = self.strats[i] 262 | strati.market = self.market.loc[stat_days[i]] 263 | strati.price = self.price 264 | print('状态%s bars:'%i, len(stat_days[i])) 265 | if len(stat_days[i])==0: 266 | continue 267 | strati.get_hold() 268 | df_holds.append(strati.df_hold) 269 | keeppool_rank.append(strati.keeppool_rank) 270 | self.keeppool_rank = pd.concat(keeppool_rank).reset_index().\ 271 | sort_values(by=['date', 0]).set_index(['date', 'code'])[0] 272 | self.keeppool_rank = self.get_interval(self.keeppool_rank) 273 | self.df_hold = pd.concat(df_holds).sort_values(by='date').fillna(0) 274 | 275 | 276 | 277 | # 根据权重组合不同的MetaStrat 278 | class MixStrat(MetaStrat): 279 | def __init__(self, weights, strats, market, inexclude=None, score=None, hold_num=None, \ 280 | price='close', interval=1, direct=1, hold_weight=None, code_returns=None): 281 | super().__init__(market, inexclude, score, hold_num, price, interval, direct, hold_weight, code_returns) 282 | self.weights = weights 283 | self.strats = strats 284 | def get_hold(self): 285 | from functools import reduce 286 | # 虚拟货值矩阵 按权重分配 287 | # 不同策略隔离运行 288 | #df_amount = reduce(lambda x, y: x.add(y, fill_value=0), \ 289 | # [w*s.df_amount for w,s in zip(theory_weights, select_strats)]) 290 | # 不同策略合并运行 291 | total_amount = pd.concat([i.df_amount.sum(axis=1) for i in self.strats], axis=1).sum(axis=1) 292 | df_amount = reduce(lambda x, y: x.add(y, fill_value=0), \ 293 | [w*s.df_weight for w,s in zip(self.weights, self.strats)]).mul(total_amount, axis=0) 294 | df_price = pd.DataFrame(self.market[self.price]).pivot_table(self.price, 'date' ,'code') 295 | df_price['cash'] = 1 296 | self.df_hold = (df_amount/df_price).fillna(0) 297 | self.keeppool_rank = reduce(lambda x, y: x.add(y, fill_value=0), \ 298 | [w*s.keeppool_rank for w,s in zip(self.weights, self.strats)]) 299 | 300 | 301 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | backtest,stock, option, future,factor investing, portfiolio analyis 2 | 3 | # -----------------------Module------------------------ 4 | 5 | **alpha**:截面因子测试 6 | 7 | **signal**:时序信号测试 8 | 9 | **strat**: 基于并行的择股和择时策略的回测 10 | 11 | **barbybar**:逐k线,逐笔成交的事件驱动回测 12 | 13 | **opt**:投资组合优化 14 | 15 | **post**: 后处理模块 16 | 17 | **display**: 可视化模块 18 | 19 | **my_pd**:pandas常用操作 20 | 21 | **event**:事件信号测试 22 | 23 | # -----------------------INSTALL------------------------- 24 | 从pypi安装: 25 | 26 | pip install FreeBack 27 | 28 | 从github安装: 29 | 30 | pip3 install --upgrade --user git+https://github.com/LHanLi/FreeBack.git 31 | 32 | python setup.py develop 33 | 34 | # ----------------------Hello world----------------------- 35 | 36 | 37 | # -------------------- 联系作者 --------------------- 38 | 对于回测框架难以满足的个性化回测需求,可以联系作者。 39 | ![cde0c826807b3836377d0e13cf4bbf4](https://github.com/user-attachments/assets/3954cec9-8d4e-481c-a014-2ec971ab7cb4) 40 | 41 | -------------------------------------------------------------------------------- /example/data.csv.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LHanLi/FreeBack/306d01c7776db4a2362a02d68fce7cfc4a39295f/example/data.csv.xz -------------------------------------------------------------------------------- /example/data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import FreeBack as FB" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "data = pd.read_feather('D://cloud/data/stock_data/DB_daily.feather').\\\n", 20 | " set_index(['date', 'code']).loc['2021':'2024']\n", 21 | "data[data['close']>100].to_csv('data.csv.xz', compression='xz')" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "data = pd.read_csv('data.csv.xz', parse_dates=['date']).\\\n", 31 | " set_index(['date', 'code'])" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 4, 37 | "metadata": {}, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/html": [ 42 | "
\n", 43 | "\n", 56 | "\n", 57 | " \n", 58 | " \n", 59 | " \n", 60 | " \n", 61 | " \n", 62 | " \n", 63 | " \n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | "
nameopenhighlowclosevolamountex_factorfree_float_sharesfloat_sharestotal_shares
datecode
2021-01-04000538.SZ云南白药113.90124.96113.02124.9620044740.02.424452e+0918.150787462405068.06.021670e+081.277403e+09
000568.SZ泸州老窖228.00243.88228.00240.8515433467.03.669629e+0932.728099717247024.01.464307e+091.464752e+09
000596.SZ古井贡酒270.80284.64267.00279.664159819.01.156936e+093.424490112195978.03.836000e+085.036000e+08
000661.SZ长春高新454.00476.00450.00473.457103745.03.328959e+097.433691275473781.03.551826e+084.047203e+08
000799.SZ酒鬼酒156.51172.15155.80172.1512458526.02.078455e+092.028288224201689.03.249290e+083.249290e+08
.......................................
2024-01-02688639.SH华恒生物125.81125.81118.52121.191308087.01.582468e+081.474711101882294.01.019403e+081.575402e+08
688686.SH奥普特112.00113.19108.54108.54434976.04.791152e+071.50170734323455.06.162946e+071.222355e+08
688696.SH极米科技113.01113.04109.89109.89379779.04.215345e+071.43618341143196.04.567055e+077.000000e+07
688776.SH国光电气100.70105.7995.42102.383780947.03.842613e+081.40546034474541.03.447454e+071.083834e+08
832982.BJ锦波生物262.79267.66260.12264.97145461.03.844768e+072.48748623758489.02.375849e+076.808600e+07
\n", 249 | "

110378 rows × 11 columns

\n", 250 | "
" 251 | ], 252 | "text/plain": [ 253 | " name open high low close vol \\\n", 254 | "date code \n", 255 | "2021-01-04 000538.SZ 云南白药 113.90 124.96 113.02 124.96 20044740.0 \n", 256 | " 000568.SZ 泸州老窖 228.00 243.88 228.00 240.85 15433467.0 \n", 257 | " 000596.SZ 古井贡酒 270.80 284.64 267.00 279.66 4159819.0 \n", 258 | " 000661.SZ 长春高新 454.00 476.00 450.00 473.45 7103745.0 \n", 259 | " 000799.SZ 酒鬼酒 156.51 172.15 155.80 172.15 12458526.0 \n", 260 | "... ... ... ... ... ... ... \n", 261 | "2024-01-02 688639.SH 华恒生物 125.81 125.81 118.52 121.19 1308087.0 \n", 262 | " 688686.SH 奥普特 112.00 113.19 108.54 108.54 434976.0 \n", 263 | " 688696.SH 极米科技 113.01 113.04 109.89 109.89 379779.0 \n", 264 | " 688776.SH 国光电气 100.70 105.79 95.42 102.38 3780947.0 \n", 265 | " 832982.BJ 锦波生物 262.79 267.66 260.12 264.97 145461.0 \n", 266 | "\n", 267 | " amount ex_factor free_float_shares \\\n", 268 | "date code \n", 269 | "2021-01-04 000538.SZ 2.424452e+09 18.150787 462405068.0 \n", 270 | " 000568.SZ 3.669629e+09 32.728099 717247024.0 \n", 271 | " 000596.SZ 1.156936e+09 3.424490 112195978.0 \n", 272 | " 000661.SZ 3.328959e+09 7.433691 275473781.0 \n", 273 | " 000799.SZ 2.078455e+09 2.028288 224201689.0 \n", 274 | "... ... ... ... \n", 275 | "2024-01-02 688639.SH 1.582468e+08 1.474711 101882294.0 \n", 276 | " 688686.SH 4.791152e+07 1.501707 34323455.0 \n", 277 | " 688696.SH 4.215345e+07 1.436183 41143196.0 \n", 278 | " 688776.SH 3.842613e+08 1.405460 34474541.0 \n", 279 | " 832982.BJ 3.844768e+07 2.487486 23758489.0 \n", 280 | "\n", 281 | " float_shares total_shares \n", 282 | "date code \n", 283 | "2021-01-04 000538.SZ 6.021670e+08 1.277403e+09 \n", 284 | " 000568.SZ 1.464307e+09 1.464752e+09 \n", 285 | " 000596.SZ 3.836000e+08 5.036000e+08 \n", 286 | " 000661.SZ 3.551826e+08 4.047203e+08 \n", 287 | " 000799.SZ 3.249290e+08 3.249290e+08 \n", 288 | "... ... ... \n", 289 | "2024-01-02 688639.SH 1.019403e+08 1.575402e+08 \n", 290 | " 688686.SH 6.162946e+07 1.222355e+08 \n", 291 | " 688696.SH 4.567055e+07 7.000000e+07 \n", 292 | " 688776.SH 3.447454e+07 1.083834e+08 \n", 293 | " 832982.BJ 2.375849e+07 6.808600e+07 \n", 294 | "\n", 295 | "[110378 rows x 11 columns]" 296 | ] 297 | }, 298 | "execution_count": 4, 299 | "metadata": {}, 300 | "output_type": "execute_result" 301 | } 302 | ], 303 | "source": [ 304 | "data" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": null, 310 | "metadata": {}, 311 | "outputs": [], 312 | "source": [] 313 | } 314 | ], 315 | "metadata": { 316 | "kernelspec": { 317 | "display_name": "gitenv", 318 | "language": "python", 319 | "name": "python3" 320 | }, 321 | "language_info": { 322 | "codemirror_mode": { 323 | "name": "ipython", 324 | "version": 3 325 | }, 326 | "file_extension": ".py", 327 | "mimetype": "text/x-python", 328 | "name": "python", 329 | "nbconvert_exporter": "python", 330 | "pygments_lexer": "ipython3", 331 | "version": "3.10.13" 332 | } 333 | }, 334 | "nbformat": 4, 335 | "nbformat_minor": 2 336 | } 337 | -------------------------------------------------------------------------------- /example/output/alpha-Portfolio-Bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LHanLi/FreeBack/306d01c7776db4a2362a02d68fce7cfc4a39295f/example/output/alpha-Portfolio-Bar.png -------------------------------------------------------------------------------- /example/output/alpha-Portfolio-HoldReturn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LHanLi/FreeBack/306d01c7776db4a2362a02d68fce7cfc4a39295f/example/output/alpha-Portfolio-HoldReturn.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | 4 | with open("README.md","r",encoding='utf-8') as fh: 5 | long_description = fh.read() 6 | 7 | setup( 8 | name="FreeBack", 9 | # 版本号: 第几次模块增加,第几次函数增加,第几次函数功能修改 10 | # (每次高级别序号增加后,低级别序号归0) 11 | # alpha为调试版,beta为测试版,没有后缀为稳定版 12 | version="6.4.3", 13 | author="LH.Li,zzq", 14 | author_email="lh98lee@zju.edu.cn", 15 | description='Package for backtest', 16 | long_description=long_description, 17 | # 描述文件为md格式 18 | long_description_content_type="text/markdown", 19 | url="https://github.com/LHanLi/FreeBack", 20 | packages=find_packages(), 21 | install_requires = [ 22 | #'pandas', 23 | #'scipy', 24 | #'statsmodels', 25 | #'seaborn', 26 | #'plottable', 27 | #'pyecharts', 28 | #'numpy_ext', 29 | #'xlsxwriter' 30 | ], 31 | classifiers=[ 32 | # 该软件包仅与Python3兼容 33 | "Programming Language :: Python :: 3", 34 | # 根据GPL 3.0许可证开源 35 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 36 | # 与操作系统无关 37 | "Operating System :: OS Independent", 38 | ], 39 | ) 40 | 41 | # python3 setup.py install --u 42 | -------------------------------------------------------------------------------- /upload_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mypython=/Users/h1nlee/miniconda3/bin/python 3 | 4 | # Upload project to pypi 5 | 6 | rm -rf ./build 7 | rm -rf ./dist 8 | rm -rf ./FreeBack.egg-info 9 | 10 | $mypython setup.py sdist bdist_wheel 11 | twine check dist/* 12 | twine upload dist/* 13 | # (-u __token__ -p password) 14 | --------------------------------------------------------------------------------