zoukankan      html  css  js  c++  java
  • 实现低PE策略的回测

    回测的基本流程

    • 首先需要声明下,此回测属于日间回测,即当天收盘后对交易信号进行检测,得到买入或卖出检测结果,然后由第二天开盘后根据前一天的检测结果完成交易。
    • 其次要对账户进行除权除息处理。因为除权除息后价格会发生变化,如果不处理,那账户总资产就会出现偏差,除权除息分两种情况,分红和送股,先从价格上看,当日价格都会发生变化,一般都是变小,从资金账户上来看,但指分红,账户上的现金是增多的,现金由分红的股票市值产生,因此,该只股票的市值是减少的,但总资产是不变的;送股,总市值是保持不变的,只是持股数的变化
    • 这里先卖出再买入,是考虑到只有先卖出后,账户现金才充足,才能更大可能的买入股票

    假设我们的总资产是1000万元,对头寸的分配方式:均分、每只20万
    书写backtest函数,实现策略回测,其中包括总资金的设定、单只股票的买入金额、以及PE选股策略,均线上穿/下穿买入/卖出信号的调用

    资金、待卖、代买、持仓等的初始化
     #参数设定
        #初始资金,现金1000万  
        cash = 1E7
        #单只股票的仓位为20万
        single_position=2E5
        #时间为key的净值、收益和同期沪深基准
        df_profit = pd.DataFrame(columns=['net_value','profit','hs300'])
        #获取回测开始日期和结束日期之间的所有交易日,并按正序排列
        all_dates = get_trading_date(begin_date,end_date)
        #获取沪深300在回测开始的第一个交易日的值
        hs300_begin_value = DB_CONN['daily'].find_one(
            {'code':'000300','index':True,'date':all_dates[0]},
            projection={'close':True})['close']
        #获取回测周期内的股票池数据
        #adjust_dates:正序排列的调整日列表
        #date_codes_dict:调整日和当期的股票列表组成的dict,key为调整日,value为股票列表
        adjust_dates,date_codes_dict=stock_pool(begin_date,end_date)
        #股票池上期股票代码列表
        last_phase_codes = None
        #股票池当期股票代码列表
        this_phase_codes = None
        #待卖股票代码集合
        to_be_sold_codes= set()
        #待买股票代码结合
        to_be_bought_codes = set()
        #持仓股票dict,key为股票代码,value为一个dict
        #三个字段分别为:cost - 持仓成本,volume - 持仓数量,last_value - 前一天的市值
        holding_code_dict = dict()
        #前一个交易日
        last_date=None
        #在交易日的顺序,一天天完成信号检测
        for _date in all_dates:
            print('BackTest at %s.' %_date)
            #当前持仓股票的代码列表
            before_sell_holding_codes = list(holding_code_dict.keys())
    
    资金账户除权除息处理
    for _date in all_dates:
            print('BackTest at %s.' %_date)
            #当前持仓股票的代码列表
            before_sell_holding_codes = list(holding_code_dict.keys())
            """
            持仓股的除权除息处理
            如果当前不是第一个交易日,并且有持仓股票,则处理除权除息对持仓股的影响
            这里处理只考虑复权因子的变化,而实际的复权因子变化有可能是因为除权、除息以及配股等
            那么具体的持仓股变化要根据他们的不同逻辑来处理
            """
            if last_date is not None and len(before_sell_holding_codes)>0:
                #从daily数据集中查询出所有持仓股的前一个交易日的复权因子
                last_daily_cursor = DB_CONN['daily'].find(
                    {'code':{'$in':before_sell_holding_codes},'date':last_date,'index':False},
                    projection={'code':True,'au_factor':True})
                #构造一个dict,key为股票代码,value是上一个交易日的复权因子
                code_last_aufactor_dict = dict([(daily['code'],daily['au_factor'])for daily in last_daily_cursor])
    
                #从daily数据集中查询出所有持仓股的当前交易日的复权因子
                current_daily_cursor = DB_CONN['daily'].find(
                    {'code':{'$in':before_sell_holding_codes},'date':_date,'index':False},
                    projection={'code':True,'au_factor':True}
                    )
                #一只股票一只股票的处理
                for current_daily in current_daily_cursor:
                    #当前交易日的复权因子
                    current_aufactor =current_daily['au_factor']
                    #股票代码
                    code = current_daily['code']
                    #从持仓股中找到该股票的持仓数量
                    last_volume = holding_code_dict[code]['volume']
                    #如果该股票存在前一个交易日的复权因子,则对持仓数量进行调整
                    if code in code_last_aufactor_dict:
                        #上一个交易日的复权因子
                        last_aufactor = code_last_aufactor_dict[code]
                        # 计算复权因子变化后的持仓股票数量,如果复权因子不发生变化,那么持仓数量是不会发生变化的
                        #相关公式:
                        #市值不变:last_close * last_volume = pre_close * current_volume
                        #价格的关系:last_close * last_aufactor = pre_close * current_aufactor
                        #转换之后得到下面的公式
                        current_volume = int(last_volume *(current_aufactor / last_aufactor))
                        #改变持仓数量
                        holding_code_dict[code]['volume'] = current_volume
                        print('持仓量调整:%s,%6d,%10.6f,%6d,%10.6f'%(code,last_volume,last_aufactor,current_volume,current_aufactor))
    
    买入、卖出逻辑
    """
            卖出逻辑处理
            卖出价格是当日的开盘价,卖出的数量就是持仓股的数量,卖出后获得资金累加到账户的可用现金上
            """
            print('待卖股票池:',to_be_sold_codes,flush=True)
            #如果有待卖股票,则继续处理
            if len(to_be_sold_codes)>0:
                #从daily数据集中查询所有待卖股票的开盘价,这里用不复权的价格,以模拟出真实的交易情况
                sell_daily_cursor = DB_CONN['daily'].find(
                    {'code':{'$in':list(to_be_sold_codes)},'date':_date,'index':False,'is_trading':True},
                    projection={'code':True,'open':True})
                #一只股票一只股票处理
                for sell_daily in sell_daily_cursor:
                    #待卖股票
                    code = sell_daily['code']
                    #如果股票在持仓股中
                    if code in before_sell_holding_codes:
                        #获取持仓股
                        holding_stock = holding_code_dict[code]
                        #获取持仓数量
                        holding_volume = holding_stock['volume']
                        #卖出价格为当日开盘价
                        sell_price = sell_daily['open']
                        #卖出获得金额为持仓量*卖出价格
                        sell_amount = holding_volume * sell_price
                        #卖出得到的资金加入账户的可用现金上
                        cash += sell_amount
    
                        #获取该只股票的持仓成本
                        cost = holding_stock['cost']
                        #计算持仓的收益
                        single_profit = (sell_amount - cost) * 100/cost
                        print('卖出 %s,%6d,%6.2f,%8.2f,%4.2f'%(code,holding_volume,sell_price,sell_amount,single_profit))
                        #删除该只股票的持仓信息
                        del holding_code_dict[code]
                        to_be_sold_codes.remove(code)
            print("卖出后,现金:%10.f" %cash)
            """
            买入的逻辑处理
            买入的价格为当日的开盘价,每只股票可买入的金额为20万,如果可用现金少于20万,就不再买入
            """
            print('待买入股票池:',to_be_bought_codes,flush=True)
            if len(to_be_bought_codes)>0:
                #获取所有待买入股票的开盘价
                buy_daily_cursor = DB_CONN['daily'].find(
                    {'code':{'$in':list(to_be_bought_codes)},'date':_date,'is_trading':True,'index':False},
                    projection={'code':True,'open':True})
                #处理所有待买入的股票
                for buy_daily in buy_daily_cursor:
                    #判断现金是否够用
                    if cash > single_position:
                        #获取买入价格
                        buy_price = buy_daily['open']
                        #获取股票代码
                        code = buy_daily['code']
                        #获取可买入的数量,数量必须为正手数
                        volume = int(int(single_position/buy_price)/100)* 100          
                        #买入花费的成本为买入价格乘以实际的买入数量
                        buy_amount = buy_price * volume
                        #从现金中减去本次花费的成本
                        cash -= buy_amount
                        #增加持仓股
                        holding_code_dict[code] = {
                            'volume':volume,#持仓量
                            'cost':buy_amount,#持仓成本
                            'last_value':buy_amount#初始前一日的市值为持仓成本
                        }
                        print('买入 %s,%6d,%6.2f,%8.2f'%(code,volume,buy_price,buy_amount))
            print('买入后,现金:%10.2f'%cash)
    
    第二天待交易
    #持仓股票代码列表
            holding_codes = list(holding_code_dict.keys())
            
            """
            股票池调整日的处理逻辑:
            如果当前日期为股票池调整日,那么需要获取当期的备选股票列表,同时找到被调出的股票,如果这些被调出的股票为持仓股,则需要卖出
            """
            #判断当前交易日是否为股票交易日
            if _date in adjust_dates:
                print('股票调整日:%s,备选股票列表:'% _date,flush=True)
    
                #如果上期股票列表存在,也就是当前不是第一期股票,则将当前股票列表设为上期股票列表
                if this_phase_codes is not None:
                    last_phase_codes = this_phase_codes
    
                #获取当前股票列表
                this_phase_codes = date_codes_dict[_date]
                print(this_phase_codes,flush=True)
    
                #如果存在上期股票列表,则需要找出被调出的股票列表
                if last_phase_codes is not None:
                    #找到被调出股票池的股票列表
                    out_codes = find_out_stocks(last_phase_codes,this_phase_codes)
                    # 将所有被调出的且是在持仓中的股票添加到待卖股票集合中
                    for out_code in out_codes:
                        if out_code in holding_code_dict:
                            to_be_sold_codes.add(out_code)
            #检查是否有需要第二天卖出的股票
            for holding_code in holding_codes:
                if is_k_down_break_ma10(holding_code,_date):
                    to_be_sold_codes.add(holding_code)
            #检查是否需要第二天买入的股票
            to_be_bought_codes.clear()
            if this_phase_codes is not None:
                for _code in this_phase_codes:
                    if _code not in holding_codes and is_k_up_break_ma10(_code,_date):
                        to_be_bought_codes.add(_code)
    
    计算净值以及绘制收益曲线
     #计算总资产
            total_value = 0
    
            # 获取所有持仓股的当日收盘价
            holding_daily_cursor = DB_CONN['daily'].find(
                {'code': {'$in': holding_codes}, 'date': _date},
                projection={'close': True, 'code': True}
            )
    
            # 计算所有持仓股的总市值
            for holding_daily in holding_daily_cursor:
                code = holding_daily['code']
                holding_stock = holding_code_dict[code]
                # 单只持仓的市值等于收盘价乘以持仓量
                value = holding_daily['close'] * holding_stock['volume']
                # 总市值等于所有持仓股市值的累加之和
                total_value += value
    
                # 计算单只股票的持仓收益
                profit = (value - holding_stock['cost']) * 100 / holding_stock['cost']
                # 计算单只股票的单日收益
                one_day_profit = (value - holding_stock['last_value']) * 100 / holding_stock['last_value']
                # 更新前一日市值
                holding_stock['last_value'] = value
                print('持仓: %s, %10.2f, %4.2f, %4.2f' %
                      (code, value, profit, one_day_profit))
    
            # 总资产等于总市值加上总现金
            total_capital = total_value + cash
    
            # 获取沪深300的当日收盘值
            hs300_current_value = DB_CONN['daily'].find_one(
                {'code': '000300', 'index': True, 'date': _date},
                projection={'close': True})['close']
    
            print('收盘后,现金: %10.2f, 总资产: %10.2f' % (cash, total_capital))
            last_date = _date
            # 将当日的净值、收益和沪深300的涨跌幅放入DataFrame
            df_profit.loc[_date] = {
                'net_value': round(total_capital / 1e7, 2),
                'profit': round(100 * (total_capital - 1e7) / 1e7, 2),
                'hs300': round(100 * (hs300_current_value - hs300_begin_value) / hs300_begin_value, 2)
            }
    
        print(df_profit)
    
        #计算最大回撤
        drawdown = compute_drawdown(df_profit['net_value'])
        #计算年化收益和夏普比率
        annual_profit,sharpe_ratio = compute_sharpe_ratio(df_profit['net_value'])
    
        print('回测结果 %s - %s,年化收益:%7.3f,最大回撤:%7.3f,夏普比率:%4.3f'%(begin_date,end_date,annual_profit,drawdown,sharpe_ratio))
    
        #绘制收益曲线
        df_profit.plot(title='BackTest Result',y=['profit','hs300'],kind='line')
        plt.show()
    

    策略评价指标--年化收益:

    [Years = frac{TradingDays}{AnnualTradingDays} ]

    [NetValue=(1 + AnnualProfit)^{Years} ]

    [AnnualProfit=sqrt[Years]{NetValue}-1 ]

    [AnnualProfit = NetValue^{frac{1}{Years}}-1 ]

    策略评价指标--夏普比率:

    [ProfitMean = frac{1}{N}sum_{i=0}^{N}Profit_i ]

    [ProfitStd= sqrt{frac{1}{N}sum_{i=0}^{N}(Profit_i - ProfitMena)^2} ]

    [SharpeRatio= frac{AnnalProfit - R_f}{ProfitStd} ; R_f--无风险收益 ]

    [Sharpe Ratio = frac{E(R_P)-R_f}{sigma_p} ]

    最大回撤、年化收益、夏普比率的计算
    def compute_drawdown(net_values):
        """
        计算最大回撤
        :param net_values :净值列表
        """
        #最大回撤初始值设为0
        max_drawdown = 0
        size = len(net_values)
        index = 0
        #双层循环找到出最大回撤
        for net_value in net_values:
            #计算从当前开始直到结束,和当前净值相比的最大回撤
            for sub_net_value in net_values[index:]:
                #计算回撤
                drawdown = 1 - sub_net_value / net_value
                #如果当前的回撤大于已经计算的最大回撤,则当前回撤为最大回撤
                if drawdown > max_drawdown:
                    max_drawdown = drawdown
    
            index+=1
        return max_drawdown
    
    def compute_annual_profit(trading_days,net_value):
        """
        计算年化收益
        """
        annual_profit = 0
        #交易日数大于0,才计算年化收益
        if trading_days> 0:
            #计算年数
            years = trading_days / 245
            #计算年化收益
            annual_profit = pow(net_value,1/years)-1
        #将年化收益转化为百分数,保留两位小数
        annual_profit = round(annual_profit*100,2)
    
        return annual_profit
    
    def compute_sharpe_ratio(net_values):
        """
        计算夏普比率
        :param net_values:净值列表
        """
        #总交易日数
        trading_days = len(net_values)
        #所有收益的DataFrame
        profit_df = pd.DataFrame(columns=['profit'])
        #收益之后,初始化为第一天的收益
        profit_df.loc[0] = {'profit':round((net_values[0]-1)*100,2)}
        #计算每天的收益
        for index in range(1,trading_days):
            #计算每日的收益变换
            profit = (net_values[index] - net_values[index-1])/net_values[index-1]
            profit = round(profit*100 ,2)
            profit_df.loc[index] = {'profit':profit}
    
        #计算当日收益标准差
        profit_std = pow(profit_df.var()['profit'],1/2)
        #年化收益
        annual_profit = compute_annual_profit(trading_days,net_values[-1])
        #夏普比率
        sharpe_ratio = (annual_profit-4.75)/(profit_std*pow(245,1/2))
        return annual_profit,sharpe_ratio
    
    收益效果图展示:


    就目前的数据测试的结果效果并不是特别的好

  • 相关阅读:
    linux批量远程多服务器FTP并下载文件的脚本
    NPM更换国内源
    Win10禁用无用的服务
    JS测试
    FastAdmin导出
    VScode全局设置
    Vue路由history模式
    kill_devtmpfsi
    获取域名URL
    Axios去除Respones中的config、headers、request
  • 原文地址:https://www.cnblogs.com/whiteBear/p/13124013.html
Copyright © 2011-2022 走看看