From 1525ecfb7b6b9f8d779c22d6b61d6af203f7e6e4 Mon Sep 17 00:00:00 2001 From: "lin.dongzhao" <542698096@qq.com> Date: Wed, 27 Dec 2023 11:50:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=9F=E8=B4=A7=E5=9B=9E=E6=B5=8B=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=97=B6=E5=BA=8F=E4=BA=A4=E6=98=93=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rqalpha/data/base_data_source/data_source.py | 32 +++- rqalpha/data/base_data_source/storages.py | 122 ++++++++++++-- rqalpha/data/bundle.py | 157 +++++++++++++++++- rqalpha/data/data_proxy.py | 4 + rqalpha/interface.py | 8 +- rqalpha/main.py | 8 +- .../position_model.py | 7 +- .../validators/cash_validator.py | 2 +- .../__init__.py | 2 + .../deciders.py | 23 ++- rqalpha/model/instrument.py | 32 +++- rqalpha/portfolio/account.py | 2 +- rqalpha/utils/testing/fixtures.py | 2 +- .../test_simulation_event_source.py | 4 +- .../test_commission_multiplier.py | 2 +- tests/outs/test_f_mean_reverting.pkl | Bin 111814 -> 111839 bytes tests/test_f_buy_and_hold.py | 3 + tests/test_f_mean_reverting.py | 3 + 18 files changed, 365 insertions(+), 48 deletions(-) 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 120ba74fe9285103ef16c51de544d52804ef42e0..b1c1c0923e2265062d676e06150b25565872d0e6 100644 GIT binary patch delta 8900 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 UOGvN{3lM1}_y2RnxIeuAA5-=$;s5{u delta 8808 zcmeHLOHhb@? z`<#3KuluJ*)Bf~mTJJxm#-(D%{OHe9*CyK8F&+=x#pBQn*Q#eR`JPMeb;#nfc*OO=1DE_z$ziMbV0eZ1fE|w~RJzl(I75q1Y4K?-CTKBH zi!-%2ON&WboUO%VA*P8&rD)hoVl?18&-Byy2Y-y?NPI&W3Meu^q0+k4y0)Ue4km)Zi&No^8Qt=*8>1p%uP!9`FjD5|&T{ z=WsMjcrJxghnR-&{L0QO>-|5%-NKnrBP9E7A+~MFmWp33$RfAcjiCf(-jyhgU=SOt zGNmVIgA3J6mPud;2Q#fSxPZ;OV-uZpRxroV_rm_zBIuDJu;JtCY>@#BuE+LV%a$In z`=$H^)>~K~4{a=V*Fu?PCf*{cYIeTtNE?P1k%QfEYLN`eOnR|qfiy~kiRYX$U>ti0 z_d?G?3IP`?pOa4P!7L2Vw`_b^gbf?<%zUZ>FYK9b;muFrdLhg3cAlz~)m@ggY11ZE2Xvn4p#b7YoiG7ooCx;97;XKLM{S4Mqg)lg6cCPF`j>XCRp{5WU zsX90pDZpSOrN-&xsrPrW;9`zs- z&t<5oHXY8ia0~|z!8PoED&3LK;%#5}r~0)OrQ7@#)?hgQb3E~GvV}v_c0fzX%w!81 zn91-`sGZ|e@_rmiv9^DSCviN0bzTKeOH3zp>z-#wVoTAJt`sui>fCVS>kZ*R!4{kK zYXW=VDm=vN&suM7g;UG_06)yNjzI`U_A>l|lB=;lgJ3*f$x7n7cy<0%?XOU&IM)E->|=d;*i)@)vnW9tv(Sexeg@pL}0L%^gBShJTc_~0t8h>=a~Gzc3! zlTMnidD-W}7%xp{Yt;=yaPKn+9#E>{l~lPcF*f?&qWx>*^51=Oi^AO}x9F2w^vNyy z^j5pf6o z56AHbujmYay?SLSy&gsys2%tn)JhXPzd*y!ACn+_|Dklevw#Dver&)SK4%0lfF0VZ z3+5w>V8E|cVZ2g~V0TQah_f15TlsL+o@LgFDZd?ex%ajrv9vWYML_% zZ(^FrKNXp==-(Z@CgWf`o|V|tQi;IW4k=WwCgoNf^@jt+-L{%6POOy`yi~4J!%^46 zI({&mq)$Qwhq8Ry-s=o@?q{Sj=o8pfKivK;u79D#~?USuL zj?Hp`u?x?;wr>E>$z#C2$Fxh0cc4ove0*3h_&WzRz6bBfGxs_4(lLx2mIC9GC%o2b z)dm>6PAS@UKf|+Bd)AvtZjCOojdcmyV>$ zvN4LcAu6CHIl7S;@{(ft+q!EQ!8%f2Ii1=TT|kNl*tZdtB1AF>#o?9gV&r|yTdJVY zt2CO<=>&O~xNVCFh35Aj4^sNN&T-=wrPt3+FE0v%Z>ktAdnFWzWr(<#J;l;l!ONr* zVCR!3wri0x>gD%6-)~0;1;l-O0;HA1-zI9|5{i{=2edw3)z|!P<^k+HzcOYw1xNDm zAvKbxrP{{&J4P_pE|aNLrI2avDp`?-eb15uJAxs(ne3K;z5q}1=nlPV32O*kXD86! z2H#K+Apy7u_l4y`xT!a1Ybv2o4|nB`dHf1$fl;}CyP!^CJ*!lNF9}M+cBR$MzBq~) z(D8~=V~!eS*n!uM|{vQx|c z_XL)c;j$@eyH$jBct=Orx{dVsQBxbT^&0}GduVx!RT>?5R%LvcFub-vtg0;t(Z&~P zZ=i?0{2Z4MS*-Ae!*pogR7lYE!oko=g5&7UEZ9SVlHye5%EqFmf*r~MG4_zT^8Gkf$*wF@!OAlO4Y-Ja?kHA zXR_?*%RPe*V1!WN5SdL~XFm#_ar>P{_(JIOhp#>IE?Nec)EJg2o1W$J6K1l?W$N0k;zxY-<=bL2`qD;!LNp2r>`bb^f&jF{} zxNLv8@|P65qU?9a*})j3o^ldh9Y+dXR{%B>x&+warfx(CgCNw)Bz9p7VP#OKuTz$7 z7sJKd4~Ms@Ho;8wXR;&XzqeyKVIB?P5CuasU?c0F*5?y^M+8r}OlZ;zt|9JJ}H zRV1I3r>`QC(}^R{8xUo+q$tcwS~y(oa>++8G>hM+(6dzNE1SIfeT);z<7gszOmcMx3`9fQzKr&ImPeQ;;BTrlBBh1|&F} zfg~}?W&o2C5?oEH*t9{DhA6n2;f7N+16W6#%&B}5AUK*~`$PUb>b z{NFiclQVMIn3EA_V=hM?kE9L)qM=SYD4c{DHed@DW?+JZ8HnUxCZwpBlMis}W$19$ zWdNLY89>Dh`!U4fT-q<^T!t0rGzK8pmVrpNWk3$uay;{f*`_DpT+1*MG|NB)$uf{h z32};LXmEmM0E%3(`O~l_UoDbX8D^4F84zbvrf)&33?1aG%0M`&;t1b=l>p~ghLYr0 zCgkud$4=2J2N3kiuyUA{qr-_6S6S*HoKzV`4xw^X1f9C{uUDr0W^>kIXgh?%(Ux?> zP!epzKpdK(%{a+0bvVZ`kTjcd3