diff --git a/vnpy/app/cta_strategy_pro/back_testing.py b/vnpy/app/cta_strategy_pro/back_testing.py index 91311b7f..da4c334a 100644 --- a/vnpy/app/cta_strategy_pro/back_testing.py +++ b/vnpy/app/cta_strategy_pro/back_testing.py @@ -65,6 +65,7 @@ from vnpy.trader.util_logger import setup_logger from vnpy.data.mongo.mongo_data import MongoData from uuid import uuid1 + class BackTestingEngine(object): """ CTA回测引擎 @@ -208,7 +209,7 @@ class BackTestingEngine(object): # 回测任务/回测结果,保存在数据库中 self.mongo_api = None self.task_id = None - self.test_setting = None # 回测设置 + self.test_setting = None # 回测设置 self.strategy_setting = None # 所有回测策略得设置 def create_fund_kline(self, name, use_renko=False): @@ -302,6 +303,77 @@ class BackTestingEngine(object): def get_margin_rate(self, vt_symbol: str): return self.margin_rate.get(vt_symbol, 0.1) + def get_margin(self, vt_symbol: str): + """ + 按照当前价格,计算1手合约需要得保证金 + :param vt_symbol: + :return: 普通合约/期权 => 当前价格 * size * margin_rate + SP j2101&j2105.DCE => max( 当前价格 * size * margin_rate) + j2101-1-i2101-3-BJ.SPD => (主动腿价格*主动腿size * 主动腿margin_rate + 被动腿价格*被动腿size * 被动腿margin_rate + rb2101-1-rb2105-1-CJ.SPD => max(主动腿价格*主动腿size * 主动腿margin_rate , 被动腿价格*被动腿size * 被动腿margin_rate + """ + + if '.SPD99' in vt_symbol: + vt_symbol = vt_symbol.replace('.SPD99', '.SPD') + + if not vt_symbol.endswith('.SPD') and '&' not in vt_symbol: + cur_price = self.get_price(vt_symbol) + cur_size = self.get_size(vt_symbol) + cur_margin_rate = self.get_margin_rate(vt_symbol) + if cur_price and cur_size and cur_margin_rate: + return abs(cur_price * cur_size * cur_margin_rate) + else: + # 取不到价格,取不到size,或者取不到保证金比例 + self.write_error(f'无法计算{vt_symbol}的保证金,价格:{cur_price}或size:{cur_size}或margin_rate:{cur_margin_rate}') + return None + + # j2101-1-i2101-3-BJ.SPD rb2101-1-rb2105-1-CJ.SPD + if vt_symbol.endswith('.SPD'): + act_symbol, act_ratio, pas_symbol, pas_ratio, spd_type = vt_symbol.replace('.SPD', '').split('-') + act_vt_symbol = '{}.{}'.format(act_symbol, self.get_exchange(act_symbol).value) + pas_vt_symbol = '{}.{}'.format(pas_symbol, self.get_exchange(pas_symbol).value) + act_ratio = int(act_ratio) + pas_ratio = int(pas_ratio) + # SP j2101&j2105.DCE + elif '&' in vt_symbol: + symbol, exchange = extract_vt_symbol(vt_symbol) + symbol = symbol.split(' ')[-1] + act_symbol, pas_symbol = symbol.split('&') + act_vt_symbol = f'{act_symbol}.{exchange.value}' + pas_vt_symbol = f'{pas_symbol}.{exchange.value}' + act_ratio = 1 + pas_ratio = 1 + else: + self.write_error(f'无法计算{vt_symbol}的保证金:无法分解') + return None + + act_cur_price = self.get_price(act_vt_symbol) + act_size = self.get_size(act_vt_symbol) + act_margin_rate = self.get_margin_rate(act_vt_symbol) + pas_cur_price = self.get_price(pas_vt_symbol) + pas_size = self.get_size(pas_vt_symbol) + pas_margin_rate = self.get_margin_rate(pas_vt_symbol) + + if not all([act_cur_price, act_size, act_margin_rate]): + self.write_error( + f'无法计算{vt_symbol}的保证金,{act_vt_symbol}价格:{act_cur_price}或size:{act_size}或margin_rate:{act_margin_rate}') + return None + if not all([pas_cur_price, pas_size, pas_margin_rate]): + self.write_error( + f'无法计算{vt_symbol}的保证金,{pas_vt_symbol}价格:{pas_cur_price}或size:{pas_size}或margin_rate:{pas_margin_rate}') + return None + + # 跨期合约 + if get_underlying_symbol(act_symbol) == get_underlying_symbol(pas_symbol): + spd_margin = max(act_cur_price * act_size * act_margin_rate * act_ratio, + pas_cur_price * pas_size * pas_margin_rate * pas_ratio) + + # 跨品种合约,取最大值 + else: + spd_margin = act_cur_price * act_size * act_margin_rate * act_ratio + pas_cur_price * pas_size * pas_margin_rate * pas_ratio + + return spd_margin + def set_slippage(self, vt_symbol: str, slippage: float): """设置滑点点数""" self.slippage.update({vt_symbol: slippage}) @@ -599,7 +671,8 @@ class BackTestingEngine(object): def new_bar(self, bar): """新的K线""" self.last_bar.update({bar.vt_symbol: bar}) - if self.last_dt is None or (bar.datetime and bar.datetime > self.last_dt - timedelta(seconds=self.bar_interval_seconds)): + if self.last_dt is None or ( + bar.datetime and bar.datetime > self.last_dt - timedelta(seconds=self.bar_interval_seconds)): self.last_dt = bar.datetime + timedelta(seconds=self.bar_interval_seconds) self.set_price(bar.vt_symbol, bar.close_price) self.cross_stop_order(bar=bar) # 撮合停止单 @@ -717,7 +790,7 @@ class BackTestingEngine(object): elif self.contract_type == 'future': symbol = vt_symbol if self.using_99_contract: - underly_symbol = get_underlying_symbol(symbol).upper() # WJ: 当需要回测A1701.DCE时,不能替换成99合约。 + underly_symbol = get_underlying_symbol(symbol).upper() # WJ: 当需要回测A1701.DCE时,不能替换成99合约。 exchange = self.get_exchange(f'{underly_symbol}99') else: exchange = self.get_exchange(symbol) @@ -798,7 +871,7 @@ class BackTestingEngine(object): """保存策略数据""" for strategy in self.strategies.values(): self.write_log(u'save strategy data') - if hasattr(strategy,'save_data'): + if hasattr(strategy, 'save_data'): strategy.save_data() def send_order(self, @@ -1441,7 +1514,8 @@ class BackTestingEngine(object): # raise Exception(u'异常!没有空单持仓,不能cover') return - cur_short_pos_list = [s_pos.volume for s_pos in self.short_position_list if s_pos.vt_symbol == trade.vt_symbol] + cur_short_pos_list = [s_pos.volume for s_pos in self.short_position_list if + s_pos.vt_symbol == trade.vt_symbol] self.write_log(u'{}当前空单:{}'.format(trade.vt_symbol, cur_short_pos_list)) @@ -1454,7 +1528,8 @@ class BackTestingEngine(object): self.write_error(f'没有{trade.strategy_name}对应的symbol:{trade.vt_symbol}的空单持仓, 继续') break else: - self.write_error(u'异常,{}没有对应symbol:{}的空单持仓, 终止'.format(trade.strategy_name, trade.vt_symbol)) + self.write_error( + u'异常,{}没有对应symbol:{}的空单持仓, 终止'.format(trade.strategy_name, trade.vt_symbol)) # raise Exception(u'realtimeCalculate2() Exception,没有对应symbol:{0}的空单持仓'.format(trade.vt_symbol)) return @@ -1587,7 +1662,6 @@ class BackTestingEngine(object): cover_volume = 0 - if g_result is not None: # 更新组合的数据 g_result.turnover = g_result.turnover + result.turnover @@ -1629,7 +1703,8 @@ class BackTestingEngine(object): # raise RuntimeError(f'realtimeCalculate2() Exception,没有对应的symbol:{trade.vt_symbol}多单数据,') return - cur_long_pos_list = [s_pos.volume for s_pos in self.long_position_list if s_pos.vt_symbol == trade.vt_symbol] + cur_long_pos_list = [s_pos.volume for s_pos in self.long_position_list if + s_pos.vt_symbol == trade.vt_symbol] self.write_log(u'{}当前多单:{}'.format(trade.vt_symbol, cur_long_pos_list)) @@ -2206,12 +2281,12 @@ class BackTestingEngine(object): self.mongo_api = MongoData(host=save_mongo.get('host', 'localhost'), port=save_mongo.get('port', 27017)) d = { - 'task_id': self.task_id, # 单实例回测任务id - 'name': self.test_name, # 回测实例名称, 策略名+参数+时间 + 'task_id': self.task_id, # 单实例回测任务id + 'name': self.test_name, # 回测实例名称, 策略名+参数+时间 'group_id': self.test_setting.get('group_id', datetime.now().strftime('%y-%m-%d')), # 回测组合id 'status': 'start', - 'task_start_time': datetime.now(), # 任务开始执行时间 - 'run_host': socket.gethostname(), # 任务运行得host主机 + 'task_start_time': datetime.now(), # 任务开始执行时间 + 'run_host': socket.gethostname(), # 任务运行得host主机 'test_setting': self.test_setting, # 回测参数 'strategy_setting': self.strategy_setting, # 策略参数 } @@ -2274,11 +2349,11 @@ class BackTestingEngine(object): flt=flt) if d: - d.update({'status': 'finish'}) # 更新状态未完成 - d.update(result_info) # 补充回测结果 - d.update({'task_finish_time': datetime.now()}) # 更新回测完成时间 - d.update({'trade_list': binary.Binary(zlib.compress(pickle.dumps(self.trade_pnl_list)))}) # 更新交易记录 - d.update({'daily_list': binary.Binary(zlib.compress(pickle.dumps(self.daily_list)))}) # 更新每日净值记录 + d.update({'status': 'finish'}) # 更新状态未完成 + d.update(result_info) # 补充回测结果 + d.update({'task_finish_time': datetime.now()}) # 更新回测完成时间 + d.update({'trade_list': binary.Binary(zlib.compress(pickle.dumps(self.trade_pnl_list)))}) # 更新交易记录 + d.update({'daily_list': binary.Binary(zlib.compress(pickle.dumps(self.daily_list)))}) # 更新每日净值记录 self.write_log(u'更新回测结果至数据库') diff --git a/vnpy/app/cta_strategy_pro/engine.py b/vnpy/app/cta_strategy_pro/engine.py index f3f96b79..e0579399 100644 --- a/vnpy/app/cta_strategy_pro/engine.py +++ b/vnpy/app/cta_strategy_pro/engine.py @@ -850,6 +850,78 @@ class CtaEngine(BaseEngine): return contract.min_volume + def get_margin(self, vt_symbol: str): + """ + 按照当前价格,计算1手合约需要得保证金 + :param vt_symbol: + :return: 普通合约/期权 => 当前价格 * size * margin_rate + SP j2101&j2105.DCE => max( 当前价格 * size * margin_rate) + j2101-1-i2101-3-BJ.SPD => (主动腿价格*主动腿size * 主动腿margin_rate + 被动腿价格*被动腿size * 被动腿margin_rate + rb2101-1-rb2105-1-CJ.SPD => max(主动腿价格*主动腿size * 主动腿margin_rate , 被动腿价格*被动腿size * 被动腿margin_rate + """ + if not vt_symbol.endswith('.SPD') and '&' not in vt_symbol: + cur_price = self.get_price(vt_symbol) + cur_size = self.get_size(vt_symbol) + cur_margin_rate = self.get_margin_rate(vt_symbol) + if cur_price and cur_size and cur_margin_rate: + return abs(cur_price * cur_size * cur_margin_rate) + else: + # 取不到价格,取不到size,或者取不到保证金比例 + self.write_error(f'无法计算{vt_symbol}的保证金,价格:{cur_price}或size:{cur_size}或margin_rate:{cur_margin_rate}') + return None + + # j2101-1-i2101-3-BJ.SPD rb2101-1-rb2105-1-CJ.SPD + if vt_symbol.endswith('.SPD'): + contract_conf = self.get_custom_contract(vt_symbol) + if contract_conf is None: + self.write_error(f'无法计算{vt_symbol}保证金,获取不到自定义合约配置') + return None + act_symbol = contract_conf.get('leg1_symbol') + pas_symbol = contract_conf.get('leg2_symbol') + act_vt_symbol = '{}.{}'.format(act_symbol, contract_conf.get('leg1_exchange')) + pas_vt_symbol = '{}.{}'.format(pas_symbol, contract_conf.get('leg2_exchange')) + act_ratio = int(contract_conf.get('leg1_ratio')) + pas_ratio = int(contract_conf.get('leg2_ratio')) + # SP j2101&j2105.DCE + elif '&' in vt_symbol: + symbol, exchange = extract_vt_symbol(vt_symbol) + symbol = symbol.split(' ')[-1] + act_symbol, pas_symbol = symbol.split('&') + act_vt_symbol = f'{act_symbol}.{exchange.value}' + pas_vt_symbol = f'{pas_symbol}.{exchange.value}' + act_ratio = 1 + pas_ratio = 1 + else: + self.write_error(f'无法计算{vt_symbol}的保证金:无法分解') + return None + + act_cur_price = self.get_price(act_vt_symbol) + act_size = self.get_size(act_vt_symbol) + act_margin_rate = self.get_margin_rate(act_vt_symbol) + pas_cur_price = self.get_price(pas_vt_symbol) + pas_size = self.get_size(pas_vt_symbol) + pas_margin_rate = self.get_margin_rate(pas_vt_symbol) + + if not all([act_cur_price, act_size, act_margin_rate]): + self.write_error( + f'无法计算{vt_symbol}的保证金,{act_vt_symbol}价格:{act_cur_price}或size:{act_size}或margin_rate:{act_margin_rate}') + return None + if not all([pas_cur_price, pas_size, pas_margin_rate]): + self.write_error( + f'无法计算{vt_symbol}的保证金,{pas_vt_symbol}价格:{pas_cur_price}或size:{pas_size}或margin_rate:{pas_margin_rate}') + return None + + # 跨期合约 + if get_underlying_symbol(act_symbol) == get_underlying_symbol(pas_symbol): + spd_margin = max(act_cur_price * act_size * act_margin_rate * act_ratio, + pas_cur_price * pas_size * pas_margin_rate * pas_ratio) + + # 跨品种合约,取最大值 + else: + spd_margin = act_cur_price * act_size * act_margin_rate * act_ratio + pas_cur_price * pas_size * pas_margin_rate * pas_ratio + + return spd_margin + def get_tick(self, vt_symbol: str): """获取合约得最新tick""" return self.main_engine.get_tick(vt_symbol) @@ -873,6 +945,24 @@ class CtaEngine(BaseEngine): return self.main_engine.get_contract(vt_symbol) def get_custom_contract(self, vt_symbol): + """ + 获取自定义合约的设置 + :param symbol: "pb2012-1-pb2101-1-CJ" + :return: { + "name": "pb跨期价差", + "exchange": "SPD", + "leg1_symbol": "pb2012", + "leg1_exchange": "SHFE", + "leg1_ratio": 1, + "leg2_symbol": "pb2101", + "leg2_exchange": "SHFE", + "leg2_ratio": 1, + "is_spread": true, + "size": 1, + "margin_rate": 0.1, + "price_tick": 5 + } + """ return self.main_engine.get_custom_contract(vt_symbol.split('.')[0]) def get_all_contracts(self): diff --git a/vnpy/app/cta_strategy_pro/spread_testing.py b/vnpy/app/cta_strategy_pro/spread_testing.py index 1292e641..8792c25f 100644 --- a/vnpy/app/cta_strategy_pro/spread_testing.py +++ b/vnpy/app/cta_strategy_pro/spread_testing.py @@ -288,6 +288,9 @@ class SpreadTestingEngine(BackTestingEngine): self.write_log(u'开始套利组合回测') + # 保存回测脚本到数据库 + self.save_setting_to_mongo() + for strategy_name, strategy_setting in strategy_settings.items(): # 策略得启动日期 if 'start_date' in strategy_setting: diff --git a/vnpy/app/cta_strategy_pro/template.py b/vnpy/app/cta_strategy_pro/template.py index 6a7128a0..ba01867e 100644 --- a/vnpy/app/cta_strategy_pro/template.py +++ b/vnpy/app/cta_strategy_pro/template.py @@ -771,8 +771,8 @@ class CtaProTemplate(CtaTemplate): short_symbol = sg.snapshot.get('mi_symbol', self.vt_symbol) pos_symbols.add(short_symbol) - self.write_log(u'加载持仓空单[{},价格:{}],[指数:{},价格:{}],数量:{}手' - .format(short_symbol, sg.snapshot.get('open_price'), + self.write_log(u'加载持仓空单[ID:{},vt_symbol:{},价格:{}],[指数:{},价格:{}],数量:{}手' + .format(sg.id, short_symbol, sg.snapshot.get('open_price'), self.idx_symbol, sg.open_price, sg.volume)) self.position.short_pos -= sg.volume @@ -797,8 +797,8 @@ class CtaProTemplate(CtaTemplate): long_symbol = lg.snapshot.get('mi_symbol', self.vt_symbol) pos_symbols.add(long_symbol) - self.write_log(u'加载持仓多单[{},价格:{}],[指数{},价格:{}],数量:{}手' - .format(lg.snapshot.get('miSymbol'), lg.snapshot.get('open_price'), + self.write_log(u'加载持仓多单[ID:{},vt_symbol:{},价格:{}],[指数{},价格:{}],数量:{}手' + .format(lg.id, long_symbol, lg.snapshot.get('open_price'), self.idx_symbol, lg.open_price, lg.volume)) self.position.long_pos += lg.volume @@ -899,24 +899,29 @@ class CtaProTemplate(CtaTemplate): none_mi_grid = None none_mi_symbol = None - + self.write_log(f'持仓换月=>启动.') # 找出非主力合约的持仓网格 for g in self.gt.get_opened_grids(direction=Direction.LONG): - none_mi_symbol = g.snapshot.get('mi_symbol') + none_mi_symbol = g.snapshot.get('mi_symbol', g.vt_symbol) + # 如果持仓的合约,跟策略配置的vt_symbol一致,则不处理 if none_mi_symbol is None or none_mi_symbol == self.vt_symbol: - # 如果持仓的合约,跟策略配置的vt_symbol一致,则不处理 + self.write_log(f'none_mi_symbol:{none_mi_symbol}, vt_symbol:{self.vt_symbol} 一致,不处理') continue + + # 如果未开仓,或者处于委托状态,或者已交易完毕,不处理 if not g.open_status or g.order_status or g.volume - g.traded_volume <= 0: + self.write_log(f'开仓状态:{g.open_status}, 委托状态:{g.order_status},网格持仓:{g.volume} ,已交易数量:{g.traded_volume}, 不处理') continue none_mi_grid = g if g.traded_volume > 0 and g.volume - g.traded_volume > 0: + g.volume -= g.traded_volume g.traded_volume = 0 break if none_mi_grid is None: return - + self.write_log(f'持仓换月=>找到多单持仓:{none_mi_symbol},持仓数量:{none_mi_grid.volume}') # 找到行情中非主力合约/主力合约的最新价 none_mi_tick = self.tick_dict.get(none_mi_symbol) mi_tick = self.tick_dict.get(self.vt_symbol, None) @@ -925,24 +930,28 @@ class CtaProTemplate(CtaTemplate): # 如果涨停价,不做卖出 if self.is_upper_limit(none_mi_symbol) or self.is_upper_limit(self.vt_symbol): + self.write_log(f'{none_mi_symbol} 或 {self.vt_symbol} 为涨停价,不做换仓') return none_mi_price = max(none_mi_tick.last_price, none_mi_tick.bid_price_1) grid = deepcopy(none_mi_grid) grid.id = str(uuid.uuid1()) + grid.open_status = False + self.write_log(f'持仓换月=>复制持仓信息{none_mi_symbol},ID:{none_mi_grid.id} => {self.vt_symbol},ID:{grid.id}') # 委托卖出非主力合约 + vt_orderids = self.sell(price=none_mi_price, volume=none_mi_grid.volume, vt_symbol=none_mi_symbol, order_type=self.order_type, grid=none_mi_grid) if len(vt_orderids) > 0: - self.write_log(f'切换合约,委托卖出非主力合约{none_mi_symbol}持仓:{none_mi_grid.volume}') + self.write_log(f'持仓换月=>委托卖出非主力合约{none_mi_symbol}持仓:{none_mi_grid.volume}') # 已经发生过换月的,不执行买入新合约 if none_mi_grid.snapshot.get("switched", False): - self.write_log(f'已经执行过换月,不再创建新的买入操作') + self.write_log(f'持仓换月=>已经执行过换月,不再创建新的买入操作') return none_mi_grid.snapshot.update({'switched': True}) @@ -956,13 +965,13 @@ class CtaProTemplate(CtaTemplate): order_type=self.order_type, grid=grid) if len(vt_orderids) > 0: - self.write_log(u'切换合约,委托买入主力合约:{},价格:{},数量:{}' + self.write_log(u'持仓换月=>委托买入主力合约:{},价格:{},数量:{}' .format(self.vt_symbol, self.cur_mi_price, grid.volume)) else: - self.write_error(f'委托买入主力合约:{self.vt_symbol}失败') + self.write_error(f'持仓换月=>委托买入主力合约:{self.vt_symbol}失败') self.gt.save() else: - self.write_error(f'委托卖出非主力合约:{none_mi_symbol}失败') + self.write_error(f'持仓换月=>委托卖出非主力合约:{none_mi_symbol}失败') def tns_switch_short_pos(self): """切换合约,从持仓的非主力合约,切换至主力合约""" diff --git a/vnpy/gateway/ctp/ctp_gateway.py b/vnpy/gateway/ctp/ctp_gateway.py index e695920c..d909ebdd 100644 --- a/vnpy/gateway/ctp/ctp_gateway.py +++ b/vnpy/gateway/ctp/ctp_gateway.py @@ -1070,6 +1070,8 @@ class CtpTdApi(TdApi): future_contract.update({'margin_rate': mi_margin_rate}) future_contract.update({'symbol_size': idx_contract.size}) future_contract.update({'price_tick': idx_contract.pricetick}) + if 'exchange' not in future_contract: + future_contract.update({'exchange': contract.exchange.value}) future_contracts.update({underlying_symbol: future_contract}) self.future_contract_changed = True index_contracts.update({underlying_symbol: idx_contract}) @@ -2090,4 +2092,3 @@ class TqMdApi(): self.update_thread.join() except Exception as e: self.gateway.write_log('退出天勤行情api异常:{}'.format(str(e))) - diff --git a/vnpy/trader/engine.py b/vnpy/trader/engine.py index c320a626..1c4870e4 100644 --- a/vnpy/trader/engine.py +++ b/vnpy/trader/engine.py @@ -452,14 +452,13 @@ class OmsEngine(BaseEngine): import bz2 import pickle contract_file_name = 'vn_contract.pkb2' - if not os.path.exists(contract_file_name): - return - try: - with bz2.BZ2File(contract_file_name, 'rb') as f: - self.contracts = pickle.load(f) - self.write_log(f'加载缓存合约字典:{contract_file_name}') - except Exception as ex: - self.write_log(f'加载缓存合约异常:{str(ex)}') + if os.path.exists(contract_file_name): + try: + with bz2.BZ2File(contract_file_name, 'rb') as f: + self.contracts = pickle.load(f) + self.write_log(f'加载缓存合约字典:{contract_file_name}') + except Exception as ex: + self.write_log(f'加载缓存合约异常:{str(ex)}') # 更新自定义合约 custom_contracts = self.get_all_custom_contracts()