Exemple #1
0
def backtest(begin_date, end_date):
    """
    策略回测。结束后打印出收益曲线(沪深300基准)、年化收益、最大回撤、

    :param begin_date: 回测开始日期
    :param end_date: 回测结束日期
    """

    # 初始现金1000万
    cash = 1E7
    # 单只股票的仓位是20万
    single_position = 2E5

    # 时间为key的净值、收益和同期沪深基准
    df_profit = pd.DataFrame(columns=['net_value', 'profit', 'hs300'])

    # 获取回测开始日期和结束之间的所有交易日,并且是按照正序排列
    all_dates = get_trading_dates(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())

        """
        持仓股的除权除息处理
        如果当前不是第一个交易日,并且有持仓股票,则处理除权除息对持仓股的影响
        这里的处理只考虑复权因子的变化,而实际的复权因子变化有可能是因为除权、除息以及配股,
        那么具体的持仓股变化要根据它们的不同逻辑来处理
        """
        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={'open': True, 'code': 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.2f' % 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_rsi_over_sold(holding_code, _date):
            #if is_boll_break_up(holding_code, _date):
            #if is_macd_gold(holding_code, _date):
            #if is_fractal_up(holding_code, _date):
            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_rsi_over_bought(_code, _date):
                #if _code not in holding_codes and is_boll_break_down(_code, _date):
                #if _code not in holding_codes and is_macd_gold(_code, _date):
                #if _code not in holding_codes and is_fractal_down(_code, _date):
                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, 'index':False},
            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.2f' %
          (begin_date, end_date, annual_profit, drawdown, sharpe_ratio))

    # 绘制收益曲线
    df_profit.plot(title='Backtest Result', y=['profit', 'hs300'], kind='line')
    plt.show()
def backtest(begin_date, end_date):
    """
    策略回测。结束后打印出收益曲线(沪深300基准)、年化收益、最大回撤、

    :param begin_date: 回测开始日期
    :param end_date: 回测结束日期
    """
    cash = 1E7
    single_position = 2E5

    # 时间为key的净值、收益和同期沪深基准
    df_profit = pd.DataFrame(columns=['net_value', 'profit', 'hs300'])

    all_dates = get_trading_dates(begin_date, end_date)

    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 = stock_pool(begin_date, end_date)

    last_phase_codes = None
    this_phase_codes = None
    to_be_sold_codes = set()
    to_be_bought_codes = set()
    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())

        # 处理复权
        if last_date is not None and len(before_sell_holding_codes) > 0:
            last_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': before_sell_holding_codes
                    },
                    'date': last_date,
                    'index': False
                },
                projection={
                    'code': True,
                    'au_factor': True,
                    '_id': False
                })

            code_last_aufactor_dict = dict()
            for last_daily in last_daily_cursor:
                code_last_aufactor_dict[
                    last_daily['code']] = last_daily['au_factor']

            current_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': before_sell_holding_codes
                    },
                    'date': _date,
                    'index': False
                },
                projection={
                    'code': True,
                    'au_factor': True,
                    '_id': False
                })

            for current_daily in current_daily_cursor:
                print(current_daily['code'], _date)
                current_aufactor = current_daily['au_factor']
                code = current_daily['code']
                before_volume = holding_code_dict[code]['volume']
                if code in code_last_aufactor_dict:
                    last_aufactor = code_last_aufactor_dict[code]
                    after_volume = int(before_volume *
                                       (current_aufactor / last_aufactor))
                    holding_code_dict[code]['volume'] = after_volume
                    print('持仓量调整:%s, %6d, %10.6f, %6d, %10.6f' %
                          (code, before_volume, last_aufactor, after_volume,
                           current_aufactor))

        # 卖出
        print('待卖股票池:', to_be_sold_codes, flush=True)
        if len(to_be_sold_codes) > 0:
            sell_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': list(to_be_sold_codes)
                    },
                    'date': _date,
                    'index': False,
                    'is_trading': True
                },
                projection={
                    'open': True,
                    'code': 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.2f' % cash)

        # 买入
        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

        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
        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)
        }

    df_profit.plot(title='Backtest Result', y=['profit', 'hs300'], kind='line')
    plt.show()
Exemple #3
0
def backtest(begin_date, end_date, fun_sell, fun_buy):
    '''
    回测系统
    parameter:
    fun_sell: 卖出信号
    fun_buy: 买入信号函数
    '''
    # 设置初始值
    cash = 1E7
    single_positon = 2E5
    df_profit = pd.DataFrame(columns=['net_value', 'profit', 'hs300'])
    # 得到回测日期
    all_dates = get_trading_dates(begin_date, end_date)
    adjust_dates, date_codes_dict = stock_pool(begin_date, end_date)
    hs300_begin_value = daily_collection.find_one(
        {
            'code': '000300',
            'index': True,
            'date': adjust_dates[0]
        },
        projection={
            'close': True,
            '_id': False
        })['close']

    holding_code_dict = dict()
    last_date = None
    this_phase_codes = None
    last_phase_codes = None
    to_be_bought_codes = set()
    to_be_sold_codes = set()

    for _date in all_dates:
        print('Back test begin 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:
            # produce_au(before_sell_holding_codes)
            last_daily_cursor = daily_collection.find(
                {
                    'code': {
                        '$in': before_sell_holding_codes
                    },
                    'date': last_date
                },
                projection={
                    'code': True,
                    '_id': False,
                    'au_factor': True
                }).hint([('code', 1), ('date', 1)])

            code_last_aufactor_dict = dict()
            for last_daily in last_daily_cursor:
                code_last_aufactor_dict[
                    last_daily['code']] = last_daily['au_factor']

            current_daily_cursor = daily_collection.find(
                {
                    'code': {
                        '$in': before_sell_holding_codes
                    },
                    'date': _date
                },
                projection={
                    'code': True,
                    '_id': False,
                    'au_factor': True
                }).hint([('code', 1), ('date', 1)])

            for current_daily in current_daily_cursor:
                current_aufactor = current_daily['au_factor']
                code = current_daily['code']
                before_volume = holding_code_dict[code]['volume']
                if code in code_last_aufactor_dict:
                    last_aufactor = code_last_aufactor_dict[code]
                    after_volume = int(before_volume *
                                       (current_aufactor / last_aufactor))
                    holding_code_dict[code]['volume'] = after_volume
                    print(
                        'hold volume adjust: code: %s, %6d, %10.6f, %6d, %10.6f'
                        % (code, before_volume, last_aufactor, after_volume,
                           current_aufactor))

        # 卖出上一期持仓股
        # print('to sell stocks: %s' % to_be_sold_codes, flush=True)
        if len(to_be_sold_codes) > 0:
            sell_daily_cursor = daily_collection.find(
                {
                    'code': {
                        '$in': list(to_be_sold_codes)
                    },
                    'date': _date,
                    'index': True,
                    'is_trading': True
                },
                projection={
                    'open': True,
                    'code': True,
                    '_id': False
                }).hint([('code', 1), ('date', -1)])

            for sell_daily in sell_daily_cursor:
                sell_code = sell_daily['code']
                if sell_code in before_sell_holding_codes:
                    holding_stock = before_sell_holding_codes[code]
                    sell_price = sell_daily['open']
                    holding_volume = holding_stock['volume']
                    sell_amount = holding_volume * sell_price
                    cash += sell_amount

                    cost = holding_stock['cost']
                    single_profit = (sell_amount - cost) * 100 / cost
                    print('sell: %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('cash after sell: %10.2f' % cash)

        # 买入这一期股票
        # print('to buy stocks: ', to_be_bought_codes, flush=True)
        if len(to_be_bought_codes) > 0:
            buy_daily_cursor = daily_collection.find(
                {
                    'code': {
                        '$in': list(to_be_bought_codes)
                    },
                    'date': _date,
                    'index': False,
                    'is_trading': True
                },
                projection={
                    'code': True,
                    '_id': False,
                    'open': True
                }).hint([('code', 1), ('date', -1)])

            for buy_daily in buy_daily_cursor:
                if cash > single_positon:
                    code = buy_daily['code']
                    buy_price = buy_daily['open']
                    buy_volume = int(
                        int(single_positon / buy_price) / 100) * 100
                    buy_amount = buy_price * buy_volume
                    cash -= buy_amount
                    holding_code_dict[code] = {
                        'volume': buy_volume,
                        'last_value': buy_amount,
                        'cost': buy_amount
                    }
                    print('buy %s, %6d, %6.2f, %8.2f' %
                          (code, buy_volume, buy_price, buy_amount))

        print('cash after buy: %10.2f' % cash)

        # 计算收益率
        holding_codes = list(holding_code_dict.keys())

        if _date in adjust_dates:
            print('stock pool adjust date: %s' % _date)

            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 fun_sell(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 fun_buy(_code, _date):
                    to_be_bought_codes.add(_code)

        # 计算持仓股票市值
        total_value = 0
        holding_daily_cursor = daily_collection.find(
            {
                'code': {
                    '$in': holding_codes
                },
                'date': _date,
                'index': False
            },
            projection={
                'code': True,
                '_id': False,
                'close': True
            }).hint([('code', 1), ('date', -1)])

        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('holding stocks: %s, %10.2f, %4.2f, %4.2f' %
            #         (code, value, profit, one_day_profit))
        # 计算总资产
        total_capital = total_value + cash

        # 计算基准收益
        hs300_current_value = daily_collection.find_one(
            {
                'code': '000300',
                'date': _date,
                'index': True
            },
            projection={
                'code': True,
                'close': True,
                '_id': False
            })['close']
        print('after close, cash: %10.2f, total_capital: %10.2f' %
              (cash, total_capital))
        dt_date = datetime.strptime(_date, '%Y-%m-%d')
        df_profit.loc[dt_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)
        }
    drawdown = compute_drawdown(df_profit['net_value'])
    annual_profit, sharpe_ratio = compute_sharpe_ratio(df_profit['net_value'])
    print(
        'Backtest result: %s - %s,  annual_profit: %7.3f, maxdrawdown:%7.3f, sharpe ratio: %4.2f'
        % (begin_date, end_date, annual_profit, drawdown, sharpe_ratio))

    df_profit.plot(title='Backtest Result', y=['profit', 'hs300'], kind='line')
    plt.show()
Exemple #4
0
def backtest(begin_date,
             end_date,
             stop_method=None,
             pos_method='equal',
             is_saving=False,
             save_name=None):
    """
    Arguments:
        begin_date: 回测开始日期
        end_date: 回测结束日期
        stop_method : 止损方式
            None : 无止损
            fixed : 固定比例止损
            float : 浮动止损
            ATR_float_dynamic : 动态ATR浮动止损
            ATR_float_static : 静态ATR浮动止损
        pos_method : 头寸分配方式
            equal : 均仓分配头寸
            atr : 按照ATR分配头寸
    
    Returns:
        Account: 数据类型,dict
            
            init_assets : 初始资产, 默认1E7
            history_table : 交割单
            net_value : 每日净值
            final_net_value : 最终日净值
            profit : 收益
            day_profit : 每日收益
            positions : 每日仓位
            stop_loss : 止损的方式和止损参数
            position_manage : 头寸管理方式和相关参数
    """
    # 记录止损时间点
    #    stop_lose_position_date_current = []
    #    stop_lose_position_date = []

    # 记录回测账户信息
    Account = {}

    # 仓位相关的初始化
    position_manage = {}
    if pos_method == 'equal':
        single_position = 2E5
        position_manage['头寸分配方式'] = '均仓'
        Account['position_manage'] = position_manage
    elif pos_method == 'atr':
        single_position = 0
        position_manage['头寸分配方式'] = 'ATR分配头寸'
        position_manage['ATR_WIN'] = ATR_WIN
        position_manage['RISK_RATIO'] = RISK_RATIO
        Account['position_manage'] = position_manage

    positions = pd.Series()  # 记录每日仓位信息
    stop_loss = {}

    cash = INIT_ASSETS
    init_assets = cash
    Account['init_assets'] = init_assets
    Account['start'] = begin_date
    Account['end'] = end_date

    if stop_method is None:
        Account['stop_loss'] = '无止损'
    elif stop_method == 'fixed':
        stop_loss['单日跌幅比例'] = SINGLE_DAY_MAX_DROP_RATE
        stop_loss['累计跌幅比例'] = MAX_DROP_RATE
        stop_loss['止损方式'] = '固定比例止损'
        Account['stop_loss'] = stop_loss
    elif stop_method == 'float':
        stop_loss['跌幅比例'] = MAX_DROP_RATE
        stop_loss['止损方式'] = '浮动止损'
        Account['stop_loss'] = stop_loss
    elif (stop_method == 'ATR_float_dynamic') or (stop_method
                                                  == 'ATR_float_static'):
        stop_loss['ATR_WIN'] = ATR_WIN
        stop_loss['ATR_RATIO'] = ATR_RATIO
        if stop_method == 'ATR_float_dynamic':
            stop_loss['止损方式'] = '动态ATR浮动止损'
        elif stop_method == 'ATR_float_static':
            stop_loss['止损方式'] = '静态ATR浮动止损'
        Account['stop_loss'] = stop_loss

    # 时间为key的净值、收益和同期沪深基准
    df_profit = pd.DataFrame(columns=['net_value', 'profit', 'hs300'])
    # 时间为key的单日收益和同期沪深基准
    df_day_profit = pd.DataFrame(columns=['profit', 'hs300'])

    all_dates = get_trading_dates(begin_date, end_date)

    hs300_begin_value = DB_CONN['daily'].find_one(
        {
            'code': '000300',
            'date': all_dates[0],
            'index': True
        },
        projection={'close': True})['close']

    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()
    holding_code_dict = dict()
    last_date = None

    last_entry_dates = {}  # 用于记录入场时间
    history_table = pd.DataFrame()  # 记录 交割单

    last_total_capital = 1e7  # 前一天的总资产值,初始值为初始总资产
    last_hs300_close = hs300_begin_value  # 前一天的HS300值,初始值为第一天的值
    net_value = 1  # 净值

    count = 0
    # 按照日期一步步回测
    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:
            last_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': before_sell_holding_codes
                    },
                    'date': last_date,
                    'index': False
                },
                projection={
                    'code': True,
                    'au_factor': True,
                    '_id': False
                })

            code_last_aufactor_dict = dict()
            for last_daily in last_daily_cursor:
                code_last_aufactor_dict[
                    last_daily['code']] = last_daily['au_factor']

            current_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': before_sell_holding_codes
                    },
                    'date': _date,
                    'index': False
                },
                projection={
                    'code': True,
                    'au_factor': True,
                    '_id': False
                })

            for current_daily in current_daily_cursor:
                print(current_daily['code'], _date)
                current_aufactor = current_daily['au_factor']
                code = current_daily['code']
                before_volume = holding_code_dict[code]['volume']
                if code in code_last_aufactor_dict:
                    last_aufactor = code_last_aufactor_dict[code]
                    after_volume = int(before_volume *
                                       (current_aufactor / last_aufactor))
                    holding_code_dict[code]['volume'] = after_volume
                    print('持仓量调整:%s, %6d, %10.6f, %6d, %10.6f' %
                          (code, before_volume, last_aufactor, after_volume,
                           current_aufactor))

        # 卖出
        print('待卖股票池:', to_be_sold_codes, flush=True)
        if len(to_be_sold_codes) > 0:
            sell_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': list(to_be_sold_codes)
                    },
                    'date': _date,
                    'index': False,
                    'is_trading': True
                },
                projection={
                    'open': True,
                    'code': True,
                    'low_limit': True
                })

            for sell_daily in sell_daily_cursor:
                code = sell_daily['code']

                # 若开盘价是跌停价不准卖出
                open_price = sell_daily['open']
                low_limit = sell_daily['low_limit']

                if (code in before_sell_holding_codes) & (open_price >
                                                          low_limit):
                    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

                    last_entry_dates[code] = None

                    print('卖出 %s, %6d, %6.2f, %8.2f, %4.2f' %
                          (code, holding_volume, sell_price, sell_amount,
                           single_profit))

                    # 记录 交易记录
                    count += 1
                    _order = {
                        'datetime': _date,
                        'code': code,
                        'price': sell_price,
                        'amount': -1 * holding_volume,
                        'cash': cash
                    }
                    temp = pd.DataFrame(data=_order, index=[count])
                    history_table = pd.concat([history_table, temp])

                    del holding_code_dict[code]
                    to_be_sold_codes.remove(code)

        print('卖出后,现金: %10.2f' % cash)

        # 买入
        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,
                    'index': False,
                    'is_trading': True
                },
                projection={
                    'code': True,
                    'open': True,
                    'high_limit': True
                })

            for buy_daily in buy_daily_cursor:

                # 若开盘价是涨停价不准买入
                open_price = buy_daily['open']
                high_limit = buy_daily['high_limit']
                code = buy_daily['code']

                # ===========================ATR分配头寸 code start=========================
                if pos_method == 'atr':
                    ATR = calc_ATR(code, _date)
                    if ATR is not None:
                        single_position = init_assets * RISK_RATIO / (
                            ATR_RATIO * ATR) // 100 * 100
                    else:
                        single_position = 0

                if (cash > single_position) & (open_price < high_limit) & (
                        single_position != 0):
                    buy_price = buy_daily['open']

                    volume = int(int(single_position / buy_price) / 100) * 100
                    if volume > 100:
                        buy_amount = buy_price * volume
                        cash -= buy_amount
                        holding_code_dict[code] = {
                            'volume': volume,
                            'cost': buy_amount,
                            'last_value': buy_amount
                        }

                        last_entry_dates[code] = _date

                        print('买入 %s, %6d, %6.2f, %8.2f' %
                              (code, volume, buy_price, buy_amount))

                        # 记录 交易记录
                        count += 1
                        _order = {
                            'datetime': _date,
                            'code': code,
                            'price': buy_price,
                            'amount': volume,
                            'cash': cash
                        }
                        temp = pd.DataFrame(data=_order, index=[count])
                        history_table = pd.concat([history_table, temp])

        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)

            # ===========================止损判断 code start=========================
            if stop_method is not None:
                stop_loss_positions(holding_code, _date, last_entry_dates,
                                    to_be_sold_codes, stop_method)

        # 检查是否有需要第二天买入的股票
        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
        positions.loc[_date] = total_value / total_capital

        hs300_current_value = DB_CONN['daily'].find_one(
            {
                'code': '000300',
                'date': _date,
                'index': True
            },
            projection={'close': True})['close']

        print('收盘后,现金: %10.2f, 总资产: %10.2f' % (cash, total_capital))
        last_date = _date
        net_value = np.round(total_capital / 1e7, 4)
        df_profit.loc[_date] = {
            'net_value':
            np.round(total_capital / 1e7, 4),
            'profit':
            np.round(100 * (total_capital - 1e7) / 1e7, 4),
            'hs300':
            np.round(
                100 * (hs300_current_value - hs300_begin_value) /
                hs300_begin_value, 4)
        }
        # 计算单日收益
        df_day_profit.loc[_date] = {
            'profit':
            np.round(
                100 * (total_capital - last_total_capital) /
                last_total_capital, 4),
            'hs300':
            np.round(
                100 * (hs300_current_value - last_hs300_close) /
                last_hs300_close, 4)
        }
        # 暂存当日的总资产和HS300,作为下一个交易日计算单日收益的基础
        last_total_capital = total_capital
        last_hs300_close = hs300_current_value

    Account['history_table'] = history_table
    Account['net_value'] = df_profit['net_value']
    Account['final_net_value'] = net_value
    Account['profit'] = df_profit
    Account['day_profit'] = df_day_profit
    Account['positions'] = positions

    try:
        if is_saving:
            if save_name is None:
                print('保存失败,没有输入save_name, 请使用save_file函数重新保存')
            else:
                save_file(Account, save_name)
    except:
        print('保存失败,请使用save_file函数重新保存')

    return Account
Exemple #5
0
def backtest(begin_date, end_date):
    """
    策略回测。结束后打印出收益曲线(沪深300基准)、年化收益、最大回撤、

    :param begin_date: 回测开始日期
    :param end_date: 回测结束日期
    """
    stop_lose_position_date_current = []
    stop_lose_position_date = []

    cash = 1E7
    single_position = 2E5

    # 时间为key的净值、收益和同期沪深基准
    df_profit = pd.DataFrame(columns=['net_value', 'profit', 'hs300'])

    all_dates = get_trading_dates(begin_date, end_date)

    hs300_begin_value = DB_CONN['daily'].find_one(
        {
            'code': '000300',
            'date': all_dates[0],
            'index': True
        },
        projection={'close': True})['close']

    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()
    holding_code_dict = dict()
    last_date = None
    last_entry_dates = {}

    # 按照日期一步步回测
    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:
            last_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': before_sell_holding_codes
                    },
                    'date': last_date,
                    'index': False
                },
                projection={
                    'code': True,
                    'au_factor': True,
                    '_id': False
                })

            code_last_aufactor_dict = dict()
            for last_daily in last_daily_cursor:
                code_last_aufactor_dict[
                    last_daily['code']] = last_daily['au_factor']

            current_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': before_sell_holding_codes
                    },
                    'date': _date,
                    'index': False
                },
                projection={
                    'code': True,
                    'au_factor': True,
                    '_id': False
                })

            for current_daily in current_daily_cursor:
                print(current_daily['code'], _date)
                current_aufactor = current_daily['au_factor']
                code = current_daily['code']
                before_volume = holding_code_dict[code]['volume']
                if code in code_last_aufactor_dict:
                    last_aufactor = code_last_aufactor_dict[code]
                    after_volume = int(before_volume *
                                       (current_aufactor / last_aufactor))
                    holding_code_dict[code]['volume'] = after_volume
                    print('持仓量调整:%s, %6d, %10.6f, %6d, %10.6f' %
                          (code, before_volume, last_aufactor, after_volume,
                           current_aufactor))

        # 卖出
        print('待卖股票池:', to_be_sold_codes, flush=True)
        if len(to_be_sold_codes) > 0:
            sell_daily_cursor = DB_CONN['daily'].find(
                {
                    'code': {
                        '$in': list(to_be_sold_codes)
                    },
                    'date': _date,
                    'index': False,
                    'is_trading': True
                },
                projection={
                    'open': True,
                    'code': True,
                    'low_limit': True
                })

            for sell_daily in sell_daily_cursor:
                code = sell_daily['code']

                # 若开盘价是跌停价不准卖出
                open_price = sell_daily['open']
                low_limit = sell_daily['low_limit']

                if (code in before_sell_holding_codes) & (open_price >
                                                          low_limit):
                    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

                    last_entry_dates[code] = None

                    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.2f' % cash)

        # 买入
        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,
                    'index': False,
                    'is_trading': True
                },
                projection={
                    'code': True,
                    'open': True,
                    'high_limit': True
                })

            for buy_daily in buy_daily_cursor:

                # 若开盘价是涨停价不准买入
                open_price = buy_daily['open']
                high_limit = buy_daily['high_limit']

                if (cash > single_position) & (open_price < high_limit):
                    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
                    }

                    last_entry_dates[code] = _date

                    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)
            """
            止损条件:
                1.当日亏损超过3%
                2.累计亏损超过10%
            满足其一就卖出
            注意,这里的回测逻辑是无法应用到模拟盘的,因为用了当天的收盘价去计算亏损;
            当然在回测里没问题,毕竟是第二天要卖出的股票,所以姑且当做收盘后的判断吧;
            """
            # 当天收盘价
            current_close = DB_CONN['daily'].find_one({
                'code': holding_code,
                'date': _date,
                'index': False
            })['close']

            # 买入时的价格和日期
            entry_date = last_entry_dates[holding_code]
            entry_daily_cursor = DB_CONN['daily'].find_one({
                'code': holding_code,
                'date': entry_date,
                'index': False
            })
            entry_price = entry_daily_cursor['open']
            entry_date_close = entry_daily_cursor['close']

            if (entry_date == _date) & (((entry_price - entry_date_close) /
                                         entry_price) > CURRENT_MAX_DROP_RATE):
                to_be_sold_codes.add(holding_code)
                stop_lose_position_date_current.append(_date)

            elif ((entry_price - current_close) / entry_price) > MAX_DROP_RATE:
                to_be_sold_codes.add(holding_code)
                stop_lose_position_date.append(_date)

        # 检查是否有需要第二天买入的股票
        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

        hs300_current_value = DB_CONN['daily'].find_one(
            {
                'code': '000300',
                'date': _date,
                'index': True
            },
            projection={'close': True})['close']

        print('收盘后,现金: %10.2f, 总资产: %10.2f' % (cash, total_capital))
        last_date = _date
        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(np.sort(list(set(stop_lose_position_date))))
    print(np.sort(list(set(stop_lose_position_date_current))))
    df_profit.plot(title='Backtest Result', y=['profit', 'hs300'], kind='line')
    plt.show()