def fixing_is_st(start, end): # 第一阶段 df = pd.read_excel('data/stock_basic.xlsx', header=0, dtype={'code':str}) df = df.set_index('code') codes = df[df['是否ST过'] == 1].index.tolist() total = len(codes) # all_dates = get_trading_dates(start, end) daily = DB_CONN['daily'] excel_name = 'data/st_info.xlsx' for i in range(4): if i == 0: all_dates = get_trading_dates('2015-01-01', '2015-12-31') elif i == 1: all_dates = get_trading_dates('2016-01-01', '2016-12-31') if i == 2: all_dates = get_trading_dates('2017-01-01', '2017-12-31') elif i == 3: all_dates = get_trading_dates('2018-01-01', '2018-09-30') print('数据读取中') df = pd.read_excel(excel_name, i, header=0, dtype={'code':str}) df = df.set_index(['code','state']) df.columns = df.columns.astype(np.datetime64) df.columns = df.columns.to_period('D') df.columns = df.columns.astype('str') print('数据读取完毕') for j, code in enumerate(codes): update_requests = [] for date in all_dates: try: st_state = df.xs([code])[date]['是否ST'] sst_state = df.xs([code])[date]['是否*ST'] if (st_state == '否') and (sst_state == '否'): is_st_flag = False else: is_st_flag = True update_requests.append( UpdateOne( {'code':code, 'date':date, 'index':False}, {'$set':{'is_st':is_st_flag}} ) ) except: print('something is wrong, code : %s, date : %s' % (code, date)) if len(update_requests)>0: update_result = daily.bulk_write(update_requests, ordered=False) print('第%s年填充进度: %s/%s, 字段名: is_st,数据集:%s,插入:%4d条,更新:%4d条' % (i+1, j+1, total, 'daily', update_result.upserted_count, update_result.modified_count), flush=True)
def crawl_basic(begin_date=None, end_date=None): """ 抓取指定时间范围内的股票基础信息 :param begin_date: 开始日期 :param end_date: 结束日期 """ # 如果没有指定开始日期,则默认为前一日 if begin_date is None: begin_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') # 如果没有指定结束日期,则默认为前一日 if end_date is None: end_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') # 获取指定日期范围的所有交易日列表 all_dates = get_trading_dates(begin_date, end_date) # 按照每个交易日抓取 for date in all_dates: try: # 抓取当日的基本信息 crawl_basic_at_date(date) except: print('抓取股票基本信息时出错,日期:%s' % date, flush=True)
def fill_daily_k_at_suspension_days(begin_date=None, end_date=None): """ :param begin_date: :param end_date: :return: """ before = datetime.now() - timedelta(days=1) while 1: last_trading_date = before.strftime('%Y-%m-%d') basic_cursor = DB_CONN['basic'].find({'date': last_trading_date}, projection={ 'code': True, 'timeToMarket': True, '_id': False }, batch_size=5000) basics = [basic for basic in basic_cursor] if len(basics) > 0: break before -= timedelta(days=1) all_dates = get_trading_dates(begin_date, end_date) fill_daily_k_at_suspension_days_at_date_one_collection( basics, all_dates, 'daily') fill_daily_k_at_suspension_days_at_date_one_collection( basics, all_dates, 'daily_hfq')
def fill_high_and_low_price_between(start, end): all_trades = get_trading_dates(start, end) codes = ts.get_stock_basics().index.tolist() # get_all_codes() # basics = getBasics() total = len(all_trades) for i, date in enumerate(all_trades): fill_high_and_low_price_at_one_date(codes, date) print('涨跌停计算进度: (%s/%s)' % (i + 1, total))
def fill_is_trading_between(start, end): """ """ pass all_dates = get_trading_dates(start, end) for date in all_dates: fill_single_date_is_trading(date, 'daily') fill_single_date_is_trading(date, 'daily_hfq')
def fill_daily_k_at_suspension_days(begin_date=None, end_date=None): """ 填充指定日期范围内,股票停牌日的行情数据。 填充时,停牌的开盘价、最高价、最低价和收盘价都为最近一个交易日的收盘价,成交量为0, is_trading是False :param begin_date: 开始日期 :param end_date: 结束日期 """ # 当前日期的前一天 before = datetime.now() - timedelta(days=1) # 找到据当前最近一个交易日的所有股票的基本信息 basics = [] while 1: # 转化为str last_trading_date = before.strftime('%Y-%m-%d') # 因为TuShare的基本信息最早知道2016-08-09,所以如果日期早于2016-08-09 # 则结束查找 if last_trading_date < '2016-08-09': break # 找到当日的基本信息 basic_cursor = DB_CONN['basic'].find( {'date': last_trading_date}, # 填充时需要用到两个字段股票代码code和上市日期timeToMarket, # 上市日期用来判断 projection={ 'code': True, 'timeToMarket': True, '_id': False }, # 一次返回5000条,可以降低网络IO开销,提高速度 batch_size=5000) # 将数据放到basics列表中 basics = [basic for basic in basic_cursor] # 如果查询到了数据,在跳出循环 if len(basics) > 0: break # 如果没有找到数据,则继续向前一天 before -= timedelta(days=1) # 获取指定日期范围内所有交易日列表 all_dates = get_trading_dates(begin_date, end_date) # 填充daily数据集中的停牌日数据 fill_daily_k_at_suspension_days_at_date_one_collection( basics, all_dates, 'daily') # 填充daily_hfq数据中的停牌日数据 fill_daily_k_at_suspension_days_at_date_one_collection( basics, all_dates, 'daily_hfq')
def fill_is_trading_between(begin_date=None, end_date=None): """ 填充指定时间段内的is_trading字段 :param begin_date: 开始日期 :param end_date: 结束日期 """ all_dates = get_trading_dates(begin_date, end_date) for date in all_dates: fill_single_date_is_trading(date, 'daily') fill_single_date_is_trading(date, 'daily_hfq')
def fill_is_trading_between(begin_date=None, end_date=None): """ 填充指定时间段内的is_trading字段 :param begin_date: 开始日期 :param end_date: 结束日期 """ all_dates = get_trading_dates(begin_date, end_date) total = len(all_dates) for i,date in enumerate(all_dates): fill_single_date_is_trading(date, 'daily') fill_single_date_is_trading(date, 'daily_hfq') print('is_trading字段填充进度: (%s/%s)' % (i+1, total))
def get_tushare_code(begin_date='2000-01-01', end_date=None): if (end_date is None): end_date = datetime.now().strftime('%Y-%m-%d') # 初始化pro接口 pro = ts.pro_api( 'f3ef4ac4dc04104e0573aa75c29aef70f30837a416baf6cd1a0f8e81') tradingdays_list = get_trading_dates(begin_date=begin_date, end_date=end_date) # tradingdays_list = get_trade_days(begin_date= begin_date,end_date = end_date) codes = set() for day in tradingdays_list: data = pro.daily(trade_date=day.replace('-', '')) codes = codes | set(data.ts_code) return list(codes).sort()
def crawl_basic(start, end=None): """ """ end = start if end is None else end start = str(start)[0:10] end = str(end)[0:10] all_dates = get_trading_dates(start, end) for date in all_dates: try: crawl_basic_at_date(date) except: print('抓取股票基本信息时出错,日期:%s' % date, flush=True)
def fill_is_trading_between(begin_date=None, end_date=None): """ 填充指定时间段内的is_trading字段 :param begin_date: 开始日期 :param end_date: 结束日期 """ # 获取指定日期范围的所有交易日列表,按日期正序排列 all_dates = get_trading_dates(begin_date, end_date) # 循环填充所有交易日的is_trading字段 for date in all_dates: # 填充daily数据集 fill_single_date_is_trading(date, 'daily') # 填充daily_hfq数据集 fill_single_date_is_trading(date, 'daily_hfq')
def fill_is_trading_between(begin_date=None, end_date=None): """ 填充指定时间段内的is_trading字段 :param begin_date: 开始日期 :param end_date: 结束日期 """ all_dates = get_trading_dates(begin_date, end_date) total = len(all_dates) for i, date in enumerate(all_dates): _tic = time.process_time() fill_single_date_is_trading(date, 'daily') fill_single_date_is_trading(date, 'daily_hfq') _toc = time.process_time() expect_time = (_toc - _tic) * (total - i - 1) print('is_trading字段填充进度: (%s/%s), 预计还需要%.2fs' % (i + 1, total, expect_time))
def fill_is_trading(date=None): """ 为日线数据增加is_trading字段,表示是否交易的状态,True - 交易 False - 停牌 从Tushare来的数据不包含交易状态,也不包含停牌的日K数据,为了系统中使用的方便,我们需要填充停牌是的K数据。 一旦填充了停牌的数据,那么数据库中就同时包含了停牌和交易的数据,为了区分这两种数据,就需要增加这个字段。 在填充该字段时,要考虑到是否最坏的情况,也就是数据库中可能已经包含了停牌和交易的数据,但是却没有is_trading 字段。这个方法通过交易量是否为0,来判断是否停牌 """ if date is None: all_dates = get_trading_dates() else: all_dates = [date] for date in all_dates: fill_single_date_is_trading(date, 'daily') fill_single_date_is_trading(date, 'daily_hfq')
def crawl_basic(start, end=None): """ """ end = start if end is None else end start = str(start)[0:10] end = str(end)[0:10] all_dates = get_trading_dates(start, end) if all_dates is None: print('没有获取到交易日历') total = len(all_dates) for i, date in enumerate(all_dates): try: crawl_basic_at_date(date) except: print('抓取股票基本信息时出错,日期:%s' % date, flush=True) print('基础信息数据获取进度: (%s/%s)' % (i + 1, total))
def fill_is_trading_between(begin_date=None, end_date=None, dates=None): ''' 填充指定时间段内的is_trading字段 parameter: begin_date: 开始日期 end_date: 结束日期 ''' # 得到时间段 if dates is None: all_trading_dates = get_trading_dates(begin_date, end_date) else: if isinstance(dates, list) is False: dates = [dates] all_trading_dates = dates # 填充单个数据 for trading_date in all_trading_dates: fill_single_date_is_trading(trading_date, 'daily_none') fill_single_date_is_trading(trading_date, 'daily_hfq')
def crawl_basic(begin_date=None, end_date=None): """ 抓取指定时间范围内的股票基础信息 :param begin_date: 开始日期 :param end_date: 结束日期 """ if begin_date is None: begin_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') if end_date is None: end_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') all_dates = get_trading_dates(begin_date, end_date) for date in all_dates: try: crawl_basic_at_date(date) except: print('抓取股票基本信息时出错,日期:%s' % date, flush=True)
def threads_supension_days(begin_date=None, end_date=None, dates=None): ''' 多线程补充停牌数据 使用说明: 使用时在main中加入如下代码: dates, threads = threads_supension_days() for i in range(len(dates)): threads[i].start() for i in range(len(dates)): threads[i].join() ''' # codes = get_all_codes() if dates is None: dates = get_trading_dates(begin_date, end_date) threads = [] # dates = get_trading_dates() for date in dates: t = threading.Thread(target=fill_daily_k_supension_days, args=(None, None, date)) threads.append(t) return dates, threads
def threads_fill_is_trading(): ''' 多线程补充字段:is_trading 使用说明: 使用时在main中加入如下代码: dates, threads = threads_fill_is_trading() for i in range(len(dates)): threads[i].start() for i in range(len(dates)): threads[i].join() ''' # codes = get_all_codes() dates = get_trading_dates() threads = [] # dates = get_trading_dates() for date in dates: t = threading.Thread(target=fill_is_trading_between, args=(None, None, date)) threads.append(t) length = len(dates) return length, threads
def crawl_basic(self, begin_date=None, end_date=None): ''' 从tushare中抓取指定时间段的基本数据 parameter: begin_date: 开始日期 end_date: 结束日期 ''' # 没有指定日期范围时设置日期 if begin_date is None: begin_date = '2016-08-08' if end_date is None: end_date = datetime.now().strftime('%Y-%m-%d') # 得到日期内所有的交易日 all_dates = get_trading_dates(begin_date, end_date) for date in all_dates: # 从数据库中拿到需要补充基本信息的股票代码 # 从tushare中得到股票代码的基本数据 # 将股票代码的基本数据补充到原有的数据当中 # 保存更新完的股票数据 self.crawl_basic_at_date(date)
def threads_dates(begin_date, end_date, fun): ''' date 多线程函数 有几个date就有几个线程 fun:需要多线程的函数 使用说明: 使用时在main中加入如下代码: dates, threads = threads_dates() for i in range(len(dates)): threads[i].start() for i in range(len(dates)): threads[i].join() ''' # codes = get_all_codes() dates = get_trading_dates(begin_date, end_date) threads = [] # dates = get_trading_dates() for date in dates: t = threading.Thread(target=fun, args=(None, None, date)) threads.append(t) length = len(dates) return length, threads
def crawl_basic_one_code(self, code): all_dates = get_trading_dates() updata_requests = [] for date in all_dates: try: df_basics = ts.get_stock_basics(date) if df_basics is None: print('no basic data in tushare, code %s, date %s' % (code, date)) continue doc = dict(df_basics.loc[code]) time_to_market = datetime.strptime(str(doc['timeToMarket']), '%Y%m%d').strftime('%Y-%m-%d') totals = float(doc['totals']) outstanding = float(doc['outstanding']) doc.update({ 'code': code, 'date': date, 'timeToMarket': time_to_market, 'totals': totals, 'outstanding': outstanding }) # 将股票的基本数据补充到原有的数据当中 updata_requests.append( UpdateOne( {'code': doc['code'], 'date': doc['date']}, {'$set': doc}, upsert=True) ) except: print('Error!, code: %s, date: %s' % (code, date), flush=True) print(doc, flush=True) # 将更新好的数据保存到数据库中 if len(updata_requests) > 0: # self.basic.create_index([("code", 1), ("date", -1)], background=True) request_result = self.basic.bulk_write(updata_requests, ordered=False) print('save basic data for one code, code: %s, date: %s, insert: %4d, update: %4d' % (code, date, request_result.upserted_count, request_result.modified_count), flush=True)
def fill_daily_k_supension_days(begin_date=None, end_date=None, dates=None): ''' 补充股票在停牌日的数据 parameter: begin_date: 开始日期,为None时即从‘2008-01-01’开始 end_date: 结束日期, 为None时为数据库中能找到的最新日期 ''' # 找到指定时间段所有的交易日期 if dates is None: all_dates = get_trading_dates(begin_date, end_date) else: if isinstance(dates, list) is False: dates = [dates] all_dates = dates # 找到所有股票的上市日期 basic_date = datetime.now().strftime('%Y-%m-%d') while True: basic_cursor = DB_CONN['basic'].find( {'date': basic_date}, projection={'code': True, 'date': True, 'timeToMarket': True, '_id': False}, ).hint([('date', -1)]) basics = [basic for basic in basic_cursor] if len(basics) > 0: break basic_date = (datetime.strptime(basic_date, '%Y-%m-%d') - timedelta(days=1)).strftime('%Y-%m-%d') for date in all_dates: for basic in basics: code = basic['code'] # 判断股票是否在当前交易日期停牌 is_supension_flag = is_supension(date, code) # 对停牌日期补充数据 if is_supension_flag: fill_daily_k_supension_days_at_date_one_collection(date, code, basic, 'daily_none') fill_daily_k_supension_days_at_date_one_collection(date, code, basic, 'daily_hfq')
def stock_pool(begin_date, end_date): adjust_date_codes_dict = dict() # 调整周期 adjust_interval = 7 all_adjust_dates = [] last_phase_codes = [] # 从数据库中挑选出0<pe<30的股票 all_dates = get_trading_dates(begin_date=begin_date, end_date=end_date) for _index in range(0, len(all_dates), adjust_interval): adjust_date = all_dates[_index] all_adjust_dates.append(adjust_date) print('adjust date: %s' % adjust_date) daily_cursor = daily_collection.find( { 'date': adjust_date, 'pe': { '$gt': 0, '$lt': 30 }, 'is_trading': True, 'index': False }, projection={ 'code': True, '_id': False }, sort=[('pe', ASCENDING)], limit=100) codes = [x['code'] for x in daily_cursor] if codes == []: continue this_phase_codes = [] # 判断是否在调整日停牌 supension_codes = [] if len(last_phase_codes) > 0: supension_cursor = daily_collection.find( { 'code': { '$in': last_phase_codes }, 'date': adjust_date, 'is_trading': False }, projection={ 'code': True }).hint([('code', 1), ('date', -1)]) supension_codes = [x['code'] for x in supension_cursor] this_phase_codes = supension_codes # 判断是否在上一期股票池中 print('last phase code supended in this adjust day:') print(supension_codes) # 得到这一期的股票池 this_phase_codes += codes[0:100 - len(this_phase_codes)] last_phase_codes = this_phase_codes # 建立该调整日和股票列表的对应关系 adjust_date_codes_dict[adjust_date] = this_phase_codes print('this phase codes:') print(this_phase_codes) return all_adjust_dates, adjust_date_codes_dict
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 stock_pool(begin_date, end_date): """ 股票池 :param begin_date: 开始日期 :param end_date: 结束日期 :return: tuple,所有调整日,以及调整日和代码列表对应的dict """ adjust_date_codes_dict = dict() # 获取指定时间区间的所有交易日 all_dates = get_trading_dates(begin_date=begin_date, end_date=end_date) # 上一期的所有股票代码 last_phase_codes = [] # 调整周期是7个交易日 adjust_interval = 7 # 所有的调整日 all_adjust_dates = [] # 在调整日调整股票池 for _index in range(0, len(all_dates), adjust_interval): # 保存调整日 adjust_date = all_dates[_index] all_adjust_dates.append(adjust_date) print('调整日期: %s' % adjust_date, flush=True) # 查询出调整当日,0 < pe_ttm < 30,且非停牌的股票 # 最重要的一点是,按照pe_ttm正序排列,只取前100只 daily_cursor = daily.find( { 'date': adjust_date, 'pe': { '$lt': 30, '$gt': 0 }, 'is_trading': True }, sort=[('pe', ASCENDING)], projection={'code': True}, limit=100) codes = [x['code'] for x in daily_cursor] # 本期股票列表 this_phase_codes = [] # 查询出上次股票池中正在停牌的股票 if len(last_phase_codes) > 0: suspension_cursor = daily.find( { 'code': { '$in': last_phase_codes }, 'date': adjust_date, 'is_trading': False }, projection={'code': True}) suspension_codes = [x['code'] for x in suspension_cursor] # 保留股票池中正在停牌的股票 this_phase_codes = suspension_codes print('上期停牌', flush=True) print(this_phase_codes, flush=True) # 用新的股票将剩余位置补齐 this_phase_codes += codes[0:100 - len(this_phase_codes)] # 将本次股票设为下次运行的时的上次股票池 last_phase_codes = this_phase_codes # 建立该调整日和股票列表的对应关系 adjust_date_codes_dict[adjust_date] = this_phase_codes print('最终出票', flush=True) print(this_phase_codes, flush=True) # 返回结果 return all_adjust_dates, adjust_date_codes_dict
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, 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 stock_pool(begin_date, end_date): """ 股票池的选股逻辑 :param begin_date: 开始日期 :param end_date: 结束日期 :return: tuple,所有调整日,以及调整日和代码列表对应的dict """ """ 下面的几个参数可以自己修改 """ # 调整周期是7个交易日,可以改变的参数 adjust_interval = 7 # PE的范围 pe_range = (0, 30) # PE的排序方式, ASCENDING - 从小到大,DESCENDING - 从大到小 sort = ASCENDING # 股票池内的股票数量 pool_size = 100 # 返回值:调整日和当期股票代码列表 adjust_date_codes_dict = dict() # 返回值:所有的调整日列表 all_adjust_dates = [] # 获取指定时间范围内的所有交易日列表,按照日期正序排列 all_dates = get_trading_dates(begin_date=begin_date, end_date=end_date) # 上一期的所有股票代码 last_phase_codes = [] # 在调整日调整股票池 for _index in range(0, len(all_dates), adjust_interval): # 保存调整日 adjust_date = all_dates[_index] all_adjust_dates.append(adjust_date) print('调整日期: %s' % adjust_date, flush=True) # 查询出调整当日,0 < pe < 30,且非停牌的股票 # 最重要的一点是,按照pe正序排列,只取前100只 daily_cursor = daily.find( {'date': adjust_date, 'pe': {'$lt': pe_range[1], '$gt': pe_range[0]}, 'is_trading': True}, sort=[('pe', sort)], projection={'code': True}, limit=pool_size ) # 拿到所有的股票代码 codes = [x['code'] for x in daily_cursor] # 本期股票列表 this_phase_codes = [] # 如果上期股票代码列表不为空,则查询出上次股票池中正在停牌的股票 if len(last_phase_codes) > 0: suspension_cursor = daily.find( # 查询是股票代码、日期和是否为交易,这里is_trading=False {'code': {'$in': last_phase_codes}, 'date': adjust_date, 'is_trading': False}, # 只需要使用股票代码 projection={'code': True} ) # 拿到股票代码 suspension_codes = [x['code'] for x in suspension_cursor] # 保留股票池中正在停牌的股票 this_phase_codes = suspension_codes # 打印出所有停牌的股票代码 print('上期停牌', flush=True) print(this_phase_codes, flush=True) # 用新的股票将剩余位置补齐 this_phase_codes += codes[0: pool_size - len(this_phase_codes)] # 将本次股票设为下次运行的时的上次股票池 last_phase_codes = this_phase_codes # 建立该调整日和股票列表的对应关系 adjust_date_codes_dict[adjust_date] = this_phase_codes print('最终出票', flush=True) print(this_phase_codes, flush=True) # 返回结果 return all_adjust_dates, adjust_date_codes_dict
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()
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()