def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.order = QA_QAMarket_bid_list() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = '' self.strategy_start_date = '' self.strategy_start_time = '' self.strategy_end_date = '' self.strategy_end_time = '' self.today = '' self.strategy_stock_list = [] self.trade_list = [] self.start_real_id = 0 self.end_real_id = 0 self.temp = {} self.commission_fee_coeff = 0.0015 self.benchmark_type = 'index' self.market_data_dict = {} self.backtest_print_log = True # 打印
def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ if len(str(self.strategy_start_date))==10: self.strategy_start_time=str(self.strategy_start_date)+' 15:00:00' elif len(str(self.strategy_start_date))==19: self.strategy_start_time=str(self.strategy_start_date) self.strategy_start_date=str(self.strategy_start_date)[0:10] else: QA_util_log_info('Wrong start date format') if len(str(self.strategy_end_date))==10: self.strategy_end_time=str(self.strategy_end_date)+' 15:00:00' elif len(str(self.strategy_end_date))==19: self.strategy_end_time=str(self.strategy_end_date) self.strategy_end_date=str(self.strategy_end_date)[0:10] else: QA_util_log_info('Wrong end date format') # 重新初始账户资产 self.market = QA_Market(self.commission_fee_coeff) self.setting.QA_setting_init() self.account.init() self.start_real_date = QA_util_get_real_date( self.strategy_start_date, self.trade_list, 1) self.start_real_time=str(self.start_real_date)+' '+self.strategy_start_time.split(' ')[1] self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date( self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) self.end_real_time=str(self.end_real_date)+' '+self.strategy_end_time.split(' ')[1] # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 if self.benchmark_type in ['I','index']: self.benchmark_data = QA_fetch_index_day_adv( self.benchmark_code, self.start_real_date, self.end_real_date) elif self.benchmark_type in ['S','stock']: self.benchmark_data = QA_fetch_stock_day_adv( self.benchmark_code, self.start_real_date, self.end_real_date) if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int( self.strategy_gap+1)], self.trade_list[self.end_real_id]).to_qfq() elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: self.market_data = QA_fetch_stocklist_min_adv( self.strategy_stock_list, QA_util_time_gap(self.start_real_time,self.strategy_gap+1,'<',self.backtest_type), QA_util_time_gap(self.end_real_time,1,'>',self.backtest_type), self.backtest_type).to_qfq() elif self.backtest_type in ['index_day']: self.market_data = QA_fetch_index_day_adv(self.strategy_stock_list, self.trade_list[self.start_real_id - int( self.strategy_gap+1)], self.end_real_date) elif self.backtest_type in ['index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min']: self.market_data = QA_fetch_index_min_adv( self.strategy_stock_list, QA_util_time_gap(self.start_real_time,self.strategy_gap+1,'<',self.backtest_type.split('_')[1]), QA_util_time_gap(self.end_real_time,1,'>',self.backtest_type.split('_')[1]), self.backtest_type.split('_')[1])
def __init__(self): self.account = QA_Account() self.market = QA_Market() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name
def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = '' self.today = ''
def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.market = QA_Market(self.commission_fee_coeff) self.setting.QA_setting_init() self.account.init() self.start_real_date = QA_util_get_real_date(self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date(self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 self.benchmark_data = QA_fetch_index_day_adv(self.benchmark_code, self.start_real_date, self.end_real_date) if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id]).to_qfq() elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: self.market_data = QA_fetch_stocklist_min_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id + 1], self.backtest_type).to_qfq() elif self.backtest_type in ['index_day']: self.market_data = QA_fetch_index_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id]) elif self.backtest_type in [ 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min' ]: self.market_data = QA_fetch_index_min_adv( self.strategy_stock_list, self.start_real_date, self.end_real_date, self.backtest_type.split('_')[1])
def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.order = QA_QAMarket_bid_list() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = '' self.today = '' self.strategy_stock_list = [] self.trade_list = [] self.start_real_id = 0 self.end_real_id = 0 self.temp = {}
def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.order = QA_QAMarket_bid_list() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = None self.last_time = None self.strategy_start_date = '' self.strategy_start_time = '' self.strategy_end_date = '' self.strategy_end_time = '' self.today = None self.strategy_stock_list = [] self.trade_list = [] self.start_real_id = 0 self.end_real_id = 0 self.temp = {} self.commission_fee_coeff = 0.0015 self.account_d_value = [] self.account_d_key = [] self.benchmark_type = 'index' self.market_data_dict = {} self.backtest_print_log = True # 打印 self.if_save_to_mongo = True self.if_save_to_csv = True self.stratey_version = 'V1' self.topic_name = 'EXAMPLE' self.topic_id = '' self.outside_data = [] self.outside_data_dict = [] self.outside_data_hashable = {} self.dirs = '.{}QUANTAXIS_RESULT{}{}{}{}{}'.format( os.sep, os.sep, self.topic_name, os.sep, self.stratey_version, os.sep)
def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.order = QA_QAMarket_bid_list() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = '' self.today = '' self.strategy_stock_list = [] self.trade_list = [] self.start_real_id = 0 self.end_real_id = 0 self.temp = {} self.commission_fee_coeff = 0.0015
def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.market = QA_Market(self.commission_fee_coeff) self.setting.QA_setting_init() self.account.init() self.start_real_date = QA_util_get_real_date( self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date( self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 self.benchmark_data = QA_fetch_index_day_adv( self.benchmark_code, self.start_real_date, self.end_real_date) if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int( self.strategy_gap)], self.trade_list[self.end_real_id]).to_qfq() elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: self.market_data = QA_fetch_stocklist_min_adv( self.strategy_stock_list, self.trade_list[ self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id + 1], self.backtest_type).to_qfq() elif self.backtest_type in ['index_day']: self.market_data = QA_fetch_index_day_adv(self.strategy_stock_list, self.trade_list[self.start_real_id - int( self.strategy_gap)], self.trade_list[self.end_real_id]) elif self.backtest_type in ['index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min']: self.market_data = QA_fetch_index_min_adv( self.strategy_stock_list, self.start_real_date, self.end_real_date, self.backtest_type.split('_')[1])
class QA_Backtest(): account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name def QA_backtest_init(self): pass def QA_backtest_start(self): QA_util_log_info('backtest start') def QA_backtest_day_start(self): pass def QA_backtest_handle(self): pass def QA_backtest_day_end(self): pass def QA_get_data(self): self.QA_get_data_from_market() self.QA_get_data_from_ARP() def QA_get_data_from_market(self): db = self.clients.quantaxis def QA_get_data_from_ARP(self): pass def QA_strategy_update(self): pass
class QA_Backtest(): account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name """ backtest 类不应该只是一个简单的构造函数,他应该包含一个回测框架常用的方法和一些定制化的需求实现 @yutiansut 2017/6/19 需要做一个内部的setting表,以dict格式,value是函数名 @yutiansut 2017/6/27 """ def QA_backtest_load_strategy(self, ): __f_strategy_path = self.__backtest_setting['strategy']['file_path'] __f_strategy_path = __f_strategy_path.split(':') if __f_strategy_path[0] == 'file' and __f_strategy_path[1] == 'py': if __f_strategy_path[2] == 'local': __current_dir = os.getcwd() try: if os.path.exists( os.path.exists( str(__current_dir) + '\\backtest_strategy.py')): __file_path = os.path.exists( str(__current_dir) + '\\backtest_strategy.py') except: return "wrong with strategy file in current dir" elif os.path.exists(__f_strategy_path[2]): __file_path = __f_strategy_path[2] else: QA_util_log_info('error with loading strategy file') sys.exit() try: import __file_path as QUANTAXIS_Strategy QA_util_log_info(dir(QUANTAXIS_Strategy)) except: QA_util_log_info('wrong with loading strategy') def QA_backtest_import_setting(self, __setting_file_path='10x'): if __setting_file_path == '10x': __current_dir = os.getcwd() try: if os.path.exists( os.path.exists( str(__current_dir) + '\\backtest_setting.ini')): __file_path = str(__current_dir) + '\\backtest_setting.ini' except: return "wrong with config file in current dir" elif os.path.exists(__setting_file_path): __file_path = os.path.exists( str(__current_dir) + '\\backtest_setting.ini') self.__backtest_setting = configparser.ConfigParser() self.__backtest_setting.read(__file_path) def __QA_backtest_set_stock_list(self): self.strategy_stock_list = [] __t_strategy_stock_list = self.__backtest_setting['account'][ 'stock_list'] __t_strategy_stock_list = __t_strategy_stock_list.split(':') if __t_strategy_stock_list[0] == 'file': if __t_strategy_stock_list[1] == 'csv': if __t_strategy_stock_list[2] == 'local': __current_dir = os.getcwd() try: if os.path.exists( os.path.exists( str(__current_dir) + '\\stock_list.csv')): __stock_list_file_path = str(__current_dir) + \ '\\stock_list.csv' else: QA_util_log_info( "wrong with csv file in current dir, \ the name should be \\stock_list.csv") except: QA_util_log_info("wrong with csv file in current dir, \ the name should be \\stock_list.csv") else: try: if os.path.exists(__t_strategy_stock_list[2]): __stock_list_file_path = __t_strategy_stock_list[2] else: QA_util_log_info( "wrong with csv file in current dir, \ the name should be \\stock_list.csv") except: QA_util_log_info("wrong with csv file in current dir, \ the name should be \\stock_list.csv") with open(__stock_list_file_path, 'r') as csv_file: __data = csv.reader(csv_file) for item in __data: self.strategy_stock_list.append(item[0]) elif __t_strategy_stock_list[1] == 'json': if __t_strategy_stock_list[2] == 'local': __current_dir = os.getcwd() try: if os.path.exists( os.path.exists( str(__current_dir) + '\\stock_list.json')): __stock_list_file_path = str(__current_dir) + \ '\\stock_list.json' except: return "wrong with csv file in current dir, \ the name should be \\stock_list.json" else: try: if os.path.exists(__t_strategy_stock_list[2]): __stock_list_file_path = __t_strategy_stock_list[2] else: return "wrong with csv file in current dir, \ the name should be \\stock_list.json" except: return "wrong with csv file in current dir, \ the name should be \\stock_list.json" elif __t_strategy_stock_list[0] == 'mongo': try: import pymongo coll = pymongo.MongoClient().__t_strategy_stock_list[1].split( '-')[0].__t_strategy_stock_list[1].split['-'][1] assert isinstance(__t_strategy_stock_list[2], str) return coll.find(__t_strategy_stock_list[2]) except: QA_util_log_info( 'something wrong with import stock_list from mongodb') elif __t_strategy_stock_list[0] == 'data': if __t_strategy_stock_list[1] == 'list': self.strategy_stock_list = __t_strategy_stock_list[2] def __QA_backtest_set_bid_model(self): if self.__backtest_setting['bid']['bid_model'] == 'market_price': self.bid.bid['price'] = 'market_price' self.bid.bid['bid_model'] = 'auto' elif self.__backtest_setting['bid']['bid_model'] == 'close_price': self.bid.bid['price'] = 'close_price' self.bid.bid['bid_model'] = 'auto' elif self.__backtest_setting['bid']['bid_model'] == 'strategy': self.bid.bid['price'] = 0 self.bid.bid['bid_model'] = 'strategy' else: QA_util_log_info('support bid model') sys.exit() def __QA_backtest_set_save_model(self): pass def QA_backtest_init(self): # 设置回测的开始结束时间 try: self.QA_backtest_import_setting() except: sys.exit() self.strategy_start_date = str( self.__backtest_setting['backtest']['strategy_start_date']) self.strategy_end_date = str( self.__backtest_setting['backtest']['strategy_end_date']) # 设置回测标的,是一个list对象,不过建议只用一个标的 # gap是回测时,每日获取数据的前推日期(交易日) self.strategy_gap = int( self.__backtest_setting['backtest']['strategy_gap']) # 设置全局的数据库地址,回测用户名,密码,并初始化 self.setting.QA_util_sql_mongo_ip = str( self.__backtest_setting['backtest']['database_ip']) self.setting.QA_setting_user_name = str( self.__backtest_setting['backtest']['username']) self.setting.QA_setting_user_password = str( self.__backtest_setting['backtest']['password']) self.setting.QA_setting_init() # 回测的名字 self.strategy_name = str( self.__backtest_setting['backtest']['strategy_name']) # 股票的交易日历,真实回测的交易周期,和交易周期在交易日历中的id self.trade_list = QA_fetch_trade_date( self.setting.client.quantaxis.trade_date) self.benchmark_code = self.__backtest_setting['backtest'][ 'benchmark_code'] """ 这里会涉及一个区间的问题,开始时间是要向后推,而结束时间是要向前推,1代表向后推,-1代表向前推 """ self.start_real_date = QA_util_get_real_date(self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date(self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) self.__QA_backtest_set_stock_list() self.account.init_assest = self.__backtest_setting['account'][ 'account_assets'] def __QA_backtest_init_inside(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.account.init() # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 self.market_data = QA_fetch_stocklist_day( self.strategy_stock_list, self.setting.client.quantaxis.stock_day, [ self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id] ]) def QA_backtest_start(self, outside_handle, *args, **kwargs): """ 这个是回测流程开始的入口 """ assert len(self.strategy_stock_list) > 0 assert len(self.trade_list) > 0 assert isinstance(self.start_real_date, str) assert isinstance(self.end_real_date, str) self.__QA_backtest_init_inside() assert len(self.market_data) == len(self.strategy_stock_list) QA_util_log_info('QUANTAXIS Backtest Engine Initial Successfully') QA_util_log_info('Basical Info: \n' + tabulate( [[str(__version__), str(self.strategy_name)]], headers=('Version', 'Strategy_name'))) QA_util_log_info('Stock_List: \n' + tabulate([self.strategy_stock_list])) # 初始化报价模式 self.__QA_backtest_set_bid_model() try: # 在末尾增加一个回调给策略 outside_handle.on_start(self) except: pass # 加载外部策略 self.__QA_backest_handle_data(outside_handle) def __QA_backest_handle_data(self, outside_handle): '这个outside_handle就是一个外部的注入函数' # 首先判断是否能满足回测的要求` _info = {} _info['stock_list'] = self.strategy_stock_list __messages = {} self.__init_cash_per_stock = int( float(self.account.init_assest) / len(self.strategy_stock_list)) # 策略的交易日循环 for i in range(int(self.start_real_id), int(self.end_real_id) - 1, 1): # 正在进行的交易日期 __running_date = self.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __running_date) QA_util_log_info( tabulate(self.account.message['body']['account']['hold'])) for __j in range(0, len(self.strategy_stock_list)): if __running_date in [l[6] for l in self.market_data[__j]] and \ [l[6] for l in self.market_data[__j]].index(__running_date) \ > self.strategy_gap + 1: __data = self.__QA_data_handle([ __l[6] for __l in self.market_data[__j] ].index(__running_date), __j) __amount = 0 for item in __data['account']['body']['account']['hold']: if self.strategy_stock_list[__j] in item: __amount = __amount + item[3] if __amount > 0: __hold = 1 else: __hold = 0 __result = outside_handle.predict(__data['market'], __data['account'], __hold, _info) if float(self.account.message['body']['account']['cash'] [-1]) > 0: self.__QA_backtest_excute_bid( __result, __running_date, __hold, str(self.strategy_stock_list[__j])[0:6], __amount) else: QA_util_log_info('not enough free money') else: pass # 在回测的最后一天,平掉所有仓位(回测的最后一天是不买入的) while len(self.account.hold) > 1: __hold_list = self.account.hold[1::] pre_del_id = [] for item_ in range(0, len(__hold_list)): if __hold_list[item_][3] > 0: __last_bid = self.bid.bid __last_bid['amount'] = int(__hold_list[item_][3]) __last_bid['order_id'] = str(random.random()) __last_bid['price'] = 'close_price' __last_bid['code'] = str(__hold_list[item_][1]) __last_bid['date'] = self.trade_list[self.end_real_id] __last_bid['towards'] = -1 __last_bid['user'] = self.setting.QA_setting_user_name __last_bid['strategy'] = self.strategy_name __last_bid['bid_model'] = 'auto' __last_bid['status'] = '0x01' __last_bid['amount_model'] = 'amount' __message = self.market.receive_bid( __last_bid, self.setting.client) _remains_day = 0 while __message['header']['status'] == 500: # 停牌状态,这个时候按停牌的最后一天计算价值(假设平仓) __last_bid['date'] = self.trade_list[self.end_real_id - _remains_day] _remains_day += 1 __message = self.market.receive_bid( __last_bid, self.setting.client) # 直到市场不是为0状态位置,停止前推日期 __messages = self.account.QA_account_receive_deal( __message) else: pre_del_id.append(item_) pre_del_id.sort() pre_del_id.reverse() for item_x in pre_del_id: __hold_list.pop(item_x) try: # 在末尾增加一个回调给策略 outside_handle.on_end(__data['market'], __data['account'], __hold, _info) except: pass # 开始分析 QA_util_log_info('start analysis====\n' + str(self.strategy_stock_list)) QA_util_log_info('=' * 10 + 'Trade History' + '=' * 10) QA_util_log_info('\n' + tabulate(self.account.history, headers=('date', 'code', 'price', 'towards', 'amounts', 'order_id', 'trade_id', 'commission'))) QA_util_log_info( tabulate(self.account.detail, headers=('date', 'code', 'price', 'amounts', 'order_id', 'trade_id', 'sell_price', 'sell_order_id', 'sell_trade_id', 'sell_date', 'left_amount', 'commission'))) __exist_time = int(self.end_real_id) - int(self.start_real_id) + 1 self.__benchmark_data = QA_fetch_index_day(self.benchmark_code, self.start_real_date, self.end_real_date) performace = QA_backtest_analysis_start( self.setting.client, self.strategy_stock_list, __messages, self.trade_list[self.start_real_id:self.end_real_id + 1], self.market_data, self.__benchmark_data) _backtest_mes = { 'user': self.setting.QA_setting_user_name, 'strategy': self.strategy_name, 'stock_list': performace['code'], 'start_time': self.strategy_start_date, 'end_time': self.strategy_end_date, 'account_cookie': self.account.account_cookie, 'annualized_returns': performace['annualized_returns'], 'benchmark_annualized_returns': performace['benchmark_annualized_returns'], 'assets': performace['assets'], 'benchmark_assets': performace['benchmark_assets'], 'trade_date': performace['trade_date'], 'total_date': performace['total_date'], 'win_rate': performace['win_rate'], 'alpha': performace['alpha'], 'beta': performace['beta'], 'sharpe': performace['sharpe'], 'vol': performace['vol'], 'benchmark_vol': performace['benchmark_vol'], 'max_drop': performace['max_drop'], 'exist': __exist_time, 'time': datetime.datetime.now() } QA_SU_save_backtest_message(_backtest_mes, self.setting.client) QA_SU_save_account_message(__messages, self.setting.client) QA_SU_save_account_to_csv(__messages) # QA.QA_SU_save_backtest_message(analysis_message, self.setting.client) def __QA_backtest_excute_bid(self, __result, __date, __hold, __code, __amount): """ 这里是处理报价的逻辑部分 2017/7/19 修改 __result传进来的变量重新区分: 现在需要有 if_buy, if_sell 因为需要对于: 持仓状态下继续购买进行进一步的支持*简单的情形就是 浮盈加仓 if_buy, if_sell都需要传入 现在的 买卖状态 和 持仓状态 是解耦的 """ # 为了兼容性考虑,我们会在开始的时候检查是否有这些变量 if 'if_buy' not in list(__result.keys()): __result['if_buy'] = 0 if 'if_sell' not in list(__result.keys()): __result['if_sell'] = 0 self.__QA_backtest_set_bid_model() if self.bid.bid['bid_model'] == 'strategy': __bid_price = __result['price'] else: __bid_price = self.bid.bid['price'] __bid = self.bid.bid __bid['order_id'] = str(random.random()) __bid['user'] = self.setting.QA_setting_user_name __bid['strategy'] = self.strategy_name __bid['code'] = __code __bid['date'] = __date __bid['price'] = __bid_price __bid['amount'], __bid['amount_model'] = self.__QA_bid_amount( __result['amount'], __amount) if __result['if_buy'] == 1: # 这是买入的情况 __bid['towards'] = 1 __message = self.market.receive_bid(__bid, self.setting.client) if float(self.account.message['body']['account']['cash'][-1]) > \ float(__message['body']['bid']['price']) * \ float(__message['body']['bid']['amount']): # 这里是买入资金充足的情况 # 不去考虑 pass else: # 如果买入资金不充足,则按照可用资金去买入 __message['body']['bid']['amount'] = int( float(self.account.message['body']['account']['cash'][-1]) / float( float(str(__message['body']['bid']['price'])[0:5]) * 100)) * 100 if __message['body']['bid']['amount'] > 0: # 这个判断是为了 如果买入资金不充足,所以买入报了一个0量单的情况 #如果买入量>0, 才判断为成功交易 self.account.QA_account_receive_deal(__message) elif __result['if_buy'] == 0: # 如果买入状态为0,则不进行任何买入操作 pass # 下面是卖出操作,这里在卖出前需要考虑一个是否有仓位的问题: # 因为在股票中是不允许卖空操作的,所以这里是股票的交易引擎和期货的交易引擎的不同所在 if __result['if_sell'] == 1 and __hold == 1: __bid['towards'] = -1 __message = self.market.receive_bid(__bid, self.setting.client) self.account.QA_account_receive_deal(__message) def __QA_bid_amount(self, __strategy_amount, __amount): if __strategy_amount == 'mean': return float( float(self.account.message['body']['account']['cash'][-1]) / len(self.strategy_stock_list)), 'price' elif __strategy_amount == 'half': return __amount * 0.5, 'amount' elif __strategy_amount == 'all': return __amount, 'amount' def __QA_get_data_from_market(self, __id, stock_id): # x=[x[6] for x in self.market_data] if __id > self.strategy_gap + 1: index_of_day = __id index_of_start = index_of_day - self.strategy_gap + 1 return self.market_data[stock_id][index_of_start:index_of_day + 1] # 从账户中更新数据 def __QA_data_handle(self, __id, __stock_id): market_data = self.__QA_get_data_from_market(__id, __stock_id) __message = self.account.message return {'market': market_data, 'account': __message}
class QA_Backtest(): '最终目的还是实现一个通用的回测类' backtest_type = 'day' account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() order = QA_QAMarket_bid_list() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name market_data = [] now = '' today = '' strategy_stock_list = [] trade_list = [] start_real_id = 0 end_real_id = 0 temp = {} commission_fee_coeff = 0.0015 def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.order = QA_QAMarket_bid_list() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = '' self.today = '' self.strategy_stock_list = [] self.trade_list = [] self.start_real_id = 0 self.end_real_id = 0 self.temp = {} self.commission_fee_coeff = 0.0015 def __QA_backtest_init(self): """既然是被当做装饰器使用,就需要把变量设置放在装饰函数的前面,把函数放在装饰函数的后面""" # 设置回测的开始结束时间 self.strategy_start_date = str('2017-01-05') self.strategy_end_date = str('2017-07-01') # 设置回测标的,是一个list对象,不过建议只用一个标的 # gap是回测时,每日获取数据的前推日期(交易日) self.strategy_gap = int(60) # 设置全局的数据库地址,回测用户名,密码,并初始化 self.setting.QA_util_sql_mongo_ip = str('127.0.0.1') self.setting.QA_setting_user_name = str('admin') self.setting.QA_setting_user_password = str('admin') self.setting.QA_setting_init() # 回测的名字 self.strategy_name = str('example_min') # 股票的交易日历,真实回测的交易周期,和交易周期在交易日历中的id self.trade_list = trade_date_sse self.benchmark_code = '000300' """ 这里会涉及一个区间的问题,开始时间是要向后推,而结束时间是要向前推,1代表向后推,-1代表向前推 """ self.strategy_stock_list = ['000001', '000002', '000004'] self.account.init_assest = 1000000 self.backtest_bid_model = 'market_price' self.commission_fee_coeff = 0.0015 def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.market = QA_Market(self.commission_fee_coeff) self.setting.QA_setting_init() self.account.init() self.start_real_date = QA_util_get_real_date(self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date(self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 self.benchmark_data = QA_fetch_index_day_adv(self.benchmark_code, self.start_real_date, self.end_real_date) if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id]).to_qfq() elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: self.market_data = QA_fetch_stocklist_min_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id + 1], self.backtest_type).to_qfq() elif self.backtest_type in ['index_day']: self.market_data = QA_fetch_index_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id]) elif self.backtest_type in [ 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min' ]: self.market_data = QA_fetch_index_min_adv( self.strategy_stock_list, self.start_real_date, self.end_real_date, self.backtest_type.split('_')[1]) def __QA_backtest_start(self, *args, **kwargs): """ 这个是回测流程开始的入口 """ QA_util_log_info('QUANTAXIS Backtest Engine Initial Successfully') QA_util_log_info('Basical Info: \n' + tabulate( [[str(__version__), str(self.strategy_name)]], headers=('Version', 'Strategy_name'))) QA_util_log_info('BACKTEST Cookie_ID is: ' + str(self.account.account_cookie)) QA_util_log_info('Stock_List: \n' + tabulate([self.strategy_stock_list])) # 初始化报价模式 self.__messages = [] def __check_state(self, bid_price, bid_amount): pass def __QA_bid_amount(self, __strategy_amount, __amount): if __strategy_amount == 'mean': return float( float(self.account.message['body']['account']['cash'][-1]) / len(self.strategy_stock_list)), 'price' elif __strategy_amount == 'half': return __amount * 0.5, 'amount' elif __strategy_amount == 'all': return __amount, 'amount' def __end_of_trading(self, *arg, **kwargs): # 在回测的最后一天,平掉所有仓位(回测的最后一天是不买入的) # 回测最后一天的交易处理 self.now = str(self.end_real_date) + ' 15:00:00' self.today = self.end_real_date self.QA_backtest_sell_all(self) self.__sell_from_order_queue(self) self.__sync_order_LM(self, 'daily_settle') # 每日结算 def __wrap_bid(self, __bid, __order=None): __market_data_for_backtest = self.market_data.get_bar( __bid.code, __bid.datetime) if __market_data_for_backtest.len() == 1: __O, __H, __L, __C, __V = self.QA_backtest_get_OHLCV( self, __market_data_for_backtest) if __O is not None and __order is not None: if __order['bid_model'] in [ 'limit', 'Limit', 'Limited', 'limited', 'l', 'L', 0, '0' ]: # 限价委托模式 __bid.price = __order['price'] elif __order['bid_model'] in [ 'Market', 'market', 'MARKET', 'm', 'M', 1, '1' ]: # 2017-09-18 修改 市价单以当前bar开盘价下单 __bid.price = float(__O[0]) elif __order['bid_model'] in [ 'strict', 'Strict', 's', 'S', '2', 2 ]: __bid.price = float( __H[0]) if __bid.towards == 1 else float(__L[0]) elif __order['bid_model'] in [ 'close', 'close_price', 'c', 'C', '3', 3 ]: __bid.price = float(__C[0]) __bid.price = float('%.2f' % __bid.price) return __bid, __market_data_for_backtest else: return __bid, __market_data_for_backtest else: QA_util_log_info( 'BACKTEST ENGINE ERROR=== CODE %s TIME %s NO MARKET DATA!' % (__bid.code, __bid.datetime)) return __bid, 500 def __end_of_backtest(self, *arg, **kwargs): # 开始分析 # 对于account.detail做一定的整理 self.account.detail = detail = pd.DataFrame( self.account.detail, columns=[ 'date', 'code', 'price', 'amounts', 'order_id', 'trade_id', 'sell_price', 'sell_order_id', 'sell_trade_id', 'sell_date', 'left_amount', 'commission' ]) self.account.detail['sell_average'] = self.account.detail[ 'sell_price'].apply(lambda x: mean(x)) self.account.detail['pnl_persentage'] = self.account.detail['sell_average'] - \ self.account.detail['price'] self.account.detail['pnl'] = self.account.detail['pnl_persentage'] * ( self.account.detail['amounts'] - self.account.detail['left_amount'] ) - self.account.detail['commission'] self.account.detail = self.account.detail.drop( ['order_id', 'trade_id', 'sell_order_id', 'sell_trade_id'], axis=1) QA_util_log_info('start analysis====\n' + str(self.strategy_stock_list)) QA_util_log_info('=' * 10 + 'Trade History' + '=' * 10) QA_util_log_info('\n' + tabulate(self.account.history, headers=('date', 'code', 'price', 'towards', 'amounts', 'order_id', 'trade_id', 'commission'))) QA_util_log_info('\n' + tabulate( self.account.detail, headers=(self.account.detail.columns))) __exist_time = int(self.end_real_id) - int(self.start_real_id) + 1 if len(self.__messages) > 1: performace = QA_backtest_analysis_start( self.setting.client, self.strategy_stock_list, self.__messages, self.trade_list[self.start_real_id:self.end_real_id + 1], self.benchmark_data.data) _backtest_mes = { 'user': self.setting.QA_setting_user_name, 'strategy': self.strategy_name, 'stock_list': performace['code'], 'start_time': self.strategy_start_date, 'end_time': self.strategy_end_date, 'account_cookie': self.account.account_cookie, 'annualized_returns': performace['annualized_returns'], 'benchmark_annualized_returns': performace['benchmark_annualized_returns'], 'assets': performace['assets'], 'benchmark_assets': performace['benchmark_assets'], 'trade_date': performace['trade_date'], 'total_date': performace['total_date'], 'win_rate': performace['win_rate'], 'alpha': performace['alpha'], 'beta': performace['beta'], 'sharpe': performace['sharpe'], 'vol': performace['vol'], 'benchmark_vol': performace['benchmark_vol'], 'max_drop': performace['max_drop'], 'exist': __exist_time, 'time': datetime.datetime.now() } QA_SU_save_backtest_message(_backtest_mes, self.setting.client) QA_SU_save_account_message(self.__messages, self.setting.client) QA_SU_save_account_to_csv(self.__messages) self.account.detail.to_csv('backtest-pnl--' + str(self.account.account_cookie) + '.csv') # QA_SU_save_pnl_to_csv(self.account.detail,self.account.account_cookie) def QA_backtest_get_market_data(self, code, date, gap_=None): '这个函数封装了关于获取的方式 用GAP的模式' gap_ = self.strategy_gap if gap_ is None else gap_ return self.market_data.select_code(code).select_time_with_gap( date, gap_, 'lte') def QA_backtest_get_market_data_bar(self, code, time): '这个函数封装了关于获取的方式' return self.market_data.get_bar(code, time) def QA_backtest_sell_available(self, __code): try: return self.account.sell_available[__code] except: return 0 def QA_backtest_hold_amount(self, __code): try: return pd.DataFrame( self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum()[__code] except: return 0 def __sell_from_order_queue(self): # 每个bar结束的时候,批量交易 __result = [] self.order.__init__() if len(self.account.order_queue) >= 1: __bid_list = self.order.from_dataframe( self.account.order_queue.query('status!=200').query( 'status!=500').query('status!=400')) for item in __bid_list: # 在发单的时候要改变交易日期 item.date = self.today item.datetime = self.now __bid, __market = self.__wrap_bid(self, item) __message = self.__QA_backtest_send_bid( self, __bid, __market.to_json()[0]) if isinstance(__message, dict): if __message['header']['status'] in ['200', 200]: self.__sync_order_LM(self, 'trade', __bid, __message['header']['order_id'], __message['header']['trade_id'], __message) else: self.__sync_order_LM(self, 'wait') else: QA_util_log_info('FROM BACKTEST: Order Queue is empty at %s!' % self.now) pass def QA_backtest_get_OHLCV(self, __data): '快速返回 OHLCV格式' return (__data.open, __data.high, __data.low, __data.close, __data.vol) def QA_backtest_send_order(self, __code, __amount, __towards, __order): """ 2017/8/4 委托函数 在外部封装的一个报价接口,尽量满足和实盘一样的模式 输入 ============= 买入/卖出 股票代码 买入/卖出数量 委托模式* 0 限价委托 LIMIT ORDER 1 市价委托 MARKET ORDER 2 严格模式(买入按最高价 卖出按最低价) STRICT ORDER 功能 ============= 1. 封装一个bid类(分配地址) 2. 检查账户/临时扣费 3. 检查市场(wrap) 4. 发送到_send_bid方法 """ # 必须是100股的倍数 # 封装bid __amount = int(__amount / 100) * 100 __bid = self.bid # init (__bid.order_id, __bid.user, __bid.strategy, __bid.code, __bid.date, __bid.datetime, __bid.sending_time, __bid.amount, __bid.towards) = (str(random.random()), self.setting.QA_setting_user_name, self.strategy_name, __code, self.running_date, str(self.now), self.running_date, __amount, __towards) if self.backtest_type in ['day', 'index_day']: __bid.type = '0x01' elif self.backtest_type in ['1min', '5min', '15min']: __bid.type = '0x02' # 检查账户/临时扣费 __bid, __market = self.__wrap_bid(self, __bid, __order) if __bid is not None and __market != 500: print( 'GET the Order Code %s Amount %s Price %s Towards %s Time %s' % (__bid.code, __bid.amount, __bid.price, __bid.towards, __bid.datetime)) self.__sync_order_LM(self, 'create_order', order_=__bid) def __sync_order_LM(self, event_, order_=None, order_id_=None, trade_id_=None, market_message_=None): """ 订单事件: 生命周期管理 Order-Lifecycle-Management status1xx 订单待生成 status3xx 初始化订单 临时扣除资产(可用现金/可卖股份) status3xx 订单存活(等待交易) status2xx 订单完全交易/未完全交易 status4xx 主动撤单 status500 订单死亡(每日结算) 恢复临时资产 ======= 1. 更新持仓 2. 更新现金 """ if event_ is 'init_': self.account.cash_available = self.account.cash[-1] self.account.sell_available = pd.DataFrame( self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() elif event_ is 'create_order': if order_ is not None: if order_.towards is 1: # 买入 if self.account.cash_available - order_.amount * order_.price > 0: self.account.cash_available -= order_.amount * order_.price order_.status = 300 # 修改订单状态 self.account.order_queue = self.account.order_queue.append( order_.to_df()) elif order_.towards is -1: if self.QA_backtest_sell_available( self, order_.code) - order_.amount >= 0: self.account.sell_available[ order_.code] -= order_.amount self.account.order_queue = self.account.order_queue.append( order_.to_df()) else: QA_util_log_info('Order Event Warning:%s in %s' % (event_, str(self.now))) elif event_ in ['wait', 'live']: # 订单存活 不会导致任何状态改变 pass elif event_ in ['cancel_order']: # 订单事件:主动撤单 # try: assert isinstance(order_id_, str) self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 400 # 注销事件 if order_.towards is 1: # 多单 撤单 现金增加 self.account.cash_available += self.account.order_queue.query( 'order_id=="order_id_"' )['amount'] * self.account.order_queue.query( 'order_id=="order_id_"')['price'] elif order_.towards is -1: # 空单撤单 可卖数量增加 self.account.sell_available[ order_.code] += self.account.order_queue.query( 'order_id=="order_id_"')['price'] elif event_ in ['daily_settle']: # 每日结算/全撤/把成交的买入/卖出单标记为500 同时结转 # 买入 """ 每日结算流程 - 同步实际的现金和仓位 - 清空留仓单/未成功的订单 """ self.account.cash_available = self.account.cash[-1] self.account.sell_available = pd.DataFrame( self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() self.account.order_queue = pd.DataFrame() elif event_ in ['t_0']: """ T+0交易事件 同步t+0的账户状态 /允许卖出 """ self.account.cash_available = self.account.cash[-1] self.account.sell_available = pd.DataFrame( self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() elif event_ in ['trade']: # try: assert isinstance(order_, QA_QAMarket_bid) assert isinstance(order_id_, str) assert isinstance(trade_id_, str) assert isinstance(market_message_, dict) if order_.towards is 1: # 买入 # 减少现金 order_.trade_id = trade_id_ order_.transact_time = self.now order_.amount -= market_message_['body']['bid']['amount'] if order_.amount == 0: # 完全交易 # 注销(成功交易)['买入单不能立即结转'] self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 200 elif order_.amount > 0: # 注销(成功交易) self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 203 self.account.order_queue.query('order_id=="order_id_"')[ 'amount'] -= market_message_['body']['bid']['amount'] elif order_.towards is -1: # self.account.sell_available[order_.code] -= market_message_[ # 'body']['bid']['amount'] # 当日卖出的股票 可以继续买入/ 可用资金增加(要减去手续费) self.account.cash_available += market_message_['body']['bid'][ 'amount'] * market_message_['body']['bid'][ 'price'] - market_message_['body']['fee']['commission'] order_.trade_id = trade_id_ order_.transact_time = self.now order_.amount -= market_message_['body']['bid']['amount'] if order_.amount == 0: # 注销(成功交易) self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 200 else: # 注销(成功交易) self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 203 self.account.order_queue[ self.account.order_queue['order_id'] == order_id_]['amount'] -= market_message_['body']['bid'][ 'amount'] else: QA_util_log_info( 'EventEngine Warning:Unknown type of order event in %s' % str(self.now)) def __QA_backtest_send_bid(self, __bid, __market=None): __message = self.market.receive_bid(__bid, __market) if __bid.towards == 1: # 扣费 # 以下这个订单时的bar的open扣费 # 先扔进去买入,再通过返回的值来判定是否成功 if __message['header']['status'] == 200 and __message['body'][ 'bid']['amount'] > 0: # 这个判断是为了 如果买入资金不充足,所以买入报了一个0量单的情况 # 如果买入量>0, 才判断为成功交易 QA_util_log_info( 'BUY %s Price %s Date %s Amount %s' % (__bid.code, __bid.price, __bid.datetime, __bid.amount)) self.__messages = self.account.QA_account_receive_deal( __message) return __message else: return __message # 下面是卖出操作,这里在卖出前需要考虑一个是否有仓位的问题:````````````` ` # 因为在股票中是不允许卖空操作的,所以这里是股票的交易引擎和期货的交易引擎的不同所在 elif __bid.towards == -1: # 如果是卖出操作 检查是否有持仓 # 股票中不允许有卖空操作 # 检查持仓面板 if __message['header']['status'] == 200: self.__messages = self.account.QA_account_receive_deal( __message) QA_util_log_info( 'SELL %s Price %s Date %s Amount %s' % (__bid.code, __bid.price, __bid.datetime, __bid.amount)) return __message else: # self.account.order_queue=self.account.order_queue.append(__bid.to_df()) return __message else: return "Error: No buy/sell towards" def QA_backtest_check_order(self, order_id_): '用于检查委托单的状态' """ 委托单被报入交易所会有一个回报,回报状态就是交易所返回的字段: 字段目前 2xx 是成功 4xx是失败 5xx是交易所无数据(停牌) 随着回测框架的不断升级,会有更多状态需要被管理: 200 委托成功,完全交易 203 委托成功,未完全成功 300 刚创建订单的时候 400 已撤单 500 服务器撤单/每日结算 """ return self.account.order_queue[self.account.order_queue['order_id'] == order_id_]['status'] def QA_backtest_status(self): return vars(self) def QA_backtest_sell_all(self): __hold_list = pd.DataFrame( self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() for item in self.strategy_stock_list: try: if __hold_list[item] > 0: self.QA_backtest_send_order(self, item, __hold_list[item], -1, {'bid_model': 'C'}) except: pass @classmethod def load_strategy(__backtest_cls, func, *arg, **kwargs): '策略加载函数' # 首先判断是否能满足回测的要求` __messages = {} __backtest_cls.__init_cash_per_stock = int( float(__backtest_cls.account.init_assest) / len(__backtest_cls.strategy_stock_list)) # 策略的交易日循环 for i in range(int(__backtest_cls.start_real_id), int(__backtest_cls.end_real_id) - 1, 1): __backtest_cls.running_date = __backtest_cls.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __backtest_cls.running_date) QA_util_log_info( tabulate( __backtest_cls.account.message['body']['account']['hold'])) __backtest_cls.now = __backtest_cls.running_date __backtest_cls.today = __backtest_cls.running_date # 交易前同步持仓状态 __backtest_cls.__sync_order_LM(__backtest_cls, 'init_') # 初始化事件 if __backtest_cls.backtest_type in ['day', 'd', 'index_day']: func(*arg, **kwargs) # 发委托单 __backtest_cls.__sell_from_order_queue(__backtest_cls) elif __backtest_cls.backtest_type in [ '1min', '5min', '15min', '30min', '60min', 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min' ]: if __backtest_cls.backtest_type in ['1min', 'index_1min']: type_ = '1min' elif __backtest_cls.backtest_type in ['5min', 'index_5min']: type_ = '5min' elif __backtest_cls.backtest_type in ['15min', 'index_15min']: type_ = '15min' elif __backtest_cls.backtest_type in ['30min', 'index_30min']: type_ = '30min' elif __backtest_cls.backtest_type in ['60min', 'index_60min']: type_ = '60min' daily_min = QA_util_make_min_index(__backtest_cls.today, type_) # 创造分钟线index # print(daily_min) for min_index in daily_min: __backtest_cls.now = min_index QA_util_log_info( '=================Min hold list====================') QA_util_log_info('in the begining of %s' % str(min_index)) QA_util_log_info( tabulate(__backtest_cls.account.message['body'] ['account']['hold'])) func(*arg, **kwargs) # 发委托单 __backtest_cls.__sell_from_order_queue(__backtest_cls) if __backtest_cls.backtest_type in [ 'index_1min', 'index_5min', 'index_15min' ]: __backtest_cls.__sync_order_LM(__backtest_cls, 't_0') __backtest_cls.__sync_order_LM(__backtest_cls, 'daily_settle') # 每日结算 # 最后一天 __backtest_cls.__end_of_trading(__backtest_cls) @classmethod def backtest_init(__backtest_cls, func, *arg, **kwargs): def __init_backtest(__backtest_cls, *arg, **kwargs): __backtest_cls.__QA_backtest_init(__backtest_cls) func(*arg, **kwargs) __backtest_cls.__QA_backtest_prepare(__backtest_cls) return __init_backtest(__backtest_cls) @classmethod def before_backtest(__backtest_cls, func, *arg, **kwargs): def __before_backtest(__backtest_cls, *arg, **kwargs): func(*arg, **kwargs) __backtest_cls.__QA_backtest_start(__backtest_cls) return __before_backtest(__backtest_cls) @classmethod def end_backtest(__backtest_cls, func, *arg, **kwargs): # yield __backtest_cls.cash __backtest_cls.__end_of_backtest(__backtest_cls, func, *arg, **kwargs) return func(*arg, **kwargs)
def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ self.strategy_stock_list = np.unique( self.strategy_stock_list).tolist() # 保证不会重复 if len(str(self.strategy_start_date)) == 10: self.strategy_start_time = str( self.strategy_start_date) + ' 15:00:00' elif len(str(self.strategy_start_date)) == 19: self.strategy_start_time = str(self.strategy_start_date) self.strategy_start_date = str(self.strategy_start_date)[0:10] else: self.__QA_backtest_log_info(self, 'Wrong start date format') if len(str(self.strategy_end_date)) == 10: self.strategy_end_time = str(self.strategy_end_date) + ' 15:00:00' elif len(str(self.strategy_end_date)) == 19: self.strategy_end_time = str(self.strategy_end_date) self.strategy_end_date = str(self.strategy_end_date)[0:10] else: self.__QA_backtest_log_info(self, 'Wrong end date format') # 重新初始账户资产 self.market = QA_Market(self.commission_fee_coeff) self.setting.QA_setting_init() self.account.init() self.account_d_value.append(self.account.init_assest) self.start_real_date = QA_util_get_real_date(self.strategy_start_date, self.trade_list, 1) self.start_real_time = str( self.start_real_date) + ' ' + self.strategy_start_time.split( ' ')[1] self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date(self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) self.end_real_time = str(self.end_real_date) + \ ' ' + self.strategy_end_time.split(' ')[1] # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 if self.benchmark_type in ['I', 'index']: self.benchmark_data = QA_fetch_index_day_adv( self.benchmark_code, self.trade_list[self.start_real_id - 1], self.end_real_date) elif self.benchmark_type in ['S', 'stock']: self.benchmark_data = QA_fetch_stock_day_adv( self.benchmark_code, self.trade_list[self.start_real_id - 1], self.end_real_date) if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap + 1)], self.trade_list[self.end_real_id]).to_qfq() elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: self.market_data = QA_fetch_stocklist_min_adv( self.strategy_stock_list, QA_util_time_gap(self.start_real_time, self.strategy_gap + 1, '<', self.backtest_type), QA_util_time_gap(self.end_real_time, 1, '>', self.backtest_type), self.backtest_type).to_qfq() elif self.backtest_type in ['index_day']: self.market_data = QA_fetch_index_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap + 1)], self.end_real_date) elif self.backtest_type in [ 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min' ]: self.market_data = QA_fetch_index_min_adv( self.strategy_stock_list, QA_util_time_gap(self.start_real_time, self.strategy_gap + 1, '<', self.backtest_type.split('_')[1]), QA_util_time_gap(self.end_real_time, 1, '>', self.backtest_type.split('_')[1]), self.backtest_type.split('_')[1]) self.market_data_dict = dict( zip(list(self.market_data.code), self.market_data.splits())) self.market_data_hashable = self.market_data.dicts self.dirs = '.{}QUANTAXIS_RESULT{}{}{}{}{}'.format( os.sep, os.sep, self.topic_name, os.sep, self.stratey_version, os.sep) os.makedirs(self.dirs, exist_ok=True)
class QA_Backtest_stock_day(): '因为涉及很多不继承类的情况,所以先单列出来' account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name market_data = [] today = '' def __init__(self): self.account = QA_Account() self.market = QA_Market() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.today = '' def __QA_backtest_init(self): """既然是被当做装饰器使用,就需要把变量设置放在装饰函数的前面,把函数放在装饰函数的后面""" # 设置回测的开始结束时间 self.strategy_start_date = str('2017-01-05') self.strategy_end_date = str('2017-07-01') # 设置回测标的,是一个list对象,不过建议只用一个标的 # gap是回测时,每日获取数据的前推日期(交易日) self.strategy_gap = int(60) # 设置全局的数据库地址,回测用户名,密码,并初始化 self.setting.QA_util_sql_mongo_ip = str('127.0.0.1') self.setting.QA_setting_user_name = str('admin') self.setting.QA_setting_user_password = str('admin') self.setting.QA_setting_init() # 回测的名字 self.strategy_name = str('example') # 股票的交易日历,真实回测的交易周期,和交易周期在交易日历中的id self.trade_list = QA_fetch_trade_date( self.setting.client.quantaxis.trade_date) self.benchmark_code = 'hs300' """ 这里会涉及一个区间的问题,开始时间是要向后推,而结束时间是要向前推,1代表向后推,-1代表向前推 """ self.strategy_stock_list = ['000001', '000002', '000004'] self.account.init_assest = 1000000 self.backtest_bid_model = 'market_price' def __QA_backtest_init_class(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.setting.QA_setting_init() self.account.init() self.start_real_date = QA_util_get_real_date(self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date(self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 self.market_data = QA_fetch_stocklist_day( self.strategy_stock_list, self.setting.client.quantaxis.stock_day, [ self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id] ]) def __QA_backtest_start(self, *args, **kwargs): """ 这个是回测流程开始的入口 """ assert len(self.strategy_stock_list) > 0 assert len(self.trade_list) > 0 assert isinstance(self.start_real_date, str) assert isinstance(self.end_real_date, str) assert len(self.market_data) == len(self.strategy_stock_list) QA_util_log_info('QUANTAXIS Backtest Engine Initial Successfully') QA_util_log_info('Basical Info: \n' + tabulate( [[str(__version__), str(self.strategy_name)]], headers=('Version', 'Strategy_name'))) QA_util_log_info('Stock_List: \n' + tabulate([self.strategy_stock_list])) # 初始化报价模式 self.__QA_backtest_set_bid_model(self) self.__messages = [] def __QA_backtest_set_bid_model(self): if self.backtest_bid_model == 'market_price': self.bid.bid['price'] = 'market_price' self.bid.bid['bid_model'] = 'auto' elif self.backtest_bid_model == 'close_price': self.bid.bid['price'] = 'close_price' self.bid.bid['bid_model'] = 'auto' elif self.backtest_bid_model == 'strategy': self.bid.bid['price'] = 0 self.bid.bid['bid_model'] = 'strategy' else: QA_util_log_info('support bid model') sys.exit() def __check_state(self, bid_price, bid_amount): pass def __QA_bid_amount(self, __strategy_amount, __amount): if __strategy_amount == 'mean': return float( float(self.account.message['body']['account']['cash'][-1]) / len(self.strategy_stock_list)), 'price' elif __strategy_amount == 'half': return __amount * 0.5, 'amount' elif __strategy_amount == 'all': return __amount, 'amount' def __QA_get_data_from_market(self, __id, stock_id): if __id > self.strategy_gap + 1: index_of_day = __id index_of_start = index_of_day - self.strategy_gap + 1 return self.market_data[stock_id][index_of_start:index_of_day + 1] else: return self.market_data[stock_id][0:__id + 1] def __QA_data_handle(self, __id, __stock_id): "已经废弃" market_data = self.__QA_get_data_from_market(__id, __stock_id) __message = self.account.message return {'market': market_data, 'account': __message} def __backtest_every_day_trading(self, i, func, *arg, **kwargs): # 正在进行的交易日期 __running_date = self.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __running_date) QA_util_log_info( tabulate(self.account.message['body']['account']['hold'])) for __j in range(0, len(self.strategy_stock_list)): if __running_date in [l[6] for l in self.market_data[__j]] and \ [l[6] for l in self.market_data[__j]].index(__running_date) \ > self.strategy_gap + 1: __data = self.__QA_data_handle([ __l[6] for __l in self.market_data[__j] ].index(__running_date), __j) __amount = 0 for item in __data['account']['body']['account']['hold']: if self.strategy_stock_list[__j] in item: __amount = __amount + item[3] if __amount > 0: __hold = 1 else: __hold = 0 __result = func(self, *arg, **kwargs) if float(self.account.message['body']['account']['cash'] [-1]) > 0: self.QA_backtest_excute_bid( __result, __running_date, __hold, str(self.strategy_stock_list[__j])[0:6], __amount) else: QA_util_log_info('not enough free money') else: pass def __end_of_trading(self, *arg, **kwargs): # 在回测的最后一天,平掉所有仓位(回测的最后一天是不买入的) # 回测最后一天的交易处理 while len(self.account.hold) > 1: __hold_list = self.account.hold[1::] pre_del_id = [] for item_ in range(0, len(__hold_list)): if __hold_list[item_][3] > 0: __last_bid = self.bid.bid __last_bid['amount'] = int(__hold_list[item_][3]) __last_bid['order_id'] = str(random.random()) __last_bid['price'] = 'close_price' __last_bid['code'] = str(__hold_list[item_][1]) __last_bid['date'] = self.trade_list[self.end_real_id] __last_bid['towards'] = -1 __last_bid['user'] = self.setting.QA_setting_user_name __last_bid['strategy'] = self.strategy_name __last_bid['bid_model'] = 'auto' __last_bid['status'] = '0x01' __last_bid['amount_model'] = 'amount' __message = self.market.receive_bid( __last_bid, self.setting.client) _remains_day = 0 while __message['header']['status'] == 500: # 停牌状态,这个时候按停牌的最后一天计算价值(假设平仓) __last_bid['date'] = self.trade_list[self.end_real_id - _remains_day] _remains_day += 1 __message = self.market.receive_bid( __last_bid, self.setting.client) # 直到市场不是为0状态位置,停止前推日期 self.__messages = self.account.QA_account_receive_deal( __message) else: pre_del_id.append(item_) pre_del_id.sort() pre_del_id.reverse() for item_x in pre_del_id: __hold_list.pop(item_x) def __end_of_backtest(self, *arg, **kwargs): # 开始分析 QA_util_log_info('start analysis====\n' + str(self.strategy_stock_list)) QA_util_log_info('=' * 10 + 'Trade History' + '=' * 10) QA_util_log_info('\n' + tabulate(self.account.history, headers=('date', 'code', 'price', 'towards', 'amounts', 'order_id', 'trade_id', 'commission'))) QA_util_log_info( tabulate(self.account.detail, headers=('date', 'code', 'price', 'amounts', 'order_id', 'trade_id', 'sell_price', 'sell_order_id', 'sell_trade_id', 'sell_date', 'left_amount', 'commission'))) __exist_time = int(self.end_real_id) - int(self.start_real_id) + 1 self.__benchmark_data = QA_fetch_index_day(self.benchmark_code, self.start_real_date, self.end_real_date) if len(self.__messages) > 1: performace = QA_backtest_analysis_start( self.setting.client, self.strategy_stock_list, self.__messages, self.trade_list[self.start_real_id:self.end_real_id + 1], self.market_data, self.__benchmark_data) _backtest_mes = { 'user': self.setting.QA_setting_user_name, 'strategy': self.strategy_name, 'stock_list': performace['code'], 'start_time': self.strategy_start_date, 'end_time': self.strategy_end_date, 'account_cookie': self.account.account_cookie, 'annualized_returns': performace['annualized_returns'], 'benchmark_annualized_returns': performace['benchmark_annualized_returns'], 'assets': performace['assets'], 'benchmark_assets': performace['benchmark_assets'], 'trade_date': performace['trade_date'], 'total_date': performace['total_date'], 'win_rate': performace['win_rate'], 'alpha': performace['alpha'], 'beta': performace['beta'], 'sharpe': performace['sharpe'], 'vol': performace['vol'], 'benchmark_vol': performace['benchmark_vol'], 'max_drop': performace['max_drop'], 'exist': __exist_time, 'time': datetime.datetime.now() } QA_SU_save_backtest_message(_backtest_mes, self.setting.client) QA_SU_save_account_message(self.__messages, self.setting.client) QA_SU_save_account_to_csv(self.__messages) # QA.QA_SU_save_backtest_message(analysis_message, self.setting.client) def QA_backtest_get_market_data(self, code, date): '这个函数封装了关于获取的方式' index_of_date = 0 index_of_code = self.strategy_stock_list.index(code) if date in [l[6] for l in self.market_data[index_of_code]]: index_of_date = [l[6] for l in self.market_data[index_of_code] ].index(date) return self.__QA_get_data_from_market(self, index_of_date, index_of_code) def QA_backtest_hold_amount(self, __code): __amount_hold = 0 for item in self.account.hold: if __code in item: __amount_hold += item[3] return __amount_hold def QA_backtest_get_OHLCV(self, __data): '快速返回 OHLCV格式' return (__data.T[1].astype(float).tolist(), __data.T[2].astype(float).tolist(), __data.T[3].astype(float).tolist(), __data.T[4].astype(float).tolist(), __data.T[5].astype(float).tolist()) def QA_backtest_send_order(self, __code: str, __amount: int, __towards: int, __order: dict): """ 2017/8/4 委托函数 在外部封装的一个报价接口,尽量满足和实盘一样的模式 输入 ============= 买入/卖出 股票代码 买入/卖出数量 委托模式* 0 限价委托 LIMIT ORDER 1 市价委托 MARKET ORDER 2 严格模式(买入按最高价 卖出按最低价) STRICT ORDER 输出 ============= 返回: 委托状态/委托id 成交状态/成交id/成交量/成交价 错误/错误id/ return bid_status,trade_status,error """ # 必须是100股的倍数 __amount = int(__amount / 100) * 100 # self.__QA_backtest_set_bid_model() if __order['bid_model'] in [ 'limit', 'Limit', 'Limited', 'limited', 'l', 'L', 0, '0' ]: # 限价委托模式 __bid_price = __order['price'] elif __order['bid_model'] in [ 'Market', 'market', 'MARKET', 'm', 'M', 1, '1' ]: __bid_price = 'market_price' elif __order['bid_model'] in ['strict', 'Strict', 's', 'S', '2', 2]: __bid_price = 'strict_price' elif __order['bid_model'] in [ 'close', 'close_price', 'c', 'C', '3', 3 ]: __bid_price = 'close_price' __bid = self.bid.bid __bid['order_id'] = str(random.random()) __bid['user'] = self.setting.QA_setting_user_name __bid['strategy'] = self.strategy_name __bid['code'] = __code __bid['date'] = self.running_date __bid['price'] = __bid_price __bid['amount'] = __amount if __towards == 1: # 这是买入的情况 买入的时候主要考虑的是能不能/有没有足够的钱来买入 __bid['towards'] = 1 __message = self.market.receive_bid(__bid, self.setting.client) # 先扔进去买入,再通过返回的值来判定是否成功 if float(self.account.message['body']['account']['cash'][-1]) > \ float(__message['body']['bid']['price']) * \ float(__message['body']['bid']['amount']): # 这里是买入资金充足的情况 # 不去考虑 pass else: # 如果买入资金不充足,则按照可用资金去买入 # 这里可以这样做的原因是在买入的时候 手续费为0 __message['body']['bid']['amount'] = int( float(self.account.message['body']['account']['cash'][-1]) / float( float(str(__message['body']['bid']['price'])[0:5]) * 100)) * 100 if __message['body']['bid']['amount'] > 0: # 这个判断是为了 如果买入资金不充足,所以买入报了一个0量单的情况 #如果买入量>0, 才判断为成功交易 self.account.QA_account_receive_deal(__message) return __message # 下面是卖出操作,这里在卖出前需要考虑一个是否有仓位的问题: # 因为在股票中是不允许卖空操作的,所以这里是股票的交易引擎和期货的交易引擎的不同所在 elif __towards == -1: # 如果是卖出操作 检查是否有持仓 # 股票中不允许有卖空操作 # 检查持仓面板 __amount_hold = self.QA_backtest_hold_amount(self, __code) if __amount_hold > 0: __bid['towards'] = -1 if __amount_hold >= __amount: pass else: __bid['amount'] = __amount_hold __message = self.market.receive_bid(__bid, self.setting.client) if __message['header']['status'] == 200: self.account.QA_account_receive_deal(__message) return __message else: err_info = 'Error: Not Enough amount for code %s in hold list' % str( __code) QA_util_log_expection(err_info) return err_info else: return "Error: No buy/sell towards" def QA_backtest_sell_all(self): while len(self.account.hold) > 1: __hold_list = self.account.hold[1::] pre_del_id = [] for item_ in range(0, len(__hold_list)): if __hold_list[item_][3] > 0: __last_bid = self.bid.bid __last_bid['amount'] = int(__hold_list[item_][3]) __last_bid['order_id'] = str(random.random()) __last_bid['price'] = 'close_price' __last_bid['code'] = str(__hold_list[item_][1]) __last_bid['date'] = self.today __last_bid['towards'] = -1 __last_bid['user'] = self.setting.QA_setting_user_name __last_bid['strategy'] = self.strategy_name __last_bid['bid_model'] = 'auto' __last_bid['status'] = '0x01' __last_bid['amount_model'] = 'amount' __message = self.market.receive_bid( __last_bid, self.setting.client) _remains_day = 0 while __message['header']['status'] == 500: # 停牌状态,这个时候按停牌的最后一天计算价值(假设平仓) __last_bid['date'] = self.trade_list[self.end_real_id - _remains_day] _remains_day += 1 __message = self.market.receive_bid( __last_bid, self.setting.client) # 直到市场不是为0状态位置,停止前推日期 self.__messages = self.account.QA_account_receive_deal( __message) else: pre_del_id.append(item_) pre_del_id.sort() pre_del_id.reverse() for item_x in pre_del_id: __hold_list.pop(item_x) @classmethod def load_strategy(__backtest_cls, func, *arg, **kwargs): '策略加载函数' # 首先判断是否能满足回测的要求` _info = {} _info['stock_list'] = __backtest_cls.strategy_stock_list __messages = {} __backtest_cls.__init_cash_per_stock = int( float(__backtest_cls.account.init_assest) / len(__backtest_cls.strategy_stock_list)) # 策略的交易日循环 for i in range(int(__backtest_cls.start_real_id), int(__backtest_cls.end_real_id) - 1, 1): __backtest_cls.running_date = __backtest_cls.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __backtest_cls.running_date) QA_util_log_info( tabulate( __backtest_cls.account.message['body']['account']['hold'])) __backtest_cls.today = __backtest_cls.running_date func(*arg, **kwargs) # 最后一天 __backtest_cls.__end_of_trading(__backtest_cls) @classmethod def backtest_init(__backtest_cls, func, *arg, **kwargs): def __init_backtest(__backtest_cls, *arg, **kwargs): __backtest_cls.__QA_backtest_init(__backtest_cls) func(*arg, **kwargs) __backtest_cls.__QA_backtest_init_class(__backtest_cls) return __init_backtest(__backtest_cls) @classmethod def before_backtest(__backtest_cls, func, *arg, **kwargs): func(*arg, **kwargs) __backtest_cls.__QA_backtest_start(__backtest_cls) @classmethod def end_backtest(__backtest_cls, func, *arg, **kwargs): # yield __backtest_cls.cash __backtest_cls.__end_of_backtest(__backtest_cls, func, *arg, **kwargs) return func(*arg, **kwargs)
class QA_Backtest_stock_day(): '最终目的还是实现一个通用的回测类' backtest_type = 'day' account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name market_data = [] now = '' today = '' def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = '' self.today = '' def __QA_backtest_init(self): """既然是被当做装饰器使用,就需要把变量设置放在装饰函数的前面,把函数放在装饰函数的后面""" # 设置回测的开始结束时间 self.strategy_start_date = str('2017-01-05') self.strategy_end_date = str('2017-07-01') # 设置回测标的,是一个list对象,不过建议只用一个标的 # gap是回测时,每日获取数据的前推日期(交易日) self.strategy_gap = int(60) # 设置全局的数据库地址,回测用户名,密码,并初始化 self.setting.QA_util_sql_mongo_ip = str('127.0.0.1') self.setting.QA_setting_user_name = str('admin') self.setting.QA_setting_user_password = str('admin') self.setting.QA_setting_init() # 回测的名字 self.strategy_name = str('example_min') # 股票的交易日历,真实回测的交易周期,和交易周期在交易日历中的id self.trade_list = QA_fetch_trade_date( self.setting.client.quantaxis.trade_date) self.benchmark_code = 'hs300' """ 这里会涉及一个区间的问题,开始时间是要向后推,而结束时间是要向前推,1代表向后推,-1代表向前推 """ self.strategy_stock_list = ['000001', '000002', '000004'] self.account.init_assest = 1000000 self.backtest_bid_model = 'market_price' def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.setting.QA_setting_init() self.account.init() self.start_real_date = QA_util_get_real_date( self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date( self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day( self.strategy_stock_list, [self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id]]) elif self.backtest_type in ['min', 'm', '0x01']: self.market_data = QA_fetch_stocklist_min( self.strategy_stock_list, [self.trade_list[ self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id]]) def __QA_backtest_start(self, *args, **kwargs): """ 这个是回测流程开始的入口 """ assert len(self.strategy_stock_list) > 0 assert len(self.trade_list) > 0 assert isinstance(self.start_real_date, str) assert isinstance(self.end_real_date, str) assert len(self.market_data) == len(self.strategy_stock_list) QA_util_log_info('QUANTAXIS Backtest Engine Initial Successfully') QA_util_log_info('Basical Info: \n' + tabulate( [[str(__version__), str(self.strategy_name)]], headers=('Version', 'Strategy_name'))) QA_util_log_info('BACKTEST Cookie_ID is: ' + str(self.account.account_cookie)) QA_util_log_info('Stock_List: \n' + tabulate([self.strategy_stock_list])) # 初始化报价模式 self.__QA_backtest_set_bid_model(self) self.__messages = [] def __QA_backtest_set_bid_model(self): if self.backtest_bid_model == 'market_price': self.bid.price = 'market_price' self.bid.bid_model = 'auto' elif self.backtest_bid_model == 'close_price': self.bid.price = 'close_price' self.bid.bid_model = 'auto' elif self.backtest_bid_model == 'strategy': self.bid.price = 0 self.bid.bid_model = 'strategy' else: QA_util_log_info('support bid model') sys.exit() def __check_state(self, bid_price, bid_amount): pass def __QA_bid_amount(self, __strategy_amount, __amount): if __strategy_amount == 'mean': return float(float(self.account.message['body']['account']['cash'][-1]) / len(self.strategy_stock_list)), 'price' elif __strategy_amount == 'half': return __amount * 0.5, 'amount' elif __strategy_amount == 'all': return __amount, 'amount' def __end_of_trading(self, *arg, **kwargs): # 在回测的最后一天,平掉所有仓位(回测的最后一天是不买入的) # 回测最后一天的交易处理 while len(self.account.hold) > 1: __hold_list = self.account.hold[1::] pre_del_id = [] for item_ in range(0, len(__hold_list)): if __hold_list[item_][3] > 0: __last_bid = self.bid __last_bid.amount = int(__hold_list[item_][3]) __last_bid.order_id = str(random.random()) __last_bid.price = 'close_price' __last_bid.code = str(__hold_list[item_][1]) __last_bid.date = self.trade_list[self.end_real_id] __last_bid.towards = -1 __last_bid.user = self.setting.QA_setting_user_name __last_bid.strategy = self.strategy_name __last_bid.bid_model = 'auto' __last_bid.type = '0x01' __last_bid.amount_model = 'amount' __message = self.market.receive_bid( __last_bid) _remains_day = 0 while __message['header']['status'] == 500: # 停牌状态,这个时候按停牌的最后一天计算价值(假设平仓) __last_bid.date = self.trade_list[self.end_real_id - _remains_day] _remains_day += 1 __message = self.market.receive_bid( __last_bid) # 直到市场不是为0状态位置,停止前推日期 self.__messages = self.account.QA_account_receive_deal( __message) else: pre_del_id.append(item_) pre_del_id.sort() pre_del_id.reverse() for item_x in pre_del_id: __hold_list.pop(item_x) def __end_of_backtest(self, *arg, **kwargs): # 开始分析 QA_util_log_info('start analysis====\n' + str(self.strategy_stock_list)) QA_util_log_info('=' * 10 + 'Trade History' + '=' * 10) QA_util_log_info('\n' + tabulate(self.account.history, headers=('date', 'code', 'price', 'towards', 'amounts', 'order_id', 'trade_id', 'commission'))) QA_util_log_info('\n' + tabulate(self.account.detail, headers=('date', 'code', 'price', 'amounts', 'order_id', 'trade_id', 'sell_price', 'sell_order_id', 'sell_trade_id', 'sell_date', 'left_amount', 'commission'))) __exist_time = int(self.end_real_id) - int(self.start_real_id) + 1 self.__benchmark_data = QA_fetch_index_day( self.benchmark_code, self.start_real_date, self.end_real_date) if len(self.__messages) > 1: performace = QA_backtest_analysis_start( self.setting.client, self.strategy_stock_list, self.__messages, self.trade_list[self.start_real_id:self.end_real_id + 1], self.market_data, self.__benchmark_data) _backtest_mes = { 'user': self.setting.QA_setting_user_name, 'strategy': self.strategy_name, 'stock_list': performace['code'], 'start_time': self.strategy_start_date, 'end_time': self.strategy_end_date, 'account_cookie': self.account.account_cookie, 'annualized_returns': performace['annualized_returns'], 'benchmark_annualized_returns': performace['benchmark_annualized_returns'], 'assets': performace['assets'], 'benchmark_assets': performace['benchmark_assets'], 'trade_date': performace['trade_date'], 'total_date': performace['total_date'], 'win_rate': performace['win_rate'], 'alpha': performace['alpha'], 'beta': performace['beta'], 'sharpe': performace['sharpe'], 'vol': performace['vol'], 'benchmark_vol': performace['benchmark_vol'], 'max_drop': performace['max_drop'], 'exist': __exist_time, 'time': datetime.datetime.now() } QA_SU_save_backtest_message(_backtest_mes, self.setting.client) QA_SU_save_account_message(self.__messages, self.setting.client) QA_SU_save_account_to_csv(self.__messages) # QA.QA_SU_save_backtest_message(analysis_message, self.setting.client) def QA_backtest_get_market_data(self, code, date, type_='numpy'): '这个函数封装了关于获取的方式' index_of_code = self.strategy_stock_list.index(code) __res = self.market_data[index_of_code][:date].tail(self.strategy_gap) if type_ in ['l', 'list', 'L']: return np.asarray(__res).tolist() elif type_ in ['pd', 'pandas', 'p']: return __res else: return np.asarray(__res) def QA_backtest_hold_amount(self, __code): return sum(list(map(lambda item: item[3] if __code in item else 0, self.account.hold))) def QA_backtest_get_OHLCV(self, __data): '快速返回 OHLCV格式' return (__data.T[1].astype(float).tolist(), __data.T[2].astype(float).tolist(), __data.T[3].astype(float).tolist( ), __data.T[4].astype(float).tolist(), __data.T[5].astype(float).tolist()) def QA_backtest_send_order(self, __code, __amount, __towards, __order): """ 2017/8/4 委托函数 在外部封装的一个报价接口,尽量满足和实盘一样的模式 输入 ============= 买入/卖出 股票代码 买入/卖出数量 委托模式* 0 限价委托 LIMIT ORDER 1 市价委托 MARKET ORDER 2 严格模式(买入按最高价 卖出按最低价) STRICT ORDER 输出 ============= 返回: 委托状态/委托id 成交状态/成交id/成交量/成交价 错误/错误id/ return bid_status,trade_status,error """ # 必须是100股的倍数 __amount = int(__amount / 100) * 100 # self.__QA_backtest_set_bid_model() if __order['bid_model'] in ['limit', 'Limit', 'Limited', 'limited', 'l', 'L', 0, '0']: # 限价委托模式 __bid_price = __order['price'] elif __order['bid_model'] in ['Market', 'market', 'MARKET', 'm', 'M', 1, '1']: __bid_price = 'market_price' elif __order['bid_model'] in ['strict', 'Strict', 's', 'S', '2', 2]: __bid_price = 'strict_price' elif __order['bid_model'] in ['close', 'close_price', 'c', 'C', '3', 3]: __bid_price = 'close_price' __bid = self.bid __bid.order_id = str(random.random()) __bid.user = self.setting.QA_setting_user_name __bid.strategy = self.strategy_name __bid.code = __code __bid.date = self.running_date __bid.datetime = self.running_date __bid.sending_time = self.running_date __bid.price = __bid_price __bid.amount = __amount if __towards == 1: # 这是买入的情况 买入的时候主要考虑的是能不能/有没有足够的钱来买入 __bid.towards = 1 __message = self.market.receive_bid( __bid) # 先扔进去买入,再通过返回的值来判定是否成功 if float(self.account.message['body']['account']['cash'][-1]) > \ float(__message['body']['bid']['price']) * \ float(__message['body']['bid']['amount']): # 这里是买入资金充足的情况 # 不去考虑 pass else: # 如果买入资金不充足,则按照可用资金去买入 # 这里可以这样做的原因是在买入的时候 手续费为0 __message['body']['bid']['amount'] = int(float( self.account.message['body']['account']['cash'][-1]) / float( float(str(__message['body']['bid']['price'])[0:5]) * 100)) * 100 if __message['body']['bid']['amount'] > 0: # 这个判断是为了 如果买入资金不充足,所以买入报了一个0量单的情况 #如果买入量>0, 才判断为成功交易 self.messages=self.account.QA_account_receive_deal(__message) return __message # 下面是卖出操作,这里在卖出前需要考虑一个是否有仓位的问题: # 因为在股票中是不允许卖空操作的,所以这里是股票的交易引擎和期货的交易引擎的不同所在 elif __towards == -1: # 如果是卖出操作 检查是否有持仓 # 股票中不允许有卖空操作 # 检查持仓面板 __amount_hold = self.QA_backtest_hold_amount(self, __code) if __amount_hold > 0: __bid.towards = -1 __bid.amount = __amount_hold if __amount_hold < __amount else __bid.amount __message = self.market.receive_bid( __bid) if __message['header']['status'] == 200: self.messages=self.account.QA_account_receive_deal(__message) return __message else: err_info = 'Error: Not Enough amount for code %s in hold list' % str( __code) QA_util_log_expection(err_info) return err_info else: return "Error: No buy/sell towards" def QA_backtest_check_order(self, order): '用于检查委托单的状态' """ 委托单被报入交易所会有一个回报,回报状态就是交易所返回的字段: 字段目前 2xx 是成功 4xx是失败 5xx是交易所无数据(停牌) 随着回测框架的不断升级,会有更多状态需要被管理: 200 委托成功,交易成功 203 委托成功,待成交 20 """ pass def QA_backtest_sell_all(self): while len(self.account.hold) > 1: __hold_list = self.account.hold[1::] pre_del_id = [] def __sell(id_): if __hold_list[id_][3] > 0: __last_bid = self.bid __last_bid.amount = int(__hold_list[id_][3]) __last_bid.order_id = str(random.random()) __last_bid.price = 'close_price' __last_bid.code = str(__hold_list[id_][1]) __last_bid.date = self.now __last_bid.towards = -1 __last_bid.user = self.setting.QA_setting_user_name __last_bid.strategy = self.strategy_name __last_bid.bid_model = 'auto' __last_bid.type = '0x01' __last_bid.amount_model = 'amount' __message = self.market.receive_bid( __last_bid) _remains_day = 0 while __message['header']['status'] == 500: # 停牌状态,这个时候按停牌的最后一天计算价值(假设平仓) __last_bid.date = self.trade_list[self.end_real_id - _remains_day] _remains_day += 1 __message = self.market.receive_bid( __last_bid) # 直到市场不是为0状态位置,停止前推日期 self.__messages = self.account.QA_account_receive_deal( __message) else: pre_del_id.append(id_) return pre_del_id pre_del_id = reduce(lambda _, x: __sell(x), range(len(__hold_list))) pre_del_id.sort() pre_del_id.reverse() for item_x in pre_del_id: __hold_list.pop(item_x) @classmethod def load_strategy(__backtest_cls, func, *arg, **kwargs): '策略加载函数' # 首先判断是否能满足回测的要求` __messages = {} __backtest_cls.__init_cash_per_stock = int( float(__backtest_cls.account.init_assest) / len(__backtest_cls.strategy_stock_list)) # 策略的交易日循环 for i in range(int(__backtest_cls.start_real_id), int(__backtest_cls.end_real_id) - 1, 1): __backtest_cls.running_date = __backtest_cls.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __backtest_cls.running_date) QA_util_log_info( tabulate(__backtest_cls.account.message['body']['account']['hold'])) __backtest_cls.now = __backtest_cls.running_date __backtest_cls.today = __backtest_cls.running_date func(*arg, **kwargs) # 最后一天 __backtest_cls.__end_of_trading(__backtest_cls) @classmethod def backtest_init(__backtest_cls, func, *arg, **kwargs): def __init_backtest(__backtest_cls, *arg, **kwargs): __backtest_cls.__QA_backtest_init(__backtest_cls) func(*arg, **kwargs) __backtest_cls.__QA_backtest_prepare(__backtest_cls) return __init_backtest(__backtest_cls) @classmethod def before_backtest(__backtest_cls, func, *arg, **kwargs): func(*arg, **kwargs) __backtest_cls.__QA_backtest_start(__backtest_cls) @classmethod def end_backtest(__backtest_cls, func, *arg, **kwargs): # yield __backtest_cls.cash __backtest_cls.__end_of_backtest(__backtest_cls, func, *arg, **kwargs) return func(*arg, **kwargs)
class QA_Backtest_with_class(): '因为是装饰器调用的 所以变量要写在外面 类装饰器不会调用__init__' backtest_type = 'day' account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() order = QA_QAMarket_bid_list() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name market_data = [] now = None today = None last_time = None strategy_stock_list = [] trade_list = [] start_real_id = 0 end_real_id = 0 temp = {} commission_fee_coeff = 0.0015 strategy_start_date = '' strategy_start_time = '' strategy_end_date = '' strategy_end_time = '' benchmark_type = 'index' account_d_value = [] account_d_key = [] market_data_dict = {} backtest_print_log = True if_save_to_mongo = True if_save_to_csv = True outside_data = [] outside_data_dict = [] outside_data_hashable = {} topic_name = 'EXAMPLE' stratey_version = 'V1' absoult_path = sys.path[0] def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.order = QA_QAMarket_bid_list() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = None self.last_time = None self.strategy_start_date = '' self.strategy_start_time = '' self.strategy_end_date = '' self.strategy_end_time = '' self.today = None self.strategy_stock_list = [] self.trade_list = [] self.start_real_id = 0 self.end_real_id = 0 self.temp = {} self.commission_fee_coeff = 0.0015 self.account_d_value = [] self.account_d_key = [] self.benchmark_type = 'index' self.market_data_dict = {} self.backtest_print_log = True # 打印 self.if_save_to_mongo = True self.if_save_to_csv = True self.stratey_version = 'V1' self.topic_name = 'EXAMPLE' self.outside_data = [] self.outside_data_dict = [] self.outside_data_hashable = {} self.absoult_path = sys.path[0] self.dirs = '{}{}QUANTAXIS_RESULT{}{}{}{}{}'.format( self.absoult_path, os.sep, os.sep, self.topic_name, os.sep, self.stratey_version, os.sep) def __QA_backtest_init(self): self.__init__() """既然是被当做装饰器使用,就需要把变量设置放在装饰函数的前面,把函数放在装饰函数的后面""" # 设置回测的开始结束时间 self.strategy_start_date = str('2017-01-05') self.strategy_end_date = str('2017-07-01') # 设置回测标的,是一个list对象,不过建议只用一个标的 # gap是回测时,每日获取数据的前推日期(交易日) self.strategy_gap = int(60) # 设置全局的数据库地址,回测用户名,密码,并初始化 self.setting.QA_util_sql_mongo_ip = str('127.0.0.1') self.setting.QA_setting_user_name = str('admin') self.setting.QA_setting_user_password = str('admin') self.setting.QA_setting_init() # 回测的名字 self.strategy_name = str('example_min') # 股票的交易日历,真实回测的交易周期,和交易周期在交易日历中的id self.trade_list = trade_date_sse self.benchmark_code = '000300' """ 这里会涉及一个区间的问题,开始时间是要向后推,而结束时间是要向前推,1代表向后推,-1代表向前推 """ self.strategy_stock_list = ['000001', '000002', '000004'] self.account.init_assest = 1000000 self.backtest_bid_model = 'market_price' self.commission_fee_coeff = 0.0015 def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ self.strategy_stock_list = np.unique( self.strategy_stock_list).tolist() # 保证不会重复 if len(str(self.strategy_start_date)) == 10: self.strategy_start_time = str( self.strategy_start_date) + ' 15:00:00' elif len(str(self.strategy_start_date)) == 19: self.strategy_start_time = str(self.strategy_start_date) self.strategy_start_date = str(self.strategy_start_date)[0:10] else: self.__QA_backtest_log_info('Wrong start date format') if len(str(self.strategy_end_date)) == 10: self.strategy_end_time = str(self.strategy_end_date) + ' 15:00:00' elif len(str(self.strategy_end_date)) == 19: self.strategy_end_time = str(self.strategy_end_date) self.strategy_end_date = str(self.strategy_end_date)[0:10] else: self.__QA_backtest_log_info('Wrong end date format') # 重新初始账户资产 self.market = QA_Market(self.commission_fee_coeff) self.setting.QA_setting_init() self.account.init() self.account_d_value.append(self.account.init_assest) self.start_real_date = QA_util_get_real_date(self.strategy_start_date, self.trade_list, 1) self.start_real_time = str( self.start_real_date) + ' ' + self.strategy_start_time.split( ' ')[1] self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date(self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) self.end_real_time = str(self.end_real_date) + \ ' ' + self.strategy_end_time.split(' ')[1] # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 if self.benchmark_type in ['I', 'index']: self.benchmark_data = QA_fetch_index_day_adv( self.benchmark_code, self.trade_list[self.start_real_id - 1], self.end_real_date) elif self.benchmark_type in ['S', 'stock']: self.benchmark_data = QA_fetch_stock_day_adv( self.benchmark_code, self.trade_list[self.start_real_id - 1], self.end_real_date) if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap + 1)], self.trade_list[self.end_real_id]).to_qfq() elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: self.market_data = QA_fetch_stocklist_min_adv( self.strategy_stock_list, QA_util_time_gap(self.start_real_time, self.strategy_gap + 1, '<', self.backtest_type), QA_util_time_gap(self.end_real_time, 1, '>', self.backtest_type), self.backtest_type).to_qfq() elif self.backtest_type in ['index_day']: self.market_data = QA_fetch_index_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int(self.strategy_gap + 1)], self.end_real_date) elif self.backtest_type in [ 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min' ]: self.market_data = QA_fetch_index_min_adv( self.strategy_stock_list, QA_util_time_gap(self.start_real_time, self.strategy_gap + 1, '<', self.backtest_type.split('_')[1]), QA_util_time_gap(self.end_real_time, 1, '>', self.backtest_type.split('_')[1]), self.backtest_type.split('_')[1]) self.market_data_dict = dict( zip(list(self.market_data.code), self.market_data.splits())) self.market_data_hashable = self.market_data.dicts self.dirs = '{}{}QUANTAXIS_RESULT{}{}{}{}{}'.format( self.absoult_path, os.sep, os.sep, self.topic_name, os.sep, self.stratey_version, os.sep) os.makedirs(self.dirs, exist_ok=True) self.lastest_price = {} try: self.outside_data_dict = dict( zip(list(self.outside_data.code), self.outside_data.splits())) self.outside_data_hashable = self.outside_data.dicts except: pass def __QA_backtest_log_info(self, log): if self.backtest_print_log: return QA_util_log_info(log) else: pass def __QA_backtest_before_backtest(self, *args, **kwargs): """ 这个是回测流程开始的入口 """ self.__QA_backtest_log_info( 'QUANTAXIS Backtest Engine Initial Successfully') self.__QA_backtest_log_info('Basical Info: \n' + tabulate( [[str(__version__), str(self.strategy_name)]], headers=('Version', 'Strategy_name'))) self.__QA_backtest_log_info('BACKTEST Cookie_ID is: ' + str(self.account.account_cookie)) self.__QA_backtest_log_info('Stock_List: \n' + tabulate([self.strategy_stock_list])) # 初始化报价模式 self.__messages = [] def __save_strategy_files(self): file_name = '{}backtest_{}.py'.format(self.dirs, self.account.account_cookie) with open(sys.argv[0], 'rb') as p: data = p.read() collection = self.setting.client.quantaxis.strategy collection.insert({ 'cookie': self.account.account_cookie, 'name': self.strategy_name, 'topic': self.topic_name, 'version': self.stratey_version, 'user': self.user, 'datetime': datetime.datetime.now(), 'content': data.decode('utf-8'), 'dirs': self.dirs, 'absoultpath': self.absoult_path }) with open(file_name, 'wb') as f: f.write(data) def __QA_bid_amount(self, __strategy_amount, __amount): if __strategy_amount == 'mean': return float( float(self.account.message['body']['account']['cash'][-1]) / len(self.strategy_stock_list)), 'price' elif __strategy_amount == 'half': return __amount * 0.5, 'amount' elif __strategy_amount == 'all': return __amount, 'amount' def _make_slice(self): QA_Setting.client.quantaxis.slice.insert({ 'cookie': self.account.account_cookie, 'account_message': self.__messages, 'account_d_value': self.account_d_value, 'account_d_key': self.account_d_key, 'now': self.now, 'today': self.today, 'running_date': self.running_date, 'strategy_stock_list': self.strategy_stock_list, 'dirs': self.dirs, }) def _end_of_trading(self, *arg, **kwargs): # 在回测的最后一天,平掉所有仓位(回测的最后一天是不买入的) # 回测最后一天的交易处理 # self._make_slice(self) if self.backtest_type in ['day']: self.now = str(self.end_real_date) self.today = str(self.end_real_date) elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: self.now = str(self.end_real_date) + ' 15:00:00' self.today = str(self.end_real_date) elif self.backtest_type in ['index_day']: self.now = str(self.end_real_date) self.today = str(self.end_real_date) elif self.backtest_type in [ 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min' ]: self.now = str(self.end_real_date) + ' 15:00:00' self.today = str(self.end_real_date) self.today = self.end_real_date self.sell_all() self._deal_from_order_queue() self.__sync_order_LM('daily_settle') # 每日结算 def _deal_from_order_queue(self): # 每个bar结束的时候,批量交易 __result = [] self.order.__init__() if len(self.account.order_queue) >= 1: __bid_list = self.order.from_dataframe( self.account.order_queue.query('status!=200').query( 'status!=500').query('status!=400')) for item in __bid_list: # 在发单的时候要改变交易日期 item.date = self.today item.datetime = self.now __bid, __market = self.__wrap_bid(self, item) __message = self.__send_bid(__bid, __market) if isinstance(__message, dict): if __message['header']['status'] in ['200', 200]: self.__sync_order_LM('trade', __bid, __message['header']['order_id'], __message['header']['trade_id'], __message) else: self.__sync_order_LM('wait') else: self.__QA_backtest_log_info( 'FROM BACKTEST: Order Queue is empty at %s!' % self.now) pass def __sync_order_LM(self, event_, order_=None, order_id_=None, trade_id_=None, market_message_=None): """ 订单事件: 生命周期管理 Order-Lifecycle-Management status1xx 订单待生成 status3xx 初始化订单 临时扣除资产(可用现金/可卖股份) status3xx 订单存活(等待交易) status2xx 订单完全交易/未完全交易 status4xx 主动撤单 status500 订单死亡(每日结算) 恢复临时资产 ======= 1. 更新持仓 2. 更新现金 """ if event_ is 'init_': self.account.cash_available = self.account.cash[-1] self.account.sell_available = pd.DataFrame( self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() elif event_ is 'create_order': if order_ is not None: if order_.towards is 1: # 买入 if self.account.cash_available - order_.amount * order_.price > 0: self.account.cash_available -= order_.amount * order_.price order_.status = 300 # 修改订单状态 self.account.order_queue = self.account.order_queue.append( order_.to_df()) else: self.__QA_backtest_log_info( 'FROM ENGINE: NOT ENOUGH MONEY:CASH %s Order %s' % (self.account.cash_available, order_.amount * order_.price)) elif order_.towards is -1: if self.QA_backtest_sell_available( order_.code) - order_.amount >= 0: self.account.sell_available[ order_.code] -= order_.amount self.account.order_queue = self.account.order_queue.append( order_.to_df()) else: self.__QA_backtest_log_info('Order Event Warning:%s in %s' % (event_, str(self.now))) elif event_ in ['wait', 'live']: # 订单存活 不会导致任何状态改变 pass elif event_ in ['cancel_order']: # 订单事件:主动撤单 # try: assert isinstance(order_id_, str) self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 400 # 注销事件 if order_.towards is 1: # 多单 撤单 现金增加 self.account.cash_available += self.account.order_queue.query( 'order_id=="order_id_"' )['amount'] * self.account.order_queue.query( 'order_id=="order_id_"')['price'] elif order_.towards is -1: # 空单撤单 可卖数量增加 self.account.sell_available[ order_.code] += self.account.order_queue.query( 'order_id=="order_id_"')['price'] elif event_ in ['daily_settle']: # 每日结算/全撤/把成交的买入/卖出单标记为500 同时结转 # 买入 """ 每日结算流程 - 同步实际的现金和仓位 - 清空留仓单/未成功的订单 """ self.account.cash_available = self.account.cash[-1] self.account.sell_available = self.QA_backtest_hold( )['amount'].groupby('code').sum() self.account.order_queue = pd.DataFrame() self.account_d_key.append(self.today) if len(self.account.hold) > 1: self.account_d_value.append(self.account.cash[-1] + sum([ self.lastest_price[self.account.hold[i][1]] * float(self.account.hold[i][3]) for i in range(1, len(self.account.hold)) ])) else: self.account_d_value.append(self.account.cash[-1]) elif event_ in ['t_0']: """ T+0交易事件 同步t+0的账户状态 /允许卖出 """ self.account.cash_available = self.account.cash[-1] self.account.sell_available = self.QA_backtest_hold( )['amount'].groupby('code').sum() elif event_ in ['trade']: # try: assert isinstance(order_, QA_QAMarket_bid) assert isinstance(order_id_, str) assert isinstance(trade_id_, str) assert isinstance(market_message_, dict) if order_.towards is 1: # 买入 # 减少现金 order_.trade_id = trade_id_ order_.transact_time = self.now order_.amount -= market_message_['body']['bid']['amount'] if order_.amount == 0: # 完全交易 # 注销(成功交易)['买入单不能立即结转'] self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 200 elif order_.amount > 0: # 注销(成功交易) self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 203 self.account.order_queue.query('order_id=="order_id_"')[ 'amount'] -= market_message_['body']['bid']['amount'] elif order_.towards is -1: # self.account.sell_available[order_.code] -= market_message_[ # 'body']['bid']['amount'] # 当日卖出的股票 可以继续买入/ 可用资金增加(要减去手续费) self.account.cash_available += market_message_['body']['bid'][ 'amount'] * market_message_['body']['bid'][ 'price'] - market_message_['body']['fee']['commission'] order_.trade_id = trade_id_ order_.transact_time = self.now order_.amount -= market_message_['body']['bid']['amount'] if order_.amount == 0: # 注销(成功交易) self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 200 else: # 注销(成功交易) self.account.order_queue.loc[ self.account.order_queue['order_id'] == order_id_, 'status'] = 203 self.account.order_queue[ self.account.order_queue['order_id'] == order_id_]['amount'] -= market_message_['body']['bid'][ 'amount'] else: self.__QA_backtest_log_info( 'EventEngine Warning: Unknown type of order event in %s' % str(self.now)) def __send_bid(self, __bid, __market=None): __message = self.market.receive_bid(__bid, __market) if __bid.towards == 1: # 扣费 # 以下这个订单时的bar的open扣费 # 先扔进去买入,再通过返回的值来判定是否成功 if __message['header']['status'] == 200 and __message['body'][ 'bid']['amount'] > 0: # 这个判断是为了 如果买入资金不充足,所以买入报了一个0量单的情况 # 如果买入量>0, 才判断为成功交易 self.__QA_backtest_log_info( 'BUY %s Price %s Date %s Amount %s' % (__bid.code, __bid.price, __bid.datetime, __bid.amount)) self.__messages = self.account.QA_account_receive_deal( __message) return __message else: return __message # 下面是卖出操作,这里在卖出前需要考虑一个是否有仓位的问题:````````````` ` # 因为在股票中是不允许卖空操作的,所以这里是股票的交易引擎和期货的交易引擎的不同所在 elif __bid.towards == -1: # 如果是卖出操作 检查是否有持仓 # 股票中不允许有卖空操作 # 检查持仓面板 if __message['header']['status'] == 200: self.__messages = self.account.QA_account_receive_deal( __message) self.__QA_backtest_log_info( 'SELL %s Price %s Date %s Amount %s' % (__bid.code, __bid.price, __bid.datetime, __bid.amount)) return __message else: # self.account.order_queue=self.account.order_queue.append(__bid.to_df()) return __message else: return "Error: No buy/sell towards" def __wrap_bid(self, __bid, __order=None): __market_data_for_backtest = self.find_bar(__bid.code, __bid.datetime) if __market_data_for_backtest is not None: if __market_data_for_backtest[ 'open'] is not None and __order is not None: if __order['bid_model'] in [ 'limit', 'Limit', 'Limited', 'limited', 'l', 'L', 0, '0' ]: # 限价委托模式 __bid.price = __order['price'] elif __order['bid_model'] in [ 'Market', 'market', 'MARKET', 'm', 'M', 1, '1' ]: # 2017-09-18 修改 市价单以当前bar开盘价下单 __bid.price = float(__market_data_for_backtest['open']) elif __order['bid_model'] in [ 'strict', 'Strict', 's', 'S', '2', 2 ]: __bid.price = float(__market_data_for_backtest['high'] ) if __bid.towards == 1 else float( __market_data_for_backtest['low']) elif __order['bid_model'] in [ 'close', 'close_price', 'c', 'C', '3', 3 ]: __bid.price = float(__market_data_for_backtest['close']) __bid.price = float('%.2f' % __bid.price) return __bid, __market_data_for_backtest else: return __bid, __market_data_for_backtest else: self.__QA_backtest_log_info( 'BACKTEST ENGINE ERROR=== CODE %s TIME %s NO MARKET DATA!' % (__bid.code, __bid.datetime)) return __bid, None def __end_of_backtest(self, *arg, **kwargs): # 开始分析 # 对于account.detail做一定的整理 self.account.detail = pd.DataFrame( self.account.detail, columns=[ 'date', 'code', 'price', 'amounts', 'order_id', 'trade_id', 'sell_price', 'sell_order_id', 'sell_trade_id', 'sell_date', 'left_amount', 'commission' ]) def __mean(list_): if len(list_) > 0: return mean(list_) else: return 'No Data' self.account.detail['sell_average'] = self.account.detail[ 'sell_price'].apply(lambda x: __mean(x)) try: self.account.detail['pnl_price'] = self.account.detail['sell_average'] - \ self.account.detail['price'] self.account.detail['pnl'] = self.account.detail['pnl_price'] * ( self.account.detail['amounts'] - self.account. detail['left_amount']) - self.account.detail['commission'] self.account.detail['pnl_precentage'] = self.account.detail['pnl_price'] / \ self.account.detail['price'] except: pass self.account.detail = self.account.detail.drop( ['order_id', 'trade_id', 'sell_order_id', 'sell_trade_id'], axis=1) self.__QA_backtest_log_info('start analysis====\n' + str(self.strategy_stock_list)) self.__QA_backtest_log_info('=' * 10 + 'Trade History' + '=' * 10) self.__QA_backtest_log_info( '\n' + tabulate(self.account.history, headers=('date', 'code', 'price', 'towards', 'amounts', 'order_id', 'trade_id', 'commission'))) self.__QA_backtest_log_info('\n' + tabulate( self.account.detail, headers=(self.account.detail.columns))) __exist_time = int(self.end_real_id) - int(self.start_real_id) + 1 if len(self.__messages) > 1: performace = QA_backtest_analysis_backtest( self.setting.client, self.strategy_stock_list, self.account_d_value, self.account_d_key, self.__messages, self.trade_list[self.start_real_id:self.end_real_id + 1], self.benchmark_data.data) _backtest_mes = { 'user': self.setting.QA_setting_user_name, 'strategy': self.strategy_name, 'stock_list': performace['code'], 'start_time': self.strategy_start_date, 'end_time': self.strategy_end_date, 'account_cookie': self.account.account_cookie, 'annualized_returns': performace['annualized_returns'], 'benchmark_annualized_returns': performace['benchmark_annualized_returns'], 'assets': performace['assets'], 'benchmark_assets': performace['benchmark_assets'], 'trade_date': performace['trade_date'], 'total_date': performace['total_date'], 'win_rate': performace['win_rate'], 'alpha': performace['alpha'], 'beta': performace['beta'], 'sharpe': performace['sharpe'], 'vol': performace['vol'], 'benchmark_vol': performace['benchmark_vol'], 'max_drop': performace['max_drop'], 'exist': __exist_time, 'time': datetime.datetime.now() } if self.if_save_to_mongo: QA_SU_save_backtest_message(_backtest_mes, self.setting.client) QA_SU_save_account_message(self.__messages, self.setting.client) if self.if_save_to_csv: QA_SU_save_account_to_csv(self.__messages, self.dirs) self.account.detail.to_csv('{}backtest-pnl-{}.csv'.format( self.dirs, str(self.account.account_cookie))) self.__save_strategy_files() def __check_state(self, bid_price, bid_amount): pass def QA_Backtest_before_init(self): return self.__QA_backtest_init() def QA_Backtest_after_init(self): return self.__QA_backtest_prepare() @lru_cache() def find_bar(self, code, time): if isinstance(time, str): if len(time) == 10: try: try: return self.market_data_hashable[( datetime.datetime.strptime(time, '%Y-%m-%d'), code)] except: return self.outside_data_hashable[( datetime.datetime.strptime(time, '%Y-%m-%d'), code)] except: return None elif len(time) == 19: try: try: return self.market_data_hashable[( datetime.datetime.strptime(time, '%Y-%m-%d %H:%M:%S'), code)] except: return self.outside_data_hashable[( datetime.datetime.strptime(time, '%Y-%m-%d %H:%M:%S'), code)] except: return None else: try: try: return self.market_data_hashable[(time, code)] except: return self.outside_data_hashable[(time, code)] except: return None @lru_cache() def get_market_data(self, code, date, gap_=None, type_='lt'): '这个函数封装了关于获取的方式 用GAP的模式' gap_ = self.strategy_gap if gap_ is None else gap_ try: try: return self.market_data_dict[code].select_time_with_gap( date, gap_, type_) except: return self.outside_data_dict[code].select_time_with_gap( date, gap_, type_) except: return None @lru_cache() def get_market_data_panel(self, date=None, type_='lt'): try: if date is not None: try: return self.market_data.select_time_with_gap( date, 1, type_) except: return self.outside_data.select_time_with_gap( date, 1, type_) else: try: return self.market_data.select_time_with_gap( self.now, 1, type_) except: return self.outside_data.select_time_with_gap( self.now, 1, type_) except Exception as e: raise e @lru_cache() def get_market_data_bar(self, code, time, if_trade=True): '这个函数封装了关于获取的方式' try: try: return self.market_data_dict[code].get_bar( code, time, if_trade) except: return self.outside_data_dict[code].get_bar( code, time, if_trade) except: return None def get_block(self, block_list): block_ = QA_fetch_stock_block_adv() _data = [] try: for item in block_list: _data.extend(block_.get_block(item).code) return np.unique(_data).tolist() except Exception as e: raise e #@lru_cache() def QA_backtest_sell_available(self, __code): try: return self.account.sell_available[__code] except: return 0 # @lru_cache() def QA_backtest_hold(self): return pd.DataFrame(self.account.hold[1::], columns=self.account.hold[0]).set_index('code', drop=False) def hold_amount(self, __code): try: return pd.DataFrame( self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum()[__code] except: return 0 def hold_price(self, __code): try: return self.QA_backtest_hold(self)['price'].groupby( 'code').mean()[__code] except: return None @lru_cache() def get_OHLCV(self, __data): '快速返回 OHLCV格式' return (__data.open, __data.high, __data.low, __data.close, __data.vol) def send_order(self, code, amount, towards, order_type): """ 2017/8/4 委托函数 在外部封装的一个报价接口,尽量满足和实盘一样的模式 输入 ============= 买入/卖出 股票代码 买入/卖出数量 委托模式* 0 限价委托 LIMIT ORDER 1 市价委托 MARKET ORDER 2 严格模式(买入按最高价 卖出按最低价) STRICT ORDER 功能 ============= 1. 封装一个bid类(分配地址) 2. 检查账户/临时扣费 3. 检查市场(wrap) 4. 发送到_send_bid方法 """ # 必须是100股的倍数 # 封装bid _bid = QA_QAMarket_bid() # init (_bid.order_id, _bid.user, _bid.strategy, _bid.code, _bid.date, _bid.datetime, _bid.sending_time, _bid.amount, _bid.towards) = (str(random.random()), self.setting.QA_setting_user_name, self.strategy_name, code, self.running_date, str(self.now), self.running_date, amount, towards) # 2017-09-21 修改: 只有股票的交易才需要控制amount的最小交易单位 if self.backtest_type in ['day']: _bid.type = '0x01' _bid.amount = int(_bid.amount / 100) * 100 elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: _bid.type = '0x02' _bid.amount = int(_bid.amount / 100) * 100 elif self.backtest_type in ['index_day']: _bid.type = '0x03' _bid.amount = int(_bid.amount) elif self.backtest_type in [ 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min' ]: _bid.type = '0x04' _bid.amount = int(_bid.amount) # 检查账户/临时扣费 _bid, _market = self.__wrap_bid(_bid, order_type) if _bid is not None and _market is not None and _bid.amount > 0: print( 'GET the Order Code %s Amount %s Price %s Towards %s Time %s' % (_bid.code, _bid.amount, _bid.price, _bid.towards, _bid.datetime)) self.__sync_order_LM('create_order', order_=_bid) @lru_cache() def check_order(self, order_id_): '用于检查委托单的状态' """ 委托单被报入交易所会有一个回报,回报状态就是交易所返回的字段: 字段目前 2xx 是成功 4xx是失败 5xx是交易所无数据(停牌) 随着回测框架的不断升级,会有更多状态需要被管理: 200 委托成功,完全交易 203 委托成功,未完全成功 300 刚创建订单的时候 400 已撤单 500 服务器撤单/每日结算 """ return self.account.order_queue[self.account.order_queue['order_id'] == order_id_]['status'] @lru_cache() def status(self): return vars(self) @lru_cache() def sell_all(self): __hold_list = pd.DataFrame( self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() for item in self.strategy_stock_list: try: if __hold_list[item] > 0: self.send_order(self, item, __hold_list[item], -1, {'bid_model': 'C'}) except: pass def _load_strategy(self, *arg, **kwargs): '策略加载函数' # 首先判断是否能满足回测的要求` __messages = {} self.__init_cash_per_stock = int( float(self.account.init_assest) / len(self.strategy_stock_list)) # 策略的交易日循环 for i in range(int(self.start_real_id), int(self.end_real_id)): self.running_date = self.trade_list[i] self.__QA_backtest_log_info( '=================daily hold list====================') self.__QA_backtest_log_info('in the begining of ' + self.running_date) self.__QA_backtest_log_info( tabulate(self.account.message['body']['account']['hold'])) if self.now is not None: self.last_time = self.now self.now = self.running_date self.today = self.running_date # 交易前同步持仓状态 self.__sync_order_LM(self, 'init_') # 初始化事件 if self.backtest_type in ['day', 'd', 'index_day']: _temp = self.market_data.select_time( self.today, self.today).data.set_index('code').close.to_dict() for key in _temp.keys(): self.lastest_price[key] = _temp[key] self.strategy(*arg, **kwargs) # 发委托单 self._deal_from_order_queue() elif self.backtest_type in [ '1min', '5min', '15min', '30min', '60min', 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min' ]: if self.backtest_type in ['1min', 'index_1min']: type_ = '1min' elif self.backtest_type in ['5min', 'index_5min']: type_ = '5min' elif self.backtest_type in ['15min', 'index_15min']: type_ = '15min' elif self.backtest_type in ['30min', 'index_30min']: type_ = '30min' elif self.backtest_type in ['60min', 'index_60min']: type_ = '60min' daily_min = QA_util_make_min_index(self.today, type_) # 创造分钟线index for min_index in daily_min: self.now = min_index self.__QA_backtest_log_info( '=================Min hold list====================') self.__QA_backtest_log_info('in the begining of %s' % str(min_index)) self.__QA_backtest_log_info( tabulate( self.account.message['body']['account']['hold'])) _temp = self.market_data.select_time( self.now, self.now).data.set_index('code').close.to_dict() for key in _temp.keys(): self.lastest_price[key] = _temp[key] self.strategy(*arg, **kwargs) # 发委托单 self._deal_from_order_queue() if self.backtest_type in [ 'index_1min', 'index_5min', 'index_15min' ]: self.__sync_order_LM('t_0') self.__sync_order_LM('daily_settle') # 每日结算 # 最后一天 self._end_of_trading() def _backtest_init(self): self.__QA_backtest_init() self.backtest_init() self.__QA_backtest_prepare() def _before_backtest(self): self.__QA_backtest_before_backtest() self.before_backtest() def _end_backtest(self): self.end_backtest() self.__end_of_backtest() def run(self): self._backtest_init() self._before_backtest() self._load_strategy() self._end_backtest() # 暂时不确定要不要用 def strategy(self): pass def backtest_init(self): pass def before_backtest(self): pass def end_backtest(self): pass
class QA_Backtest_stock_day(): account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name market_data = [] def __init__(self): self.account = QA_Account() self.market = QA_Market() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name @classmethod def backtest_init(__backtest_cls, func, *arg, **kwargs): def __init_backtest(__backtest_cls, *arg, **kwargs): __backtest_cls.QA_backtest_init(__backtest_cls) func(*arg, **kwargs) __backtest_cls.__QA_backtest_init_inside(__backtest_cls) return __init_backtest(__backtest_cls) @classmethod def before_backtest(__backtest_cls, func, *arg, **kwargs): # yield __backtest_cls.cash func(*arg, **kwargs) __backtest_cls.QA_backtest_start(__backtest_cls) @classmethod def end_backtest(__backtest_cls, func, *arg, **kwargs): # yield __backtest_cls.cash __backtest_cls.__end_of_backtest(__backtest_cls, func, *arg, **kwargs) return func(*arg, **kwargs) def __QA_backtest_set_bid_model(self): if self.backtest_bid_model == 'market_price': self.bid.bid['price'] = 'market_price' self.bid.bid['bid_model'] = 'auto' elif self.backtest_bid_model == 'close_price': self.bid.bid['price'] = 'close_price' self.bid.bid['bid_model'] = 'auto' elif self.backtest_bid_model == 'strategy': self.bid.bid['price'] = 0 self.bid.bid['bid_model'] = 'strategy' else: QA_util_log_info('support bid model') sys.exit() def __QA_backtest_set_save_model(self): pass def QA_backtest_init(self): """既然是被当做装饰器使用,就需要把变量设置放在装饰函数的前面,把函数放在装饰函数的后面""" # 设置回测的开始结束时间 self.strategy_start_date = str('2017-01-05') self.strategy_end_date = str('2017-07-01') # 设置回测标的,是一个list对象,不过建议只用一个标的 # gap是回测时,每日获取数据的前推日期(交易日) self.strategy_gap = int(60) # 设置全局的数据库地址,回测用户名,密码,并初始化 self.setting.QA_util_sql_mongo_ip = str('127.0.0.1') self.setting.QA_setting_user_name = str('admin') self.setting.QA_setting_user_password = str('admin') self.setting.QA_setting_init() # 回测的名字 self.strategy_name = str('example') # 股票的交易日历,真实回测的交易周期,和交易周期在交易日历中的id self.trade_list = QA_fetch_trade_date( self.setting.client.quantaxis.trade_date) self.benchmark_code = 'hs300' """ 这里会涉及一个区间的问题,开始时间是要向后推,而结束时间是要向前推,1代表向后推,-1代表向前推 """ self.start_real_date = QA_util_get_real_date(self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date(self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) self.strategy_stock_list = ['000001', '000002', '000004'] self.account.init_assest = 1000000 self.backtest_bid_model = 'market_price' def __QA_backtest_init_inside(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.setting.QA_setting_init() self.account.init() # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 self.market_data = QA_fetch_stocklist_day( self.strategy_stock_list, self.setting.client.quantaxis.stock_day, [ self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id] ]) def QA_backtest_start(self, *args, **kwargs): """ 这个是回测流程开始的入口 """ assert len(self.strategy_stock_list) > 0 assert len(self.trade_list) > 0 assert isinstance(self.start_real_date, str) assert isinstance(self.end_real_date, str) #self.__QA_backtest_init_inside() assert len(self.market_data) == len(self.strategy_stock_list) QA_util_log_info('QUANTAXIS Backtest Engine Initial Successfully') QA_util_log_info('Basical Info: \n' + tabulate( [[str(__version__), str(self.strategy_name)]], headers=('Version', 'Strategy_name'))) QA_util_log_info('Stock_List: \n' + tabulate([self.strategy_stock_list])) # 初始化报价模式 self.__QA_backtest_set_bid_model(self) self.__messages = [] """ try: # 在末尾增加一个回调给策略 outside_handle.on_start(self) except: pass # 加载外部策略 self.__QA_backest_handle_data(outside_handle) """ @classmethod def load_strategy(__backtest_cls, func, *arg, **kwargs): '策略加载函数' # 首先判断是否能满足回测的要求` _info = {} _info['stock_list'] = __backtest_cls.strategy_stock_list __messages = {} __backtest_cls.__init_cash_per_stock = int( float(__backtest_cls.account.init_assest) / len(__backtest_cls.strategy_stock_list)) # 策略的交易日循环 for i in range(int(__backtest_cls.start_real_id), int(__backtest_cls.end_real_id) - 1, 1): __backtest_cls.running_date = __backtest_cls.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __backtest_cls.running_date) QA_util_log_info( tabulate( __backtest_cls.account.message['body']['account']['hold'])) func(*arg, **kwargs) #最后一天 __backtest_cls.__end_of_trading(__backtest_cls) def __start_of_every_day(self, func, *arg, **kwargs): func(self, *arg, **kwargs) def __end_of_every_day(self, func, *arg, **kwargs): func(self, *arg, **kwargs) def __backtest_every_day_trading(self, i, func, *arg, **kwargs): # 正在进行的交易日期 __running_date = self.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __running_date) QA_util_log_info( tabulate(self.account.message['body']['account']['hold'])) for __j in range(0, len(self.strategy_stock_list)): if __running_date in [l[6] for l in self.market_data[__j]] and \ [l[6] for l in self.market_data[__j]].index(__running_date) \ > self.strategy_gap + 1: __data = self.__QA_data_handle([ __l[6] for __l in self.market_data[__j] ].index(__running_date), __j) __amount = 0 for item in __data['account']['body']['account']['hold']: if self.strategy_stock_list[__j] in item: __amount = __amount + item[3] if __amount > 0: __hold = 1 else: __hold = 0 __result = func(self, *arg, **kwargs) if float(self.account.message['body']['account']['cash'] [-1]) > 0: self.QA_backtest_excute_bid( __result, __running_date, __hold, str(self.strategy_stock_list[__j])[0:6], __amount) else: QA_util_log_info('not enough free money') else: pass def __end_of_trading(self, *arg, **kwargs): # 在回测的最后一天,平掉所有仓位(回测的最后一天是不买入的) while len(self.account.hold) > 1: __hold_list = self.account.hold[1::] pre_del_id = [] for item_ in range(0, len(__hold_list)): if __hold_list[item_][3] > 0: __last_bid = self.bid.bid __last_bid['amount'] = int(__hold_list[item_][3]) __last_bid['order_id'] = str(random.random()) __last_bid['price'] = 'close_price' __last_bid['code'] = str(__hold_list[item_][1]) __last_bid['date'] = self.trade_list[self.end_real_id] __last_bid['towards'] = -1 __last_bid['user'] = self.setting.QA_setting_user_name __last_bid['strategy'] = self.strategy_name __last_bid['bid_model'] = 'auto' __last_bid['status'] = '0x01' __last_bid['amount_model'] = 'amount' __message = self.market.receive_bid( __last_bid, self.setting.client) _remains_day = 0 while __message['header']['status'] == 500: # 停牌状态,这个时候按停牌的最后一天计算价值(假设平仓) __last_bid['date'] = self.trade_list[self.end_real_id - _remains_day] _remains_day += 1 __message = self.market.receive_bid( __last_bid, self.setting.client) # 直到市场不是为0状态位置,停止前推日期 self.__messages = self.account.QA_account_receive_deal( __message) else: pre_del_id.append(item_) pre_del_id.sort() pre_del_id.reverse() for item_x in pre_del_id: __hold_list.pop(item_x) def __end_of_backtest(self, *arg, **kwargs): # 开始分析 QA_util_log_info('start analysis====\n' + str(self.strategy_stock_list)) QA_util_log_info('=' * 10 + 'Trade History' + '=' * 10) QA_util_log_info('\n' + tabulate(self.account.history, headers=('date', 'code', 'price', 'towards', 'amounts', 'order_id', 'trade_id', 'commission'))) QA_util_log_info( tabulate(self.account.detail, headers=('date', 'code', 'price', 'amounts', 'order_id', 'trade_id', 'sell_price', 'sell_order_id', 'sell_trade_id', 'sell_date', 'left_amount', 'commission'))) __exist_time = int(self.end_real_id) - int(self.start_real_id) + 1 self.__benchmark_data = QA_fetch_index_day(self.benchmark_code, self.start_real_date, self.end_real_date) if len(self.__messages) > 1: performace = QA_backtest_analysis_start( self.setting.client, self.strategy_stock_list, self.__messages, self.trade_list[self.start_real_id:self.end_real_id + 1], self.market_data, self.__benchmark_data) _backtest_mes = { 'user': self.setting.QA_setting_user_name, 'strategy': self.strategy_name, 'stock_list': performace['code'], 'start_time': self.strategy_start_date, 'end_time': self.strategy_end_date, 'account_cookie': self.account.account_cookie, 'annualized_returns': performace['annualized_returns'], 'benchmark_annualized_returns': performace['benchmark_annualized_returns'], 'assets': performace['assets'], 'benchmark_assets': performace['benchmark_assets'], 'trade_date': performace['trade_date'], 'total_date': performace['total_date'], 'win_rate': performace['win_rate'], 'alpha': performace['alpha'], 'beta': performace['beta'], 'sharpe': performace['sharpe'], 'vol': performace['vol'], 'benchmark_vol': performace['benchmark_vol'], 'max_drop': performace['max_drop'], 'exist': __exist_time, 'time': datetime.datetime.now() } QA_SU_save_backtest_message(_backtest_mes, self.setting.client) QA_SU_save_account_message(self.__messages, self.setting.client) QA_SU_save_account_to_csv(self.__messages) # QA.QA_SU_save_backtest_message(analysis_message, self.setting.client) def QA_backtest_excute_bid(self, __result, __date, __hold, __code, __amount): """ 这里是处理报价的逻辑部分 2017/7/19 修改 """ """ 2017/8/4再次修改: 我们需要的是一个简化的报单api 买入/卖出 股票代码 买入/卖出数量 参考实盘的下单接口: tradex: nCategory - 委托业务的种类 0 买入 1 卖出 2 融资买入 3 融券卖出 4 买券还券 5 卖券还款 6 现券还券 nOrderType - 委托报价方式 0 限价委托; 上海限价委托/ 深圳限价委托 1 市价委托(深圳对方最优价格) 2 市价委托(深圳本方最优价格) 3 市价委托(深圳即时成交剩余撤销) 4 市价委托(上海五档即成剩撤/ 深圳五档即成剩撤) 5 市价委托(深圳全额成交或撤销) 6 市价委托(上海五档即成转限价) sAccount - 股东代码 sStockCode - 证券代码 sPrice - 价格 sVolume - 委托证券的股数 返回值: errinfo - 出错时函数抛出的异常信息; result - 查询到的数据。 然后去检查 1.当前账户是否持仓该股票 2.当前账户现金是否足以支持购买该股票 返回 3.是否委托(委托id) 4.是否成交(成交id) 5.成交笔数 """ self.__QA_backtest_set_bid_model() if self.bid.bid['bid_model'] == 'strategy': __bid_price = __result['price'] else: __bid_price = self.bid.bid['price'] __bid = self.bid.bid __bid['order_id'] = str(random.random()) __bid['user'] = self.setting.QA_setting_user_name __bid['strategy'] = self.strategy_name __bid['code'] = __code __bid['date'] = __date __bid['price'] = __bid_price __bid['amount'], __bid['amount_model'] = self.__QA_bid_amount( __result['amount'], __amount) if __result['if_buy'] == 1: # 这是买入的情况 __bid['towards'] = 1 __message = self.market.receive_bid(__bid, self.setting.client) if float(self.account.message['body']['account']['cash'][-1]) > \ float(__message['body']['bid']['price']) * \ float(__message['body']['bid']['amount']): # 这里是买入资金充足的情况 # 不去考虑 pass else: # 如果买入资金不充足,则按照可用资金去买入 __message['body']['bid']['amount'] = int( float(self.account.message['body']['account']['cash'][-1]) / float( float(str(__message['body']['bid']['price'])[0:5]) * 100)) * 100 if __message['body']['bid']['amount'] > 0: # 这个判断是为了 如果买入资金不充足,所以买入报了一个0量单的情况 #如果买入量>0, 才判断为成功交易 self.account.QA_account_receive_deal(__message) elif __result['if_buy'] == 0: # 如果买入状态为0,则不进行任何买入操作 pass # 下面是卖出操作,这里在卖出前需要考虑一个是否有仓位的问题: # 因为在股票中是不允许卖空操作的,所以这里是股票的交易引擎和期货的交易引擎的不同所在 if __result['if_sell'] == 1 and __hold == 1: __bid['towards'] = -1 __message = self.market.receive_bid(__bid, self.setting.client) self.account.QA_account_receive_deal(__message) def __QA_bid_amount(self, __strategy_amount, __amount): if __strategy_amount == 'mean': return float( float(self.account.message['body']['account']['cash'][-1]) / len(self.strategy_stock_list)), 'price' elif __strategy_amount == 'half': return __amount * 0.5, 'amount' elif __strategy_amount == 'all': return __amount, 'amount' def __QA_get_data_from_market(self, __id, stock_id): # x=[x[6] for x in self.market_data] if __id > self.strategy_gap + 1: index_of_day = __id index_of_start = index_of_day - self.strategy_gap + 1 return self.market_data[stock_id][index_of_start:index_of_day + 1] # 从账户中更新数据 def __QA_data_handle(self, __id, __stock_id): market_data = self.__QA_get_data_from_market(__id, __stock_id) __message = self.account.message return {'market': market_data, 'account': __message}
class QA_Backtest(): '最终目的还是实现一个通用的回测类' backtest_type = 'day' account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() order = QA_QAMarket_bid_list() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name market_data = [] now = '' today = '' strategy_stock_list = [] trade_list = [] start_real_id = 0 end_real_id = 0 temp = {} commission_fee_coeff = 0.0015 def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.order = QA_QAMarket_bid_list() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = '' self.today = '' self.strategy_stock_list = [] self.trade_list = [] self.start_real_id = 0 self.end_real_id = 0 self.temp = {} self.commission_fee_coeff = 0.0015 def __QA_backtest_init(self): """既然是被当做装饰器使用,就需要把变量设置放在装饰函数的前面,把函数放在装饰函数的后面""" # 设置回测的开始结束时间 self.strategy_start_date = str('2017-01-05') self.strategy_end_date = str('2017-07-01') # 设置回测标的,是一个list对象,不过建议只用一个标的 # gap是回测时,每日获取数据的前推日期(交易日) self.strategy_gap = int(60) # 设置全局的数据库地址,回测用户名,密码,并初始化 self.setting.QA_util_sql_mongo_ip = str('127.0.0.1') self.setting.QA_setting_user_name = str('admin') self.setting.QA_setting_user_password = str('admin') self.setting.QA_setting_init() # 回测的名字 self.strategy_name = str('example_min') # 股票的交易日历,真实回测的交易周期,和交易周期在交易日历中的id self.trade_list = trade_date_sse self.benchmark_code = '000300' """ 这里会涉及一个区间的问题,开始时间是要向后推,而结束时间是要向前推,1代表向后推,-1代表向前推 """ self.strategy_stock_list = ['000001', '000002', '000004'] self.account.init_assest = 1000000 self.backtest_bid_model = 'market_price' self.commission_fee_coeff = 0.0015 def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.market = QA_Market(self.commission_fee_coeff) self.setting.QA_setting_init() self.account.init() self.start_real_date = QA_util_get_real_date( self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date( self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 self.benchmark_data = QA_fetch_index_day_adv( self.benchmark_code, self.start_real_date, self.end_real_date) if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day_adv( self.strategy_stock_list, self.trade_list[self.start_real_id - int( self.strategy_gap)], self.trade_list[self.end_real_id]).to_qfq() elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: self.market_data = QA_fetch_stocklist_min_adv( self.strategy_stock_list, self.trade_list[ self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id + 1], self.backtest_type).to_qfq() elif self.backtest_type in ['index_day']: self.market_data = QA_fetch_index_day_adv(self.strategy_stock_list, self.trade_list[self.start_real_id - int( self.strategy_gap)], self.trade_list[self.end_real_id]) elif self.backtest_type in ['index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min']: self.market_data = QA_fetch_index_min_adv( self.strategy_stock_list, self.start_real_date, self.end_real_date, self.backtest_type.split('_')[1]) def __QA_backtest_start(self, *args, **kwargs): """ 这个是回测流程开始的入口 """ QA_util_log_info('QUANTAXIS Backtest Engine Initial Successfully') QA_util_log_info('Basical Info: \n' + tabulate( [[str(__version__), str(self.strategy_name)]], headers=('Version', 'Strategy_name'))) QA_util_log_info('BACKTEST Cookie_ID is: ' + str(self.account.account_cookie)) QA_util_log_info('Stock_List: \n' + tabulate([self.strategy_stock_list])) # 初始化报价模式 self.__messages = [] def __check_state(self, bid_price, bid_amount): pass def __QA_bid_amount(self, __strategy_amount, __amount): if __strategy_amount == 'mean': return float(float(self.account.message['body']['account']['cash'][-1]) / len(self.strategy_stock_list)), 'price' elif __strategy_amount == 'half': return __amount * 0.5, 'amount' elif __strategy_amount == 'all': return __amount, 'amount' def __end_of_trading(self, *arg, **kwargs): # 在回测的最后一天,平掉所有仓位(回测的最后一天是不买入的) # 回测最后一天的交易处理 self.now = str(self.end_real_date) + ' 15:00:00' self.today = self.end_real_date self.QA_backtest_sell_all(self) self.__sell_from_order_queue(self) self.__sync_order_LM(self, 'daily_settle') # 每日结算 def __wrap_bid(self, __bid, __order=None): __market_data_for_backtest = self.market_data.get_bar( __bid.code, __bid.datetime) if __market_data_for_backtest.len() == 1: __O, __H, __L, __C, __V = self.QA_backtest_get_OHLCV( self, __market_data_for_backtest) if __O is not None and __order is not None: if __order['bid_model'] in ['limit', 'Limit', 'Limited', 'limited', 'l', 'L', 0, '0']: # 限价委托模式 __bid.price = __order['price'] elif __order['bid_model'] in ['Market', 'market', 'MARKET', 'm', 'M', 1, '1']: # 2017-09-18 修改 市价单以当前bar开盘价下单 __bid.price = float(__O[0]) elif __order['bid_model'] in ['strict', 'Strict', 's', 'S', '2', 2]: __bid.price = float( __H[0]) if __bid.towards == 1 else float(__L[0]) elif __order['bid_model'] in ['close', 'close_price', 'c', 'C', '3', 3]: __bid.price = float(__C[0]) __bid.price = float('%.2f' % __bid.price) return __bid, __market_data_for_backtest else: return __bid, __market_data_for_backtest else: QA_util_log_info('BACKTEST ENGINE ERROR=== CODE %s TIME %s NO MARKET DATA!' % ( __bid.code, __bid.datetime)) return __bid, 500 def __end_of_backtest(self, *arg, **kwargs): # 开始分析 # 对于account.detail做一定的整理 self.account.detail = detail = pd.DataFrame(self.account.detail, columns=['date', 'code', 'price', 'amounts', 'order_id', 'trade_id', 'sell_price', 'sell_order_id', 'sell_trade_id', 'sell_date', 'left_amount', 'commission']) self.account.detail['sell_average'] = self.account.detail['sell_price'].apply( lambda x: mean(x)) self.account.detail['pnl_persentage'] = self.account.detail['sell_average'] - \ self.account.detail['price'] self.account.detail['pnl'] = self.account.detail['pnl_persentage'] * ( self.account.detail['amounts'] - self.account.detail['left_amount']) - self.account.detail['commission'] self.account.detail = self.account.detail.drop( ['order_id', 'trade_id', 'sell_order_id', 'sell_trade_id'], axis=1) QA_util_log_info('start analysis====\n' + str(self.strategy_stock_list)) QA_util_log_info('=' * 10 + 'Trade History' + '=' * 10) QA_util_log_info('\n' + tabulate(self.account.history, headers=('date', 'code', 'price', 'towards', 'amounts', 'order_id', 'trade_id', 'commission'))) QA_util_log_info('\n' + tabulate(self.account.detail, headers=(self.account.detail.columns))) __exist_time = int(self.end_real_id) - int(self.start_real_id) + 1 if len(self.__messages) > 1: performace = QA_backtest_analysis_start( self.setting.client, self.strategy_stock_list, self.__messages, self.trade_list[self.start_real_id:self.end_real_id + 1], self.benchmark_data.data) _backtest_mes = { 'user': self.setting.QA_setting_user_name, 'strategy': self.strategy_name, 'stock_list': performace['code'], 'start_time': self.strategy_start_date, 'end_time': self.strategy_end_date, 'account_cookie': self.account.account_cookie, 'annualized_returns': performace['annualized_returns'], 'benchmark_annualized_returns': performace['benchmark_annualized_returns'], 'assets': performace['assets'], 'benchmark_assets': performace['benchmark_assets'], 'trade_date': performace['trade_date'], 'total_date': performace['total_date'], 'win_rate': performace['win_rate'], 'alpha': performace['alpha'], 'beta': performace['beta'], 'sharpe': performace['sharpe'], 'vol': performace['vol'], 'benchmark_vol': performace['benchmark_vol'], 'max_drop': performace['max_drop'], 'exist': __exist_time, 'time': datetime.datetime.now() } QA_SU_save_backtest_message(_backtest_mes, self.setting.client) QA_SU_save_account_message(self.__messages, self.setting.client) QA_SU_save_account_to_csv(self.__messages) self.account.detail.to_csv( 'backtest-pnl--' + str(self.account.account_cookie) + '.csv') def QA_backtest_get_market_data(self, code, date, gap_=None): '这个函数封装了关于获取的方式 用GAP的模式' gap_ = self.strategy_gap if gap_ is None else gap_ return self.market_data.select_code(code).select_time_with_gap(date, gap_, 'lte') def QA_backtest_get_market_data_bar(self, code, time): '这个函数封装了关于获取的方式' return self.market_data.get_bar(code, time) def QA_backtest_sell_available(self, __code): try: return self.account.sell_available[__code] except: return 0 def QA_backtest_hold_amount(self, __code): try: return pd.DataFrame(self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum()[__code] except: return 0 def __sell_from_order_queue(self): # 每个bar结束的时候,批量交易 __result = [] self.order.__init__() if len(self.account.order_queue) >= 1: __bid_list = self.order.from_dataframe(self.account.order_queue.query( 'status!=200').query('status!=500').query('status!=400')) for item in __bid_list: # 在发单的时候要改变交易日期 item.date = self.today item.datetime = self.now __bid, __market = self.__wrap_bid(self, item) __message = self.__QA_backtest_send_bid( self, __bid, __market.to_json()[0]) if isinstance(__message, dict): if __message['header']['status'] in ['200', 200]: self.__sync_order_LM( self, 'trade', __bid, __message['header']['order_id'], __message['header']['trade_id'], __message) else: self.__sync_order_LM(self, 'wait') else: QA_util_log_info( 'FROM BACKTEST: Order Queue is empty at %s!' % self.now) pass def QA_backtest_get_OHLCV(self, __data): '快速返回 OHLCV格式' return (__data.open, __data.high, __data.low, __data.close, __data.vol) def QA_backtest_send_order(self, __code, __amount, __towards, __order): """ 2017/8/4 委托函数 在外部封装的一个报价接口,尽量满足和实盘一样的模式 输入 ============= 买入/卖出 股票代码 买入/卖出数量 委托模式* 0 限价委托 LIMIT ORDER 1 市价委托 MARKET ORDER 2 严格模式(买入按最高价 卖出按最低价) STRICT ORDER 功能 ============= 1. 封装一个bid类(分配地址) 2. 检查账户/临时扣费 3. 检查市场(wrap) 4. 发送到_send_bid方法 """ # 必须是100股的倍数 # 封装bid __bid = QA_QAMarket_bid() # init (__bid.order_id, __bid.user, __bid.strategy, __bid.code, __bid.date, __bid.datetime, __bid.sending_time, __bid.amount, __bid.towards) = (str(random.random()), self.setting.QA_setting_user_name, self.strategy_name, __code, self.running_date, str( self.now), self.running_date, __amount, __towards) # 2017-09-21 修改: 只有股票的交易才需要控制amount的最小交易单位 if self.backtest_type in ['day']: __bid.type = '0x01' __bid.amount = int(__bid.amount / 100) * 100 elif self.backtest_type in ['1min', '5min', '15min', '30min', '60min']: __bid.type = '0x02' __bid.amount = int(__bid.amount / 100) * 100 elif self.backtest_type in ['index_day']: __bid.type = '0x03' __bid.amount = int(__bid.amount) elif self.backtest_type in ['index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min']: __bid.type = '0x04' __bid.amount = int(__bid.amount) # 检查账户/临时扣费 __bid, __market = self.__wrap_bid(self, __bid, __order) if __bid is not None and __market != 500: print('GET the Order Code %s Amount %s Price %s Towards %s Time %s' % ( __bid.code, __bid.amount, __bid.price, __bid.towards, __bid.datetime)) self.__sync_order_LM(self, 'create_order', order_=__bid) def __sync_order_LM(self, event_, order_=None, order_id_=None, trade_id_=None, market_message_=None): """ 订单事件: 生命周期管理 Order-Lifecycle-Management status1xx 订单待生成 status3xx 初始化订单 临时扣除资产(可用现金/可卖股份) status3xx 订单存活(等待交易) status2xx 订单完全交易/未完全交易 status4xx 主动撤单 status500 订单死亡(每日结算) 恢复临时资产 ======= 1. 更新持仓 2. 更新现金 """ if event_ is 'init_': self.account.cash_available = self.account.cash[-1] self.account.sell_available = pd.DataFrame(self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() elif event_ is 'create_order': if order_ is not None: if order_.towards is 1: # 买入 if self.account.cash_available - order_.amount * order_.price > 0: self.account.cash_available -= order_.amount * order_.price order_.status = 300 # 修改订单状态 self.account.order_queue = self.account.order_queue.append( order_.to_df()) else: QA_util_log_info('FROM ENGINE: NOT ENOUGH MONEY:CASH %s Order %s' % ( self.account.cash_available, order_.amount * order_.price)) elif order_.towards is -1: if self.QA_backtest_sell_available(self, order_.code) - order_.amount >= 0: self.account.sell_available[order_.code] -= order_.amount self.account.order_queue = self.account.order_queue.append( order_.to_df()) else: QA_util_log_info('Order Event Warning:%s in %s' % (event_, str(self.now))) elif event_ in ['wait', 'live']: # 订单存活 不会导致任何状态改变 pass elif event_ in ['cancel_order']: # 订单事件:主动撤单 # try: assert isinstance(order_id_, str) self.account.order_queue.loc[self.account.order_queue['order_id'] == order_id_, 'status'] = 400 # 注销事件 if order_.towards is 1: # 多单 撤单 现金增加 self.account.cash_available += self.account.order_queue.query('order_id=="order_id_"')[ 'amount'] * self.account.order_queue.query('order_id=="order_id_"')['price'] elif order_.towards is -1: # 空单撤单 可卖数量增加 self.account.sell_available[order_.code] += self.account.order_queue.query( 'order_id=="order_id_"')['price'] elif event_ in ['daily_settle']: # 每日结算/全撤/把成交的买入/卖出单标记为500 同时结转 # 买入 """ 每日结算流程 - 同步实际的现金和仓位 - 清空留仓单/未成功的订单 """ self.account.cash_available = self.account.cash[-1] self.account.sell_available = pd.DataFrame(self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() self.account.order_queue = pd.DataFrame() elif event_ in ['t_0']: """ T+0交易事件 同步t+0的账户状态 /允许卖出 """ self.account.cash_available = self.account.cash[-1] self.account.sell_available = pd.DataFrame(self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() elif event_ in ['trade']: # try: assert isinstance(order_, QA_QAMarket_bid) assert isinstance(order_id_, str) assert isinstance(trade_id_, str) assert isinstance(market_message_, dict) if order_.towards is 1: # 买入 # 减少现金 order_.trade_id = trade_id_ order_.transact_time = self.now order_.amount -= market_message_['body']['bid']['amount'] if order_.amount == 0: # 完全交易 # 注销(成功交易)['买入单不能立即结转'] self.account.order_queue.loc[self.account.order_queue['order_id'] == order_id_, 'status'] = 200 elif order_.amount > 0: # 注销(成功交易) self.account.order_queue.loc[self.account.order_queue['order_id'] == order_id_, 'status'] = 203 self.account.order_queue.query('order_id=="order_id_"')[ 'amount'] -= market_message_['body']['bid']['amount'] elif order_.towards is -1: # self.account.sell_available[order_.code] -= market_message_[ # 'body']['bid']['amount'] # 当日卖出的股票 可以继续买入/ 可用资金增加(要减去手续费) self.account.cash_available += market_message_['body']['bid']['amount'] * market_message_[ 'body']['bid']['price'] - market_message_['body']['fee']['commission'] order_.trade_id = trade_id_ order_.transact_time = self.now order_.amount -= market_message_['body']['bid']['amount'] if order_.amount == 0: # 注销(成功交易) self.account.order_queue.loc[self.account.order_queue['order_id'] == order_id_, 'status'] = 200 else: # 注销(成功交易) self.account.order_queue.loc[self.account.order_queue['order_id'] == order_id_, 'status'] = 203 self.account.order_queue[self.account.order_queue['order_id'] == order_id_]['amount'] -= market_message_['body']['bid']['amount'] else: QA_util_log_info( 'EventEngine Warning: Unknown type of order event in %s' % str(self.now)) def __QA_backtest_send_bid(self, __bid, __market=None): __message = self.market.receive_bid(__bid, __market) if __bid.towards == 1: # 扣费 # 以下这个订单时的bar的open扣费 # 先扔进去买入,再通过返回的值来判定是否成功 if __message['header']['status'] == 200 and __message['body']['bid']['amount'] > 0: # 这个判断是为了 如果买入资金不充足,所以买入报了一个0量单的情况 # 如果买入量>0, 才判断为成功交易 QA_util_log_info('BUY %s Price %s Date %s Amount %s' % ( __bid.code, __bid.price, __bid.datetime, __bid.amount)) self.__messages = self.account.QA_account_receive_deal( __message) return __message else: return __message # 下面是卖出操作,这里在卖出前需要考虑一个是否有仓位的问题:````````````` ` # 因为在股票中是不允许卖空操作的,所以这里是股票的交易引擎和期货的交易引擎的不同所在 elif __bid.towards == -1: # 如果是卖出操作 检查是否有持仓 # 股票中不允许有卖空操作 # 检查持仓面板 if __message['header']['status'] == 200: self.__messages = self.account.QA_account_receive_deal( __message) QA_util_log_info('SELL %s Price %s Date %s Amount %s' % ( __bid.code, __bid.price, __bid.datetime, __bid.amount)) return __message else: # self.account.order_queue=self.account.order_queue.append(__bid.to_df()) return __message else: return "Error: No buy/sell towards" def QA_backtest_check_order(self, order_id_): '用于检查委托单的状态' """ 委托单被报入交易所会有一个回报,回报状态就是交易所返回的字段: 字段目前 2xx 是成功 4xx是失败 5xx是交易所无数据(停牌) 随着回测框架的不断升级,会有更多状态需要被管理: 200 委托成功,完全交易 203 委托成功,未完全成功 300 刚创建订单的时候 400 已撤单 500 服务器撤单/每日结算 """ return self.account.order_queue[self.account.order_queue['order_id'] == order_id_]['status'] def QA_backtest_status(self): return vars(self) def QA_backtest_sell_all(self): __hold_list = pd.DataFrame(self.account.hold[1::], columns=self.account.hold[0]).set_index( 'code', drop=False)['amount'].groupby('code').sum() for item in self.strategy_stock_list: try: if __hold_list[item] > 0: self.QA_backtest_send_order( self, item, __hold_list[item], -1, {'bid_model': 'C'}) except: pass @classmethod def load_strategy(__backtest_cls, func, *arg, **kwargs): '策略加载函数' # 首先判断是否能满足回测的要求` __messages = {} __backtest_cls.__init_cash_per_stock = int( float(__backtest_cls.account.init_assest) / len(__backtest_cls.strategy_stock_list)) # 策略的交易日循环 for i in range(int(__backtest_cls.start_real_id), int(__backtest_cls.end_real_id) - 1, 1): __backtest_cls.running_date = __backtest_cls.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __backtest_cls.running_date) QA_util_log_info( tabulate(__backtest_cls.account.message['body']['account']['hold'])) __backtest_cls.now = __backtest_cls.running_date __backtest_cls.today = __backtest_cls.running_date # 交易前同步持仓状态 __backtest_cls.__sync_order_LM(__backtest_cls, 'init_') # 初始化事件 if __backtest_cls.backtest_type in ['day', 'd', 'index_day']: func(*arg, **kwargs) # 发委托单 __backtest_cls.__sell_from_order_queue(__backtest_cls) elif __backtest_cls.backtest_type in ['1min', '5min', '15min', '30min', '60min', 'index_1min', 'index_5min', 'index_15min', 'index_30min', 'index_60min']: if __backtest_cls.backtest_type in ['1min', 'index_1min']: type_ = '1min' elif __backtest_cls.backtest_type in ['5min', 'index_5min']: type_ = '5min' elif __backtest_cls.backtest_type in ['15min', 'index_15min']: type_ = '15min' elif __backtest_cls.backtest_type in ['30min', 'index_30min']: type_ = '30min' elif __backtest_cls.backtest_type in ['60min', 'index_60min']: type_ = '60min' daily_min = QA_util_make_min_index( __backtest_cls.today, type_) # 创造分钟线index # print(daily_min) for min_index in daily_min: __backtest_cls.now = min_index QA_util_log_info( '=================Min hold list====================') QA_util_log_info('in the begining of %s' % str(min_index)) QA_util_log_info( tabulate(__backtest_cls.account.message['body']['account']['hold'])) func(*arg, **kwargs) # 发委托单 __backtest_cls.__sell_from_order_queue(__backtest_cls) if __backtest_cls.backtest_type in ['index_1min', 'index_5min', 'index_15min']: __backtest_cls.__sync_order_LM(__backtest_cls, 't_0') __backtest_cls.__sync_order_LM( __backtest_cls, 'daily_settle') # 每日结算 # 最后一天 __backtest_cls.__end_of_trading(__backtest_cls) @classmethod def backtest_init(__backtest_cls, func, *arg, **kwargs): def __init_backtest(__backtest_cls, *arg, **kwargs): __backtest_cls.__QA_backtest_init(__backtest_cls) func(*arg, **kwargs) __backtest_cls.__QA_backtest_prepare(__backtest_cls) return __init_backtest(__backtest_cls) @classmethod def before_backtest(__backtest_cls, func, *arg, **kwargs): def __before_backtest(__backtest_cls, *arg, **kwargs): func(*arg, **kwargs) __backtest_cls.__QA_backtest_start(__backtest_cls) return __before_backtest(__backtest_cls) @classmethod def end_backtest(__backtest_cls, func, *arg, **kwargs): # yield __backtest_cls.cash __backtest_cls.__end_of_backtest(__backtest_cls, func, *arg, **kwargs) return func(*arg, **kwargs)
class QA_Backtest(): '最终目的还是实现一个通用的回测类' backtest_type = 'day' account = QA_Account() market = QA_Market() bid = QA_QAMarket_bid() setting = QA_Setting() clients = setting.client user = setting.QA_setting_user_name market_data = [] now = '' today = '' def __init__(self): self.backtest_type = 'day' self.account = QA_Account() self.market = QA_Market() self.bid = QA_QAMarket_bid() self.setting = QA_Setting() self.clients = self.setting.client self.user = self.setting.QA_setting_user_name self.market_data = [] self.now = '' self.today = '' def __QA_backtest_init(self): """既然是被当做装饰器使用,就需要把变量设置放在装饰函数的前面,把函数放在装饰函数的后面""" # 设置回测的开始结束时间 self.strategy_start_date = str('2017-01-05') self.strategy_end_date = str('2017-07-01') # 设置回测标的,是一个list对象,不过建议只用一个标的 # gap是回测时,每日获取数据的前推日期(交易日) self.strategy_gap = int(60) # 设置全局的数据库地址,回测用户名,密码,并初始化 self.setting.QA_util_sql_mongo_ip = str('127.0.0.1') self.setting.QA_setting_user_name = str('admin') self.setting.QA_setting_user_password = str('admin') self.setting.QA_setting_init() # 回测的名字 self.strategy_name = str('example_min') # 股票的交易日历,真实回测的交易周期,和交易周期在交易日历中的id self.trade_list = QA_fetch_trade_date( self.setting.client.quantaxis.trade_date) self.benchmark_code = 'hs300' """ 这里会涉及一个区间的问题,开始时间是要向后推,而结束时间是要向前推,1代表向后推,-1代表向前推 """ self.strategy_stock_list = ['000001', '000002', '000004'] self.account.init_assest = 1000000 self.backtest_bid_model = 'market_price' def __QA_backtest_prepare(self): """ 这是模型内部的 初始化,主要是初始化一些账户和市场资产 写成了私有函数 @yutiansut 2017/7/20 """ # 重新初始账户资产 self.setting.QA_setting_init() self.account.init() self.start_real_date = QA_util_get_real_date(self.strategy_start_date, self.trade_list, 1) self.start_real_id = self.trade_list.index(self.start_real_date) self.end_real_date = QA_util_get_real_date(self.strategy_end_date, self.trade_list, -1) self.end_real_id = self.trade_list.index(self.end_real_date) # 重新初始化账户的cookie self.account.account_cookie = str(random.random()) # 初始化股票池的市场数据 if self.backtest_type in ['day', 'd', '0x00']: self.market_data = QA_fetch_stocklist_day( self.strategy_stock_list, [ self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id] ]) elif self.backtest_type in ['min', 'm', '0x01']: self.market_data = QA_fetch_stocklist_min( self.strategy_stock_list, [ self.trade_list[self.start_real_id - int(self.strategy_gap)], self.trade_list[self.end_real_id] ]) def __QA_backtest_start(self, *args, **kwargs): """ 这个是回测流程开始的入口 """ assert len(self.strategy_stock_list) > 0 assert len(self.trade_list) > 0 assert isinstance(self.start_real_date, str) assert isinstance(self.end_real_date, str) assert len(self.market_data) == len(self.strategy_stock_list) QA_util_log_info('QUANTAXIS Backtest Engine Initial Successfully') QA_util_log_info('Basical Info: \n' + tabulate( [[str(__version__), str(self.strategy_name)]], headers=('Version', 'Strategy_name'))) QA_util_log_info('Stock_List: \n' + tabulate([self.strategy_stock_list])) # 初始化报价模式 self.__messages = [] def __check_state(self, bid_price, bid_amount): pass def __QA_bid_amount(self, __strategy_amount, __amount): if __strategy_amount == 'mean': return float( float(self.account.message['body']['account']['cash'][-1]) / len(self.strategy_stock_list)), 'price' elif __strategy_amount == 'half': return __amount * 0.5, 'amount' elif __strategy_amount == 'all': return __amount, 'amount' def __end_of_trading(self, *arg, **kwargs): # 在回测的最后一天,平掉所有仓位(回测的最后一天是不买入的) # 回测最后一天的交易处理 while len(self.account.hold) > 1: __hold_list = self.account.hold[1::] pre_del_id = [] for item_ in range(0, len(__hold_list)): if __hold_list[item_][3] > 0: __last_bid = self.bid __last_bid.amount = int(__hold_list[item_][3]) __last_bid.order_id = str(random.random()) __last_bid.price = 'close_price' __last_bid.code = str(__hold_list[item_][1]) __last_bid.date = self.trade_list[self.end_real_id] __last_bid.towards = -1 __last_bid.user = self.setting.QA_setting_user_name __last_bid.strategy = self.strategy_name __last_bid.bid_model = 'auto' __last_bid.status = '0x01' __last_bid.amount_model = 'amount' __message = self.market.receive_bid(__last_bid) _remains_day = 0 while __message['header']['status'] == 500: # 停牌状态,这个时候按停牌的最后一天计算价值(假设平仓) __last_bid.date = self.trade_list[self.end_real_id - _remains_day] _remains_day += 1 __message = self.market.receive_bid(__last_bid) # 直到市场不是为0状态位置,停止前推日期 self.__messages = self.account.QA_account_receive_deal( __message) else: pre_del_id.append(item_) pre_del_id.sort() pre_del_id.reverse() for item_x in pre_del_id: __hold_list.pop(item_x) def __warp_bid(self, __bid, __order): __market_data_for_backtest = self.QA_backtest_get_market_data( self, __bid.code, __bid.date, 'np', 1) __O, __H, __L, __C, __V = self.QA_backtest_get_OHLCV( self, __market_data_for_backtest) if __order['bid_model'] in [ 'limit', 'Limit', 'Limited', 'limited', 'l', 'L', 0, '0' ]: # 限价委托模式 __bid.price = __order['price'] elif __order['bid_model'] in [ 'Market', 'market', 'MARKET', 'm', 'M', 1, '1' ]: __bid.price = 0.5 * (float(__O[0]) + float(__C[0])) elif __order['bid_model'] in ['strict', 'Strict', 's', 'S', '2', 2]: __bid.price = float(__H[0]) if __bid.towards == 1 else float( __L[0]) elif __order['bid_model'] in [ 'close', 'close_price', 'c', 'C', '3', 3 ]: __bid.price = float(__C[0]) __bid.price = float('%.2f' % __bid.price) return __bid def __end_of_backtest(self, *arg, **kwargs): # 开始分析 QA_util_log_info('start analysis====\n' + str(self.strategy_stock_list)) QA_util_log_info('=' * 10 + 'Trade History' + '=' * 10) QA_util_log_info('\n' + tabulate(self.account.history, headers=('date', 'code', 'price', 'towards', 'amounts', 'order_id', 'trade_id', 'commission'))) QA_util_log_info('\n' + tabulate(self.account.detail, headers=('date', 'code', 'price', 'amounts', 'order_id', 'trade_id', 'sell_price', 'sell_order_id', 'sell_trade_id', 'sell_date', 'left_amount', 'commission'))) __exist_time = int(self.end_real_id) - int(self.start_real_id) + 1 self.__benchmark_data = QA_fetch_index_day(self.benchmark_code, self.start_real_date, self.end_real_date) if len(self.__messages) > 1: performace = QA_backtest_analysis_start( self.setting.client, self.strategy_stock_list, self.__messages, self.trade_list[self.start_real_id:self.end_real_id + 1], self.market_data, self.__benchmark_data) _backtest_mes = { 'user': self.setting.QA_setting_user_name, 'strategy': self.strategy_name, 'stock_list': performace['code'], 'start_time': self.strategy_start_date, 'end_time': self.strategy_end_date, 'account_cookie': self.account.account_cookie, 'annualized_returns': performace['annualized_returns'], 'benchmark_annualized_returns': performace['benchmark_annualized_returns'], 'assets': performace['assets'], 'benchmark_assets': performace['benchmark_assets'], 'trade_date': performace['trade_date'], 'total_date': performace['total_date'], 'win_rate': performace['win_rate'], 'alpha': performace['alpha'], 'beta': performace['beta'], 'sharpe': performace['sharpe'], 'vol': performace['vol'], 'benchmark_vol': performace['benchmark_vol'], 'max_drop': performace['max_drop'], 'exist': __exist_time, 'time': datetime.datetime.now() } QA_SU_save_backtest_message(_backtest_mes, self.setting.client) QA_SU_save_account_message(self.__messages, self.setting.client) QA_SU_save_account_to_csv(self.__messages) def QA_backtest_get_market_data(self, code, date, type_='numpy', gap_=None): '这个函数封装了关于获取的方式' gap_ = self.strategy_gap if gap_ is None else gap_ index_of_code = self.strategy_stock_list.index(code) __res = self.market_data[index_of_code][:date].tail(gap_) if type_ in ['l', 'list', 'L']: return np.asarray(__res).tolist() elif type_ in ['pd', 'pandas', 'p']: return res else: return np.asarray(__res) def QA_backtest_hold_amount(self, __code): # return sum(list(map(lambda item: item[3] if __code in item else 0, self.account.hold))) try: return self.account.hold_available[__code] except: return 0 def QA_backtest_get_OHLCV(self, __data): '快速返回 OHLCV格式' return (__data.T[1].astype(float).tolist(), __data.T[2].astype(float).tolist(), __data.T[3].astype(float).tolist(), __data.T[4].astype(float).tolist(), __data.T[5].astype(float).tolist()) def QA_backtest_send_order(self, __code: str, __amount: int, __towards: int, __order: dict): """ 2017/8/4 委托函数 在外部封装的一个报价接口,尽量满足和实盘一样的模式 输入 ============= 买入/卖出 股票代码 买入/卖出数量 委托模式* 0 限价委托 LIMIT ORDER 1 市价委托 MARKET ORDER 2 严格模式(买入按最高价 卖出按最低价) STRICT ORDER """ # 只需要维护未成交队列即可,无非是一个可用资金和可卖股票数量的调整 # 必须是100股的倍数 __amount = int(__amount / 100) * 100 # self.__QA_backtest_set_bid_model() __bid = self.bid (__bid.order_id, __bid.user, __bid.strategy, __bid.code, __bid.date, __bid.datetime, __bid.sending_time, __bid.amount, __bid.towards) = (str(random.random()), self.setting.QA_setting_user_name, self.strategy_name, __code, self.running_date, self.running_date, self.running_date, __amount, __towards) __bid = self.__warp_bid(self, __bid, __order) return self.__QA_backtest_send_bid(self, __bid) # 先进行处理: def __QA_backtest_send_bid(self, __bid): if __bid.towards == 1: # 扣费以下这个订单时的bar的open扣费 if self.account.cash_available > (__bid.price * __bid.amount): # 这是买入的情况 买入的时候主要考虑的是能不能/有没有足够的钱来买入 __message = self.market.receive_bid(__bid) self.account.cash_available -= (__bid.price * __bid.amount) # 先扔进去买入,再通过返回的值来判定是否成功 if __message['header']['status'] == 200 and __message['body'][ 'bid']['amount'] > 0: # 这个判断是为了 如果买入资金不充足,所以买入报了一个0量单的情况 # 如果买入量>0, 才判断为成功交易 self.account.QA_account_receive_deal(__message) return __message else: # self.account.cash_available += (__bid.price * __bid.amount) #报单还挂着 不能恢复 self.account.order_queue.append(__bid) # 下面是卖出操作,这里在卖出前需要考虑一个是否有仓位的问题: # 因为在股票中是不允许卖空操作的,所以这里是股票的交易引擎和期货的交易引擎的不同所在 elif __bid.towards == -1: # 如果是卖出操作 检查是否有持仓 # 股票中不允许有卖空操作 # 检查持仓面板 __amount_hold = self.QA_backtest_hold_amount(self, __bid.code) if __amount_hold > 0: __bid.amount = __amount_hold if __amount_hold < __bid.amount else __bid.amount self.account.hold_available[__bid.code] -= __bid.amount __message = self.market.receive_bid(__bid) print(__message) if __message['header']['status'] == 200: self.account.QA_account_receive_deal(__message) return __message else: self.account.order_queue.append(__bid) return __message else: err_info = 'Error: Not Enough amount for code %s in hold list' % str( __bid.code) QA_util_log_expection(err_info) return err_info else: return "Error: No buy/sell towards" def QA_backtest_check_order(self, order): '用于检查委托单的状态' """ 委托单被报入交易所会有一个回报,回报状态就是交易所返回的字段: 字段目前 2xx 是成功 4xx是失败 5xx是交易所无数据(停牌) 随着回测框架的不断升级,会有更多状态需要被管理: 200 委托成功,交易成功 203 委托成功,待成交 20 """ pass def QA_backtest_sell_all(self): while len(self.account.hold) > 1: __hold_list = self.account.hold[1::] pre_del_id = [] def __sell(id_): if __hold_list[item_][3] > 0: __last_bid = self.bid __last_bid.amount = int(__hold_list[id_][3]) __last_bid.order_id = str(random.random()) __last_bid.price = 'close_price' __last_bid.code = str(__hold_list[id_][1]) __last_bid.date = self.now __last_bid.towards = -1 __last_bid.user = self.setting.QA_setting_user_name __last_bid.strategy = self.strategy_name __last_bid.bid_model = 'auto' __last_bid.status = '0x01' __last_bid.amount_model = 'amount' __message = self.market.receive_bid(__last_bid) _remains_day = 0 while __message['header']['status'] == 500: # 停牌状态,这个时候按停牌的最后一天计算价值(假设平仓) __last_bid.date = self.trade_list[self.end_real_id - _remains_day] _remains_day += 1 __message = self.market.receive_bid(__last_bid) # 直到市场不是为0状态位置,停止前推日期 self.__messages = self.account.QA_account_receive_deal( __message) else: pre_del_id.append(id_) return pre_del_id pre_del_id = reduce(lambda _, x: __sell(x), range(len(__hold_list))) pre_del_id.sort() pre_del_id.reverse() for item_x in pre_del_id: __hold_list.pop(item_x) @classmethod def load_strategy(__backtest_cls, func, *arg, **kwargs): '策略加载函数' # 首先判断是否能满足回测的要求` __messages = {} __backtest_cls.__init_cash_per_stock = int( float(__backtest_cls.account.init_assest) / len(__backtest_cls.strategy_stock_list)) # 策略的交易日循环 for i in range(int(__backtest_cls.start_real_id), int(__backtest_cls.end_real_id) - 1, 1): __backtest_cls.running_date = __backtest_cls.trade_list[i] QA_util_log_info( '=================daily hold list====================') QA_util_log_info('in the begining of ' + __backtest_cls.running_date) QA_util_log_info( tabulate( __backtest_cls.account.message['body']['account']['hold'])) __backtest_cls.now = __backtest_cls.running_date __backtest_cls.today = __backtest_cls.running_date # 交易前同步持仓状态 __backtest_cls.account.cash_available = __backtest_cls.account.cash[ -1] __temp_hold = pd.DataFrame(__backtest_cls.account.hold[1::], columns=__backtest_cls.account.hold[0]) __temp_hold = __temp_hold.set_index('code', drop=False) __backtest_cls.account.hold_available = __temp_hold[ 'amount'].groupby('code').sum() if __backtest_cls.backtest_type in ['day', 'd']: for item in __backtest_cls.account.order_queue: item.date, item.datetime = (__backtest_cls.today, __backtest_cls.now) __backtest_cls.__QA_backtest_send_bid(__backtest_cls, item) # input() func(*arg, **kwargs) # 发委托单 elif __backtest_cls.backtest_type in ['min', 'm']: func(*arg, **kwargs) # 发委托单 # 队列循环批量发单 # 最后一天 __backtest_cls.__end_of_trading(__backtest_cls) @classmethod def backtest_init(__backtest_cls, func, *arg, **kwargs): def __init_backtest(__backtest_cls, *arg, **kwargs): __backtest_cls.__QA_backtest_init(__backtest_cls) func(*arg, **kwargs) __backtest_cls.__QA_backtest_prepare(__backtest_cls) return __init_backtest(__backtest_cls) @classmethod def before_backtest(__backtest_cls, func, *arg, **kwargs): func(*arg, **kwargs) __backtest_cls.__QA_backtest_start(__backtest_cls) @classmethod def end_backtest(__backtest_cls, func, *arg, **kwargs): # yield __backtest_cls.cash __backtest_cls.__end_of_backtest(__backtest_cls, func, *arg, **kwargs) return func(*arg, **kwargs)