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): """ 填充指定日期范围内,股票停牌日的行情数据。 填充时,停牌的开盘价、最高价、最低价和收盘价都为最近一个交易日的收盘价,成交量为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) # 循环填充所有交易日的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(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 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: 回测结束日期 """ # 初始现金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_k_down_break_ma10(holding_code, _date): to_be_sold_codes.add(holding_code) # 检查是否有需要第二天买入的股票 to_be_bought_codes.clear() if this_phase_codes is not None: for _code in this_phase_codes: if _code not in holding_codes and is_k_up_break_ma10( _code, _date): to_be_bought_codes.add(_code) # 计算总资产 total_value = 0 # 获取所有持仓股的当日收盘价 holding_daily_cursor = DB_CONN['daily'].find( { 'code': { '$in': holding_codes }, 'date': _date }, projection={ 'close': True, 'code': True }) # 计算所有持仓股的总市值 for holding_daily in holding_daily_cursor: code = holding_daily['code'] holding_stock = holding_code_dict[code] # 单只持仓的市值等于收盘价乘以持仓量 value = holding_daily['close'] * holding_stock['volume'] # 总市值等于所有持仓股市值的累加之和 total_value += value # 计算单只股票的持仓收益 profit = (value - holding_stock['cost']) * 100 / holding_stock['cost'] # 计算单只股票的单日收益 one_day_profit = (value - holding_stock['last_value'] ) * 100 / holding_stock['last_value'] # 更新前一日市值 holding_stock['last_value'] = value print('持仓: %s, %10.2f, %4.2f, %4.2f' % (code, value, profit, one_day_profit)) # 总资产等于总市值加上总现金 total_capital = total_value + cash # 获取沪深300的当日收盘值 hs300_current_value = DB_CONN['daily'].find_one( { 'code': '000300', 'index': True, 'date': _date }, projection={'close': True})['close'] print('收盘后,现金: %10.2f, 总资产: %10.2f' % (cash, total_capital)) last_date = _date # 将当日的净值、收益和沪深300的涨跌幅放入DataFrame df_profit.loc[_date] = { 'net_value': round(total_capital / 1e7, 2), 'profit': round(100 * (total_capital - 1e7) / 1e7, 2), 'hs300': round( 100 * (hs300_current_value - hs300_begin_value) / hs300_begin_value, 2) } print(df_profit) # 计算最大回撤 drawdown = compute_drawdown(df_profit['net_value']) # 计算年化收益和夏普比率 annual_profit, sharpe_ratio = compute_sharpe_ratio(df_profit['net_value']) print('回测结果 %s - %s,年化收益: %7.3f, 最大回撤:%7.3f, 夏普比率:%4.2f' % (begin_date, end_date, annual_profit, drawdown, sharpe_ratio)) # 绘制收益曲线 df_profit.plot(title='Backtest Result', y=['profit', 'hs300'], kind='line') plt.show()