diff --git a/rqalpha/data/base_data_source/data_source.py b/rqalpha/data/base_data_source/data_source.py index fe42cc091..9f46083be 100644 --- a/rqalpha/data/base_data_source/data_source.py +++ b/rqalpha/data/base_data_source/data_source.py @@ -32,6 +32,7 @@ from rqalpha.utils.functools import lru_cache from rqalpha.utils.typing import DateLike from rqalpha.environment import Environment +from rqalpha.data.bundle import update_futures_trading_parameters from rqalpha.data.base_data_source.adjust import FIELDS_REQUIRE_ADJUSTMENT, adjust_bars from rqalpha.data.base_data_source.storage_interface import (AbstractCalendarStore, AbstractDateSet, @@ -39,7 +40,7 @@ AbstractInstrumentStore) from rqalpha.data.base_data_source.storages import (DateSet, DayBarStore, DividendStore, ExchangeTradingCalendarStore, FutureDayBarStore, - FutureInfoStore, InstrumentStore, + FutureInfoStore, FuturesTradingParametersStore,InstrumentStore, ShareTransformationStore, SimpleFactorStore, YieldCurveStore) @@ -71,7 +72,7 @@ class BaseDataSource(AbstractDataSource): INSTRUMENT_TYPE.PUBLIC_FUND, ) - def __init__(self, path, custom_future_info): + def __init__(self, path, custom_future_info, trading_parameters_update_args): if not os.path.exists(path): raise RuntimeError('bundle path {} not exist'.format(os.path.abspath(path))) @@ -86,20 +87,32 @@ def _p(name): INSTRUMENT_TYPE.ETF: funds_day_bar_store, INSTRUMENT_TYPE.LOF: funds_day_bar_store } # type: Dict[INSTRUMENT_TYPE, AbstractDayBarStore] - + + self._futures_trading_parameters_store = None + if trading_parameters_update_args: + if update_futures_trading_parameters(path, trading_parameters_update_args): + self._futures_trading_parameters_store = FuturesTradingParametersStore(_p("futures_trading_parameters.h5")) self._future_info_store = FutureInfoStore(_p("future_info.json"), custom_future_info) - + self._instruments_stores = {} # type: Dict[INSTRUMENT_TYPE, AbstractInstrumentStore] self._ins_id_or_sym_type_map = {} # type: Dict[str, INSTRUMENT_TYPE] instruments = [] + with open(_p('instruments.pk'), 'rb') as f: for i in pickle.load(f): if i["type"] == "Future" and Instrument.is_future_continuous_contract(i["order_book_id"]): i["listed_date"] = datetime(1990, 1, 1) - instruments.append(Instrument(i, lambda i: self._future_info_store.get_future_info(i)["tick_size"])) + instruments.append(Instrument( + i, + lambda i: self._future_info_store.get_future_info(i).tick_size, + lambda i, dt: self.get_futures_trading_parameters(i, dt).long_margin_ratio, + lambda i, dt: self.get_futures_trading_parameters(i, dt).short_margin_ratio + )) for ins_type in self.DEFAULT_INS_TYPES: self.register_instruments_store(InstrumentStore(instruments, ins_type)) + if "margin_rate" not in self._future_info_store._default_data[list(self._future_info_store._default_data.keys())[0]]: + self._future_info_store.data_compatible(self._instruments_stores[INSTRUMENT_TYPE.FUTURE]) dividend_store = DividendStore(_p('dividends.h5')) self._dividends = { INSTRUMENT_TYPE.CS: dividend_store, @@ -362,6 +375,15 @@ def get_yield_curve(self, start_date, end_date, tenor=None): def get_commission_info(self, instrument): return self._future_info_store.get_future_info(instrument) + def get_futures_trading_parameters(self, instrument, dt): + if self._futures_trading_parameters_store: + trading_parameters = self._futures_trading_parameters_store.get_futures_trading_parameters(instrument, dt) + if trading_parameters == None: + return self.get_commission_info(instrument) + return trading_parameters + else: + return self.get_commission_info(instrument) + def get_merge_ticks(self, order_book_id_list, trading_date, last_dt=None): raise NotImplementedError diff --git a/rqalpha/data/base_data_source/storages.py b/rqalpha/data/base_data_source/storages.py index 73c263172..bdbcf4969 100644 --- a/rqalpha/data/base_data_source/storages.py +++ b/rqalpha/data/base_data_source/storages.py @@ -29,6 +29,7 @@ import numpy as np import pandas from methodtools import lru_cache +import collections from rqalpha.const import COMMISSION_TYPE, INSTRUMENT_TYPE from rqalpha.model.instrument import Instrument @@ -56,6 +57,18 @@ class FutureInfoStore(object): "by_money": COMMISSION_TYPE.BY_MONEY } + FuturesInfo = collections.namedtuple("FuturesInfo", [ + "order_book_id", + "underlying_symbol", + "close_commission_ratio", + "close_commission_today_ratio", + "commission_type", + "open_commission_ratio", + "long_margin_ratio", + "short_margin_ratio", + "tick_size" + ]) + def __init__(self, f, custom_future_info): with open(f, "r") as json_file: self._default_data = { @@ -66,25 +79,50 @@ def __init__(self, f, custom_future_info): self._custom_data = custom_future_info self._future_info = {} + def data_compatible(self, futures_instruments_store): + """ + RQAlpha==5.3.5 后, margin_rate调整为从 future_info.json 获取,当用户的 bundle 数据未更新时,调用该函数进行兼容 + """ + hard_code = {"TC": 0.05, "ER": 0.05, "WS": 0.05, "RO": 0.05, "ME": 0.06, "WT": 0.05} + for id_or_syms in list(self._default_data.keys()): + if len(id_or_syms) <= 2: + if id_or_syms in hard_code.keys(): + self._default_data[id_or_syms]["margin_rate"] = hard_code[id_or_syms] + else: + order_book_id = futures_instruments_store._instruments[id_or_syms + "88"].trading_code + self._default_data[id_or_syms]["margin_rate"] = futures_instruments_store._instruments[order_book_id].margin_rate + else: + self._default_data[id_or_syms]["margin_rate"] = futures_instruments_store._instruments[id_or_syms].margin_rate + @classmethod def _process_future_info_item(cls, item): item["commission_type"] = cls.COMMISSION_TYPE_MAP[item["commission_type"]] return item + @lru_cache(1024) def get_future_info(self, instrument): - # type: (Instrument) -> Dict[str, float] order_book_id = instrument.order_book_id - try: - return self._future_info[order_book_id] - except KeyError: - custom_info = self._custom_data.get(order_book_id) or self._custom_data.get(instrument.underlying_symbol) - info = self._default_data.get(order_book_id) or self._default_data.get(instrument.underlying_symbol) - if custom_info: - info = copy(info) or {} - info.update(custom_info) - elif not info: - raise NotImplementedError(_("unsupported future instrument {}").format(order_book_id)) - return self._future_info.setdefault(order_book_id, info) + custom_info = self._custom_data.get(order_book_id) or self._custom_data.get(instrument.underlying_symbol) + info = self._default_data.get(order_book_id) or self._default_data.get(instrument.underlying_symbol) + if custom_info: + info = copy(info) or {} + info.update(custom_info) + elif not info: + raise NotImplementedError(_("unsupported future instrument {}").format(order_book_id)) + info = self.to_namedtuple(info) + return info + + def to_namedtuple(self, info): + if info.get("order_book_id"): + info_data_list = [info["order_book_id"], None] + else: + info_data_list = [None, info["underlying_symbol"]] + for field in self.FuturesInfo._fields: + if (field in ["order_book_id", "underlying_symbol"]): continue + if (field in ["long_margin_ratio", "short_margin_ratio"]): field = "margin_rate" + info_data_list.append(info[field]) + info = self.FuturesInfo._make(info_data_list) + return info class InstrumentStore(AbstractInstrumentStore): @@ -208,6 +246,66 @@ class FutureDayBarStore(DayBarStore): DEFAULT_DTYPE = np.dtype(DayBarStore.DEFAULT_DTYPE.descr + [("open_interest", ' FuturesTradingParameters + dt = convert_date_to_date_int(dt) + if dt < self.FUTURES_TRADING_PARAMETERS_START_DATE: + return None + self._order_book_id = instrument.order_book_id + dt = dt * 1000000 + with h5_file(self._path) as h5: + data = h5[self._order_book_id][:] + self.arr = data[data['datetime'] == dt] + if len(self.arr) == 0: + if dt in range( + convert_date_to_date_int(instrument.listed_date), + convert_date_to_date_int(instrument.de_listed_date) + ): + raise + else: + return None + self.to_namedtuple() + return self.futures_trading_parameters + + def to_namedtuple(self): + parameter_data_list = [] + for field in self.FuturesTradingParameters._fields: + if field == "order_book_id": + parameter_data = self._order_book_id + elif field == "commission_type": + parameter_data = self.COMMISSION_TYPE_MAP[int(self.arr[field][0])] + else: + parameter_data = float(self.arr[field][0]) + parameter_data_list.append(parameter_data) + self.futures_trading_parameters = self.FuturesTradingParameters._make(parameter_data_list) + + class DividendStore(AbstractDividendStore): def __init__(self, path): self._path = path diff --git a/rqalpha/data/bundle.py b/rqalpha/data/bundle.py index 69159cfcc..fe8371edc 100644 --- a/rqalpha/data/bundle.py +++ b/rqalpha/data/bundle.py @@ -136,42 +136,60 @@ def gen_share_transformation(d): def gen_future_info(d): future_info_file = os.path.join(d, 'future_info.json') + def _need_to_recreate(): + if not os.path.exists(future_info_file): + return + else: + with open(future_info_file, "r") as f: + all_futures_info = json.load(f) + if "margin_rate" not in all_futures_info[0]: + return True + + if (_need_to_recreate()): os.remove(future_info_file) + + # 新增 hard_code 的种类时,需要同时修改rqalpha.data.base_data_source.storages.FutureInfoStore.data_compatible中的内容 hard_code = [ {'underlying_symbol': 'TC', 'close_commission_ratio': 4.0, 'close_commission_today_ratio': 0.0, 'commission_type': "by_volume", 'open_commission_ratio': 4.0, + 'margin_rate': 0.05, 'tick_size': 0.2}, {'underlying_symbol': 'ER', 'close_commission_ratio': 2.5, 'close_commission_today_ratio': 2.5, 'commission_type': "by_volume", 'open_commission_ratio': 2.5, + 'margin_rate': 0.05, 'tick_size': 1.0}, {'underlying_symbol': 'WS', 'close_commission_ratio': 2.5, 'close_commission_today_ratio': 0.0, 'commission_type': "by_volume", 'open_commission_ratio': 2.5, + 'margin_rate': 0.05, 'tick_size': 1.0}, {'underlying_symbol': 'RO', 'close_commission_ratio': 2.5, 'close_commission_today_ratio': 0.0, 'commission_type': "by_volume", 'open_commission_ratio': 2.5, + 'margin_rate': 0.05, 'tick_size': 2.0}, {'underlying_symbol': 'ME', 'close_commission_ratio': 1.4, 'close_commission_today_ratio': 0.0, 'commission_type': "by_volume", 'open_commission_ratio': 1.4, + 'margin_rate': 0.06, 'tick_size': 1.0}, {'underlying_symbol': 'WT', 'close_commission_ratio': 5.0, 'close_commission_today_ratio': 5.0, 'commission_type': "by_volume", 'open_commission_ratio': 5.0, + 'margin_rate': 0.05, 'tick_size': 1.0}, ] @@ -209,7 +227,9 @@ def gen_future_info(d): commission = commission.iloc[0] for p in param: future_dict[p] = commission[p] - future_dict['tick_size'] = rqdatac.instruments(future).tick_size() + instruemnts_data = rqdatac.instruments(future) + future_dict['margin_rate'] = instruemnts_data.margin_rate + future_dict['tick_size'] = instruemnts_data.tick_size() elif underlying_symbol in symbol_list: continue else: @@ -224,7 +244,9 @@ def gen_future_info(d): for p in param: future_dict[p] = commission[p] - future_dict['tick_size'] = rqdatac.instruments(dominant).tick_size() + instruemnts_data = rqdatac.instruments(dominant) + future_dict['margin_rate'] = instruemnts_data.margin_rate + future_dict['tick_size'] = instruemnts_data.tick_size() all_futures_info.append(future_dict) with open(os.path.join(d, 'future_info.json'), 'w') as f: @@ -250,6 +272,8 @@ def __call__(self, *args, **kwargs): INDEX_FIELDS = ['open', 'close', 'high', 'low', 'prev_close', 'volume', 'total_turnover'] FUTURES_FIELDS = STOCK_FIELDS + ['settlement', 'prev_settlement', 'open_interest'] FUND_FIELDS = STOCK_FIELDS +FUTURES_TRADING_PARAMETERS_FIELDS = ["long_margin_ratio", "short_margin_ratio", "commission_type", "open_commission", "close_commission", "close_commission_today"] +TRADING_PARAMETERS_START_DATE = 20100401 class DayBarTask(ProgressedTask): @@ -395,3 +419,132 @@ def update_bundle(path, create, enable_compression=False, concurrency=1): executor.submit(GenerateFileTask(func), path) for file, order_book_id, field in day_bar_args: executor.submit(_DayBarTask(order_book_id), os.path.join(path, file), field, **kwargs) + + +class FuturesTradingParametersTask(ProgressedTask): + def __init__(self, order_book_ids): + self._order_book_ids = order_book_ids + + @property + def total_steps(self): + # type: () -> int + return len(self._order_book_ids) + + def change_commission_type(self, commission_type): + if commission_type == "by_money": + commission_type = 0 + elif commission_type == "by_volume": + commission_type = 1 + return commission_type + + def __call__(self, *args, **kwargs): + raise NotImplementedError + + +class GeneratorFuturesTradingParametersTask(FuturesTradingParametersTask): + def __call__(self, path, fields, end_date): + with h5py.File(path, "w") as h5: + df = rqdatac.futures.get_trading_parameters(self._order_book_ids, TRADING_PARAMETERS_START_DATE, end_date, fields) + if not (df is None or df.empty): + df.dropna(axis=0, how="all") + df.reset_index(inplace=True) + df.rename(columns={ + "open_commission": "open_commission_ratio", + "close_commission": "close_commission_ratio", + "close_commission_today": "close_commission_today_ratio" + }, inplace=True) + df["datetime"] = [convert_date_to_int(d) for d in df["trading_date"]] + del df["trading_date"] + df["commission_type"] = [self.change_commission_type(v) for v in df["commission_type"]] + df.set_index(["order_book_id", "datetime"], inplace=True) + df.sort_index(inplace=True) + for order_book_id in df.index.levels[0]: + h5.create_dataset(order_book_id, data=df.loc[order_book_id].to_records()) + + +class UpdateFuturesTradingParametersTask(FuturesTradingParametersTask): + def trading_parameters_need_to_update(self, path, end_date): + last_date = TRADING_PARAMETERS_START_DATE * 1000000 + with h5py.File(path, "r") as h5: + for key in h5.keys(): + if int(h5[key]['datetime'][-1]) > last_date: + last_date = h5[key]['datetime'][-1] + if (last_date // 1000000) >= int(end_date): + return False + else: + return last_date // 1000000 + + def __call__(self, path, end_date, fields, **kwargs): + need_create_h5 = False + try: + h5py.File(path, "r") + except (OSError, RuntimeError): + need_create_h5 = True + if need_create_h5: + GeneratorFuturesTradingParametersTask(self._order_book_ids)(path, fields, end_date, **kwargs) + else: + last_date = self.trading_parameters_need_to_update(path, end_date) + if last_date: + try: + h5 = h5py.File(path, "a") + except OSError: + raise OSError("File {} update failed, if it is using, please update later, " + "or you can delete then update again".format(path)) + start_date = rqdatac.get_next_trading_date(str(last_date)) + df = rqdatac.futures.get_trading_parameters(self._order_book_ids, start_date, end_date, fields) + if not(df is None or df.empty): + df.dropna(axis=0, how="all") + df.reset_index(inplace=True) + df['datetime'] = [convert_date_to_int(d) for d in df['trading_date']] + del [df['trading_date']] + df['commission_type'] = [self.change_commission_type(v) for v in df['commission_type']] + df.set_index(['order_book_id', 'datetime'], inplace=True) + for order_book_id in df.index.levels[0]: + if order_book_id in h5: + data = np.array( + [tuple(i) for i in chain(h5[order_book_id][:], df.loc[order_book_id].to_records())], + dtype=h5[order_book_id].dtype + ) + del h5[order_book_id] + h5.create_dataset(order_book_id, data=data) + else: + h5.create_dataset(order_book_id, data=df.loc[order_book_id].to_records()) + h5.close() + + +def check_rqdata_permission(): + """ + 检测以下内容,均符合才会更新期货交易参数: + 1. rqdatac 版本是否为具备 futures.get_trading_parameters API 的版本 + 2. 当前 rqdatac 是否具备上述 API 的使用权限 + """ + if rqdatac.__version__ < '2.11.11.4': + from rqalpha.utils.logger import system_log + print("RQAlpha 已支持使用期货历史保证金和费率进行回测,请将 RQDatac 升级至 2.11.12 及以上版本进行使用") + return + try: + rqdatac.futures.get_trading_parameters("A1005") + except Exception as e: + if isinstance(e, rqdatac.share.errors.PermissionDenied): + print("您的 rqdata 账号没有权限使用期货历史保证金和费率,将使用固定的保证金和费率进行回测和计算\n可联系米筐科技开通权限:0755-26569969") + return + return True + + +def update_futures_trading_parameters(path, futures_trading_parameters_config): + update_permission = check_rqdata_permission() + if not update_permission: + return + df = rqdatac.all_instruments("Future") + order_book_ids = (df[df['de_listed_date'] >= str(TRADING_PARAMETERS_START_DATE)]).order_book_id.tolist() + futures_trading_parameters_args = { + "file": "futures_trading_parameters.h5", + "order_book_ids": order_book_ids, + "fields": FUTURES_TRADING_PARAMETERS_FIELDS + } + UpdateFuturesTradingParametersTask(futures_trading_parameters_args['order_book_ids'])( + os.path.join(path, futures_trading_parameters_args['file']), + futures_trading_parameters_config["end_date"].strftime("%Y%m%d"), + futures_trading_parameters_args['fields'] + ) + return True \ No newline at end of file diff --git a/rqalpha/data/data_proxy.py b/rqalpha/data/data_proxy.py index e2337a02c..ebf3cf068 100644 --- a/rqalpha/data/data_proxy.py +++ b/rqalpha/data/data_proxy.py @@ -244,6 +244,10 @@ def get_commission_info(self, order_book_id): instrument = self.instruments(order_book_id) return self._data_source.get_commission_info(instrument) + def get_futures_trading_parameters(self, order_book_id, dt): + instrument = self.instruments(order_book_id) + return self._data_source.get_futures_trading_parameters(instrument, dt) + def get_merge_ticks(self, order_book_id_list, trading_date, last_dt=None): return self._data_source.get_merge_ticks(order_book_id_list, trading_date, last_dt) diff --git a/rqalpha/interface.py b/rqalpha/interface.py index 653cd8f1a..7f3821609 100644 --- a/rqalpha/interface.py +++ b/rqalpha/interface.py @@ -454,7 +454,7 @@ def available_data_range(self, frequency): """ raise NotImplementedError - def get_commission_info(self, instrument): + def get_commission_info(self, instrument, dt): """ 获取合约的手续费信息 :param instrument: @@ -462,6 +462,12 @@ def get_commission_info(self, instrument): """ raise NotImplementedError + def get_futures_trading_parameters(self, instrument, dt): + """ + 获取期货合约的时序手续费信息 + """ + raise NotImplementedError + def get_merge_ticks(self, order_book_id_list, trading_date, last_dt=None): """ 获取合并的 ticks diff --git a/rqalpha/main.py b/rqalpha/main.py index 5f397cb29..a4fbbf6e7 100644 --- a/rqalpha/main.py +++ b/rqalpha/main.py @@ -122,6 +122,7 @@ def init_rqdatac(rqdatac_uri): init_rqdatac_env(rqdatac_uri) try: rqdatac.init() + return True except Exception as e: system_log.warn(_('rqdatac init failed, some apis will not function properly: {}').format(str(e))) @@ -131,20 +132,23 @@ def run(config, source_code=None, user_funcs=None): persist_helper = None init_succeed = False mod_handler = ModHandler() + trading_parameters_update_args = {} try: # avoid register handlers everytime # when running in ipython set_loggers(config) - init_rqdatac(getattr(config.base, 'rqdatac_uri', None)) + is_init = init_rqdatac(getattr(config.base, 'rqdatac_uri', None)) system_log.debug("\n" + pformat(config.convert_to_dict())) env.set_strategy_loader(init_strategy_loader(env, source_code, user_funcs, config)) mod_handler.set_env(env) mod_handler.start_up() + if config.mod.sys_transaction_cost.time_series_trading_parameters and "FUTURE" in config.base.accounts and is_init: + trading_parameters_update_args = {"end_date": config.base.end_date} if not env.data_source: - env.set_data_source(BaseDataSource(config.base.data_bundle_path, getattr(config.base, "future_info", {}))) + env.set_data_source(BaseDataSource(config.base.data_bundle_path, getattr(config.base, "future_info", {}), trading_parameters_update_args)) if env.price_board is None: from rqalpha.data.bar_dict_price_board import BarDictPriceBoard env.price_board = BarDictPriceBoard() diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py b/rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py index 81a3cfdfc..8fa89cd5f 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py @@ -222,10 +222,13 @@ class FuturePosition(Position): @cached_property def contract_multiplier(self): return self._instrument.contract_multiplier - + @cached_property def margin_rate(self): - return self._instrument.margin_rate * self._env.config.base.margin_multiplier + if self.direction == POSITION_DIRECTION.LONG: + return self._instrument.long_margin_ratio(self._env.trading_dt) * self._env.config.base.margin_multiplier + elif self.direction == POSITION_DIRECTION.SHORT: + return self._instrument.short_margin_ratio(self._env.trading_dt) * self._env.config.base.margin_multiplier @property def equity(self): diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py index 7830b0686..645fe6357 100644 --- a/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py @@ -24,7 +24,7 @@ def is_cash_enough(env, order, cash, warn=False): instrument = env.data_proxy.instrument(order.order_book_id) - cost_money = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction) + cost_money = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction, order.datetime) cost_money += env.get_order_transaction_cost(order) if cost_money <= cash: return True diff --git a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py index fd5c8925f..c8d119c27 100644 --- a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py +++ b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py @@ -26,6 +26,8 @@ "futures_commission_multiplier": 1, # 印花倍率,即在默认的印花税基础上按该倍数进行调整,股票默认印花税为千分之一,单边收取 "tax_multiplier": 1, + # 是否开启期货历史交易参数进行回测,默认为True + "time_series_trading_parameters": True, } cli_prefix = "mod__sys_transaction_cost__" diff --git a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/deciders.py b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/deciders.py index f394b899b..52ed08c46 100644 --- a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/deciders.py +++ b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/deciders.py @@ -94,30 +94,29 @@ def __init__(self, commission_multiplier): self.env = Environment.get_instance() - def _get_commission(self, order_book_id, position_effect, price, quantity, close_today_quantity): - info = self.env.data_proxy.get_commission_info(order_book_id) + def _get_commission(self, order_book_id, position_effect, price, quantity, close_today_quantity, dt): + info = self.env.data_proxy.get_futures_trading_parameters(order_book_id, dt) commission = 0 - if info['commission_type'] == COMMISSION_TYPE.BY_MONEY: + if info.commission_type == COMMISSION_TYPE.BY_MONEY: contract_multiplier = self.env.get_instrument(order_book_id).contract_multiplier if position_effect == POSITION_EFFECT.OPEN: - commission += price * quantity * contract_multiplier * info[ - 'open_commission_ratio'] + commission += price * quantity * contract_multiplier * info.open_commission_ratio else: commission += price * ( quantity - close_today_quantity - ) * contract_multiplier * info['close_commission_ratio'] - commission += price * close_today_quantity * contract_multiplier * info['close_commission_today_ratio'] + ) * contract_multiplier * info.close_commission_ratio + commission += price * close_today_quantity * contract_multiplier * info.close_commission_today_ratio else: if position_effect == POSITION_EFFECT.OPEN: - commission += quantity * info['open_commission_ratio'] + commission += quantity * info.open_commission_ratio else: - commission += (quantity - close_today_quantity) * info['close_commission_ratio'] - commission += close_today_quantity * info['close_commission_today_ratio'] + commission += (quantity - close_today_quantity) * info.close_commission_ratio + commission += close_today_quantity * info.close_commission_today_ratio return commission * self.commission_multiplier def get_trade_commission(self, trade): return self._get_commission( - trade.order_book_id, trade.position_effect, trade.last_price, trade.last_quantity, trade.close_today_amount + trade.order_book_id, trade.position_effect, trade.last_price, trade.last_quantity, trade.close_today_amount, trade.trading_datetime ) def get_trade_tax(self, trade): @@ -127,5 +126,5 @@ def get_order_transaction_cost(self, order): close_today_quantity = order.quantity if order.position_effect == POSITION_EFFECT.CLOSE_TODAY else 0 return self._get_commission( - order.order_book_id, order.position_effect, order.frozen_price, order.quantity, close_today_quantity + order.order_book_id, order.position_effect, order.frozen_price, order.quantity, close_today_quantity, order.trading_datetime ) diff --git a/rqalpha/model/instrument.py b/rqalpha/model/instrument.py index eb08e43e2..19502b1f5 100644 --- a/rqalpha/model/instrument.py +++ b/rqalpha/model/instrument.py @@ -46,10 +46,12 @@ def _fix_date(ds, dflt=None) -> datetime: __repr__ = property_repr - def __init__(self, dic, future_tick_size_getter=None): - # type: (Dict, Optional[Callable[[Instrument], float]]) -> None + def __init__(self, dic, future_tick_size_getter=None, future_long_margin_ratio_getter=None, future_short_margin_ratio_getter=None): + # type: (Dict, Optional[Callable[[Instrument], float]], Optional[Callable[[Instrument], float]], Optional[Callable[[Instrument], float]]) -> None self.__dict__ = copy.copy(dic) self._future_tick_size_getter = future_tick_size_getter + self._future_long_margin_ratio_getter = future_long_margin_ratio_getter + self._future_short_margin_ratio_getter = future_short_margin_ratio_getter if "listed_date" in dic: self.__dict__["listed_date"] = self._fix_date(dic["listed_date"]) @@ -240,7 +242,7 @@ def margin_rate(self): """ [float] 合约最低保证金率(期货专用) """ - return self.__dict__.get("margin_rate", 1) + return self.__dict__.get('margin_rate', 1) @property def underlying_order_book_id(self): @@ -448,13 +450,31 @@ def tick_size(self): else: raise NotImplementedError - def calc_cash_occupation(self, price, quantity, direction): - # type: (float, float, POSITION_DIRECTION) -> float + def long_margin_ratio(self, dt): + # type: (datetime) -> float + if self.type == INSTRUMENT_TYPE.FUTURE: + return self._future_long_margin_ratio_getter(self, dt) + else: + return 1 + + def short_margin_ratio(self, dt): + # type: (datetime) -> float + if self.type == INSTRUMENT_TYPE.FUTURE: + return self._future_short_margin_ratio_getter(self, dt) + else: + return 1 + + def calc_cash_occupation(self, price, quantity, direction, dt): + # type: (float, float, float, POSITION_DIRECTION) -> float if self.type in INST_TYPE_IN_STOCK_ACCOUNT: return price * quantity elif self.type == INSTRUMENT_TYPE.FUTURE: margin_multiplier = Environment.get_instance().config.base.margin_multiplier - return price * quantity * self.contract_multiplier * self.margin_rate * margin_multiplier + if direction == POSITION_DIRECTION.LONG: + margin_rate = self.long_margin_ratio(dt) + elif direction == POSITION_DIRECTION.SHORT: + margin_rate = self.short_margin_ratio(dt) + return price * quantity * self.contract_multiplier * margin_rate * margin_multiplier else: raise NotImplementedError diff --git a/rqalpha/portfolio/account.py b/rqalpha/portfolio/account.py index d240d0317..961669af7 100644 --- a/rqalpha/portfolio/account.py +++ b/rqalpha/portfolio/account.py @@ -443,7 +443,7 @@ def _on_bar(self, _): def _frozen_cash_of_order(self, order): if order.position_effect == POSITION_EFFECT.OPEN: instrument = self._env.data_proxy.instrument(order.order_book_id) - order_cost = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction) + order_cost = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction, order.datetime) else: order_cost = 0 return order_cost + self._env.get_order_transaction_cost(order) diff --git a/rqalpha/utils/testing/fixtures.py b/rqalpha/utils/testing/fixtures.py index a818bc733..0c4124ca6 100644 --- a/rqalpha/utils/testing/fixtures.py +++ b/rqalpha/utils/testing/fixtures.py @@ -83,7 +83,7 @@ def init_fixture(self): super(BaseDataSourceFixture, self).init_fixture() default_bundle_path = os.path.abspath(os.path.expanduser('~/.rqalpha/bundle')) - self.base_data_source = BaseDataSource(default_bundle_path, {}) + self.base_data_source = BaseDataSource(default_bundle_path, {}, {}) class BarDictPriceBoardFixture(EnvironmentFixture): diff --git a/tests/api_tests/mod/sys_simulation/test_simulation_event_source.py b/tests/api_tests/mod/sys_simulation/test_simulation_event_source.py index a9b05101d..091d081a7 100644 --- a/tests/api_tests/mod/sys_simulation/test_simulation_event_source.py +++ b/tests/api_tests/mod/sys_simulation/test_simulation_event_source.py @@ -18,8 +18,8 @@ __config__ = { "base": { - "start_date": "2015-04-10", - "end_date": "2015-04-20", + "start_date": "2015-04-14", + "end_date": "2015-04-24", "frequency": "1d", "accounts": { "stock": 1000000, diff --git a/tests/api_tests/mod/sys_transaction_cost/test_commission_multiplier.py b/tests/api_tests/mod/sys_transaction_cost/test_commission_multiplier.py index 4855a90aa..7cc8f8ebd 100644 --- a/tests/api_tests/mod/sys_transaction_cost/test_commission_multiplier.py +++ b/tests/api_tests/mod/sys_transaction_cost/test_commission_multiplier.py @@ -49,7 +49,7 @@ def handle_bar(context, bar_dict): future_commission_info = env.data_proxy.get_commission_info(context.s2) context.fixed = False assert stock_order.transaction_cost == 16.66 * 59900 * 8 / 10000 * 2 - assert future_order.transaction_cost == 7308 * 200 * future_commission_info["open_commission_ratio"] * 3 + assert future_order.transaction_cost == 7308 * 200 * future_commission_info.open_commission_ratio * 3 return locals() diff --git a/tests/outs/test_f_mean_reverting.pkl b/tests/outs/test_f_mean_reverting.pkl index 120ba74fe..b1c1c0923 100644 Binary files a/tests/outs/test_f_mean_reverting.pkl and b/tests/outs/test_f_mean_reverting.pkl differ diff --git a/tests/test_f_buy_and_hold.py b/tests/test_f_buy_and_hold.py index 5fd2ec4dc..ab23e5909 100644 --- a/tests/test_f_buy_and_hold.py +++ b/tests/test_f_buy_and_hold.py @@ -27,5 +27,8 @@ def handle_bar(context, bar_dict): "enabled": True, "show": True, }, + "sys_transaction_cost": { + 'time_series_trading_parameters': False + }, }, } diff --git a/tests/test_f_mean_reverting.py b/tests/test_f_mean_reverting.py index 5cb40c66a..43654f5eb 100644 --- a/tests/test_f_mean_reverting.py +++ b/tests/test_f_mean_reverting.py @@ -139,5 +139,8 @@ def handle_bar(context, bar_dict): "enabled": True, "show": True, }, + 'sys_transaction_cost': { + 'time_series_trading_parameters': False + }, }, }