diff --git a/qlib/backtest/exchange.py b/qlib/backtest/exchange.py index eeee269bd..43b2e95d6 100644 --- a/qlib/backtest/exchange.py +++ b/qlib/backtest/exchange.py @@ -84,7 +84,7 @@ class Exchange: such as DayCumsum. !!!NOTE: if you want you use the custom operator, you need to register it in qlib_init. - "cum" means that this is a cumulative value over time, such as cumulative market volume. - So when it is used as a volume limit, it is necessary to subtract the dealed amount. + So when it is used as a volume limit, it is necessary to subtract the dealt amount. - "current" means that this is a real-time value and will not accumulate over time, so it can be directly used as a capacity limit. e.g. ("cum", "0.2 * DayCumsum($volume, '9:45', '14:45')"), ("current", "$bidV1") @@ -304,10 +304,10 @@ class Exchange: if isinstance(volume_threshold, tuple): volume_threshold = {"all": volume_threshold} - assert type(volume_threshold) == dict + assert isinstance(volume_threshold, dict) for key in volume_threshold: vol_limit = volume_threshold[key] - assert type(vol_limit) == tuple + assert isinstance(vol_limit, tuple) fields.add(vol_limit[1]) if key in ("buy", "all"): @@ -370,7 +370,7 @@ class Exchange: order, trade_account: Account = None, position: BasePosition = None, - dealed_order_amount: defaultdict = defaultdict(float), + dealt_order_amount: defaultdict = defaultdict(float), ): """ Deal order when the actual transaction @@ -380,7 +380,7 @@ class Exchange: :param order: Deal the order. :param trade_account: Trade account to be updated after dealing the order. :param position: position to be updated after dealing the order. - :param dealed_order_amount: the dealed order amount dict with the format of {stock_id: float} + :param dealt_order_amount: the dealt order amount dict with the format of {stock_id: float} :return: trade_val, trade_cost, trade_price """ # check order first. @@ -395,7 +395,7 @@ class Exchange: trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, order.direction) # NOTE: order will be changed in this function trade_val, trade_cost = self._calc_trade_info_by_order( - order, trade_account.current if trade_account else position, dealed_order_amount + order, trade_account.current if trade_account else position, dealt_order_amount ) if order.deal_amount > 1e-5: # If the order can only be deal 0 amount. Nothing to be updated @@ -659,15 +659,15 @@ 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, dealed_order_amount: dict) -> int: + def _get_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. Parameters ---------- order : Order the order to be executed. - dealed_order_amount : dict - :param dealed_order_amount: the dealed order amount dict with the format of {stock_id: float} + dealt_order_amount : dict + :param dealt_order_amount: the dealt order amount dict with the format of {stock_id: float} Returns ------- @@ -685,33 +685,23 @@ 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": - vol_limit_num.append( - 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) elif limit[0] == "cum": - vol_limit_num.append( - self.quote.get_data( - order.stock_id, - order.start_time, - order.end_time, - fields=limit[1], - method=ts_data_last, - ) - - dealed_order_amount[order.stock_id] - ) + 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) - def _calc_trade_info_by_order(self, order, position: Position, dealed_order_amount): + def _calc_trade_info_by_order(self, order, position: Position, dealt_order_amount): """ Calculation of trade info @@ -719,7 +709,7 @@ class Exchange: :param order: :param position: Position - :param dealed_order_amount: the dealed order amount dict with the format of {stock_id: float} + :param dealt_order_amount: the dealt order amount dict with the format of {stock_id: float} :return: trade_val, trade_cost """ @@ -743,7 +733,7 @@ class Exchange: # We choose to sell all order.deal_amount = order.amount - order.deal_amount = self._get_amount_by_volume(order, dealed_order_amount) + order.deal_amount = self._get_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: @@ -763,7 +753,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, dealed_order_amount) + order.deal_amount = self._get_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/backtest/executor.py b/qlib/backtest/executor.py index 6b44bd1b7..e7882714a 100644 --- a/qlib/backtest/executor.py +++ b/qlib/backtest/executor.py @@ -115,7 +115,7 @@ class BaseExecutor: get_module_logger("BaseExecutor").warning(f"`common_infra` is not set for {self}") # record deal order amount in one day - self.dealed_order_amount = defaultdict(float) + self.dealt_order_amount = defaultdict(float) self.deal_day = None def reset_common_infra(self, common_infra): @@ -500,14 +500,14 @@ class SimulatorExecutor(BaseExecutor): raise NotImplementedError(f"This type of input is not supported") return order_it - def _update_dealed_order_amount(self, order): - """update date and dealed order amount in the day.""" + def _update_dealt_order_amount(self, order): + """update date and dealt order amount in the day.""" now_deal_day = self.trade_calendar.get_step_time()[0].floor(freq="D") if self.deal_day is None or now_deal_day > self.deal_day: - self.dealed_order_amount = defaultdict(float) + self.dealt_order_amount = defaultdict(float) self.deal_day = now_deal_day - self.dealed_order_amount[order.stock_id] += order.deal_amount + self.dealt_order_amount[order.stock_id] += order.deal_amount def _collect_data(self, trade_decision: BaseTradeDecision, level: int = 0): @@ -520,10 +520,10 @@ class SimulatorExecutor(BaseExecutor): trade_val, trade_cost, trade_price = self.trade_exchange.deal_order( order, trade_account=self.trade_account, - dealed_order_amount=self.dealed_order_amount, + dealt_order_amount=self.dealt_order_amount, ) execute_result.append((order, trade_val, trade_cost, trade_price)) - self._update_dealed_order_amount(order) + self._update_dealt_order_amount(order) if self.verbose: print( "[I {:%Y-%m-%d %H:%M:%S}]: {} {}, price {:.2f}, amount {}, deal_amount {}, factor {}, value {:.2f}, cash {:.2f}.".format( diff --git a/qlib/contrib/ops/high_freq.py b/qlib/contrib/ops/high_freq.py index 6f03b71cf..3ce5c961f 100644 --- a/qlib/contrib/ops/high_freq.py +++ b/qlib/contrib/ops/high_freq.py @@ -10,6 +10,7 @@ from qlib.data import D from qlib.data.cache import H from qlib.data.data import Cal from qlib.data.ops import ElemOperator +from qlib.utils.time import time_to_day_index def get_calendar_day(freq="1min", future=False): @@ -71,18 +72,11 @@ class DayCumsum(ElemOperator): self.noon_open = datetime.strptime("13:00", "%H:%M") self.noon_close = datetime.strptime("15:00", "%H:%M") - self.start_id = self.time_to_index(self.start) - self.end_id = self.time_to_index(self.end) - - def time_to_index(self, t): - if t >= self.morning_open and t < self.morning_close: - return int((t - self.morning_open).total_seconds() / 60) - elif t >= self.noon_open and t < self.noon_close: - return int((t - self.noon_open).total_seconds() / 60) + 120 - else: - raise ValueError(f"{t} is not the opening time of the stock market") + self.start_id = time_to_day_index(self.start) + self.end_id = time_to_day_index(self.end) def period_cusum(self, df): + df = df.copy() assert len(df) == 240 df.iloc[0 : self.start_id] = 0 df = df.cumsum() diff --git a/qlib/utils/time.py b/qlib/utils/time.py index e365de6d8..54d30a9aa 100644 --- a/qlib/utils/time.py +++ b/qlib/utils/time.py @@ -11,6 +11,7 @@ from numpy import append import pandas as pd from qlib.config import C import functools +from typing import Union @functools.lru_cache(maxsize=240) @@ -96,6 +97,29 @@ 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")] +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"): + if isinstance(time_obj, str): + time_obj = datetime.strptime(time_obj, "%H:%M") + + if region == "cn": + if time_obj >= cn_time[0] and time_obj < cn_time[1]: + return int((time_obj - cn_time[0]).total_seconds() / 60) + elif time_obj >= cn_time[2] and time_obj < cn_time[3]: + return int((time_obj - cn_time[2]).total_seconds() / 60) + 120 + else: + raise ValueError(f"{time_obj} is not the opening time of the {region} stock market") + elif region == "us": + if time_obj >= us_time[0] and time_obj < us_time[1]: + return int((time_obj - us_time[0]).total_seconds() / 60) + else: + raise ValueError(f"{time_obj} is not the opening time of the {region} stock market") + else: + raise ValueError(f"{region} is not supported") + + def get_day_min_idx_range(start: str, end: str, freq: str) -> Tuple[int, int]: """ get the min-bar index in a day for a time range (both left and right is closed) given a fixed frequency