[增强功能] 增加接口获取某一合约当前保证金(包括自定义套利合约)

This commit is contained in:
msincenselee 2020-11-08 11:33:47 +08:00
parent 8c6b926b82
commit aafedafca3
6 changed files with 216 additions and 39 deletions

View File

@ -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'更新回测结果至数据库')

View File

@ -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):

View File

@ -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:

View File

@ -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):
"""切换合约,从持仓的非主力合约,切换至主力合约"""

View File

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

View File

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