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