diff --git a/qlib/backtest/backtest.py b/qlib/backtest/backtest.py index 5c9948b1b..c707aa1f0 100644 --- a/qlib/backtest/backtest.py +++ b/qlib/backtest/backtest.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from qlib.strategy.base import BaseStrategy -from qlib.backtest.executor import BaseExecutor + from qlib.backtest.executor import BaseExecutor from ..utils.time import Freq from tqdm.auto import tqdm diff --git a/qlib/backtest/exchange.py b/qlib/backtest/exchange.py index 43b2e95d6..0aab35e67 100644 --- a/qlib/backtest/exchange.py +++ b/qlib/backtest/exchange.py @@ -137,9 +137,10 @@ class Exchange: if deal_price is None: deal_price = C.deal_price - self.logger = get_module_logger("online operator", level=logging.INFO) + # we have some verbose information here. So logging is enable + self.logger = get_module_logger("online operator") - # TODO: the quote, trade_dates, codes are not necessray. + # TODO: the quote, trade_dates, codes are not necessary. # It is just for performance consideration. self.limit_type = self._get_limit_type(limit_threshold) if limit_threshold is None: @@ -387,6 +388,7 @@ class Exchange: if self.check_order(order) is False: order.deal_amount = 0.0 # using np.nan instead of None to make it more convenient to should the value in format string + self.logger.debug(f"Order failed due to trading limitation: {order}") return 0.0, 0.0, np.nan if trade_account is not None and position is not None: @@ -659,20 +661,19 @@ class Exchange: return (deal_amount * factor + 0.1) // self.trade_unit * self.trade_unit / factor return deal_amount - def _get_amount_by_volume(self, order: Order, dealt_order_amount: dict) -> int: + def _clip_amount_by_volume(self, order: Order, dealt_order_amount: dict) -> int: """parse the capacity limit string and return the actual amount of orders that can be executed. + NOTE: + this function will change the order.deal_amount **inplace** + - This will make the order info more accurate + Parameters ---------- order : Order the order to be executed. dealt_order_amount : dict :param dealt_order_amount: the dealt order amount dict with the format of {stock_id: float} - - Returns - ------- - int - the actual amount of orders that can be executed, due to the volume limit. """ if order.direction == Order.BUY: vol_limit = self.buy_vol_limit @@ -685,21 +686,33 @@ class Exchange: vol_limit_num = [] for limit in vol_limit: assert isinstance(limit, tuple) - limit_value = self.quote.get_data( - order.stock_id, - order.start_time, - order.end_time, - fields=limit[1], - method=ts_data_last, - ) if limit[0] == "current": + limit_value = self.quote.get_data( + order.stock_id, + order.start_time, + order.end_time, + fields=limit[1], + method="sum", + ) vol_limit_num.append(limit_value) elif limit[0] == "cum": + limit_value = self.quote.get_data( + order.stock_id, + order.start_time, + order.end_time, + fields=limit[1], + method=ts_data_last, + ) vol_limit_num.append(limit_value - dealt_order_amount[order.stock_id]) else: raise ValueError(f"{limit[0]} is not supported") - vol_limit_num = min(vol_limit_num) - return max(min(vol_limit_num, order.deal_amount), 0) + vol_limit_min = min(vol_limit_num) + orig_deal_amount = order.deal_amount + order.deal_amount = max(min(vol_limit_min, orig_deal_amount), 0) + if vol_limit_min < orig_deal_amount: + self.logger.debug( + f"Order clipped due to volume limitation: {order}, {[(vol, rule) for vol, rule in zip(vol_limit_num, vol_limit)]}" + ) def _calc_trade_info_by_order(self, order, position: Position, dealt_order_amount): """ @@ -733,7 +746,7 @@ class Exchange: # We choose to sell all order.deal_amount = order.amount - order.deal_amount = self._get_amount_by_volume(order, dealt_order_amount) + self._clip_amount_by_volume(order, dealt_order_amount) trade_val = order.deal_amount * trade_price trade_cost = max(trade_val * self.close_cost, self.min_cost) elif order.direction == Order.BUY: @@ -746,6 +759,7 @@ class Exchange: order.deal_amount = self.round_amount_by_trade_unit( cash / (1 + self.open_cost) / trade_price, order.factor ) + self.logger.debug(f"Order clipped due to cash limitation: {order}") else: # THe money is enough order.deal_amount = self.round_amount_by_trade_unit(order.amount, order.factor) @@ -753,7 +767,7 @@ class Exchange: # Unknown amount of money. Just round the amount order.deal_amount = self.round_amount_by_trade_unit(order.amount, order.factor) - order.deal_amount = self._get_amount_by_volume(order, dealt_order_amount) + self._clip_amount_by_volume(order, dealt_order_amount) trade_val = order.deal_amount * trade_price trade_cost = max(trade_val * self.open_cost, self.min_cost) else: diff --git a/qlib/config.py b/qlib/config.py index 7ed0aeed3..18984c7ce 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -273,7 +273,24 @@ class QlibConfig(Config): else: raise NotImplementedError(f"This type of uri is not supported") - def set(self, default_conf="client", **kwargs): + def set(self, default_conf: str = "client", **kwargs): + """ + configure qlib based on the input parameters + + The configure will act like a dictionary. + + Normally, it literally replace the value according to the keys. + However, sometimes it is hard for users to set the config when the configure is nested and complicated + + So this API provides some special parameters for users to set the keys in a more convenient way. + - region: REG_CN, REG_US + - several region-related config will be changed + + Parameters + ---------- + default_conf : str + the default config template chosen by user: "server", "client" + """ from .utils import set_log_with_config, get_module_logger, can_use_cache self.reset() diff --git a/qlib/utils/time.py b/qlib/utils/time.py index 54d30a9aa..f61c825d2 100644 --- a/qlib/utils/time.py +++ b/qlib/utils/time.py @@ -97,10 +97,16 @@ class Freq: return _count, _freq_format_dict[_freq] -cn_time = [datetime.strptime("9:30", "%H:%M"), datetime.strptime("11:30", "%H:%M"), - datetime.strptime("13:00", "%H:%M"), datetime.strptime("15:00", "%H:%M")] +cn_time = [ + datetime.strptime("9:30", "%H:%M"), + datetime.strptime("11:30", "%H:%M"), + datetime.strptime("13:00", "%H:%M"), + datetime.strptime("15:00", "%H:%M"), +] us_time = [datetime.strptime("9:30", "%H:%M"), datetime.strptime("16:00", "%H:%M")] -def time_to_day_index(time_obj: Union[str, datetime], region: str="cn"): + + +def time_to_day_index(time_obj: Union[str, datetime], region: str = "cn"): if isinstance(time_obj, str): time_obj = datetime.strptime(time_obj, "%H:%M")