diff --git a/qlib/backtest/exchange.py b/qlib/backtest/exchange.py index 58f57ed73..a22754885 100644 --- a/qlib/backtest/exchange.py +++ b/qlib/backtest/exchange.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. +from qlib.backtest.position import Position import random import logging from typing import List, Tuple, Union @@ -281,6 +282,8 @@ class Exchange: """ Deal order when the actual transaction + the results section in `Order` will be changed. + :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. @@ -343,6 +346,7 @@ class Exchange: `None`: if the stock is suspended `None` may be returned `float`: return factor if the factor exists """ + assert (start_time is not None and end_time is not None, "the time range must be given") if stock_id not in self.quote: return None return resam_ts_data(self.quote[stock_id]["$factor"], start_time, end_time, method=ts_data_last) @@ -505,20 +509,56 @@ class Exchange: ) return value - def get_amount_of_trade_unit(self, factor): + def _get_factor_or_raise_erorr(self, factor: float = None, stock_id: str = None, start_time=None, end_time=None): + """Please refer to the docs of get_amount_of_trade_unit""" + if factor is None: + if stock_id is not None and start_time is not None and end_time is not None : + factor = self.get_factor(stock_id=stock_id, start_time=start_time, end_time=end_time) + else: + raise ValueError(f"`factor` and (`stock_id`, `start_time`, `end_time`) can't both be None") + return factor + + def get_amount_of_trade_unit(self, factor: float = None, stock_id: str = None, start_time=None, end_time=None): + """ + get the trade unit of amount based on **factor** + + the factor can be given directly or calculated in given time range and stock id. + `factor` has higher priority than `stock_id`, `start_time` and `end_time` + + Parameters + ---------- + factor : float + the adjusted factor + stock_id : str + the id of the stock + start_time : + the start time of trading range + end_time : + the end time of trading range + """ if not self.trade_w_adj_price and self.trade_unit is not None: + factor = self._get_factor_or_raise_erorr(factor=factor, + stock_id=stock_id, + start_time=start_time, + end_time=end_time) return self.trade_unit / factor else: return None - def round_amount_by_trade_unit(self, deal_amount, factor): + def round_amount_by_trade_unit(self, deal_amount, factor: float = None, stock_id: str = None, start_time=None, end_time=None): """Parameter + Please refer to the docs of get_amount_of_trade_unit + deal_amount : float, adjusted amount factor : float, adjusted factor return : float, real amount """ if not self.trade_w_adj_price and self.trade_unit is not None: # the minimal amount is 1. Add 0.1 for solving precision problem. + factor = self._get_factor_or_raise_erorr(factor=factor, + stock_id=stock_id, + start_time=start_time, + end_time=end_time) return (deal_amount * factor + 0.1) // self.trade_unit * self.trade_unit / factor return deal_amount @@ -529,7 +569,7 @@ class Exchange: else: return deal_amount - def _calc_trade_info_by_order(self, order, position): + def _calc_trade_info_by_order(self, order, position: Position): """ Calculation of trade info @@ -541,6 +581,7 @@ class Exchange: """ trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, direction=order.direction) + order.factor = self.get_factor(order.stock_id, order.start_time, order.end_time) if order.direction == Order.SELL: # sell if position is not None: diff --git a/qlib/backtest/order.py b/qlib/backtest/order.py index 5c6a85cf0..816bb6fa0 100644 --- a/qlib/backtest/order.py +++ b/qlib/backtest/order.py @@ -43,16 +43,24 @@ class Order: presents the weight factor assigned in Exchange() """ + # 1) time invariant values + # - they are set by users and is time-invariant. stock_id: str - amount: float # `amount` is a non-negative value + amount: float # `amount` is a non-negative and adjusted value + direction: int + # 2) time variant values: + # - Users may want to set these values when using lower level APIs + # - If users don't, TradeDecisionWO will help users to set them # The interval of the order which belongs to (NOTE: this is not the expected order dealing range time) start_time: pd.Timestamp end_time: pd.Timestamp - direction: int - factor: float + # 3) results + # - users should not care about these values + # - they are set by the backtest system after finishing the results. deal_amount: Optional[float] = None # `deal_amount` is a non-negative value + factor: Optional[float] = None # FIXME: # for compatible now. @@ -145,9 +153,9 @@ class OrderHelper: **adjusted trading amount** direction : OrderDir trading direction - start_time : Union[str, pd.Timestamp] + start_time : Union[str, pd.Timestamp] (optional) The interval of the order which belongs to - end_time : Union[str, pd.Timestamp] + end_time : Union[str, pd.Timestamp] (optional) The interval of the order which belongs to Returns @@ -159,13 +167,13 @@ class OrderHelper: start_time = pd.Timestamp(start_time) if end_time is not None: end_time = pd.Timestamp(end_time) + # NOTE: factor is a value belongs to the results section. User don't have to care about it when creating orders return Order( stock_id=code, amount=amount, start_time=start_time, end_time=end_time, direction=direction, - factor=self.exchange.get_factor(code, start_time, end_time), ) diff --git a/qlib/contrib/evaluate.py b/qlib/contrib/evaluate.py index f7728f911..e08039413 100644 --- a/qlib/contrib/evaluate.py +++ b/qlib/contrib/evaluate.py @@ -73,20 +73,20 @@ def indicator_analysis(df, method="mean"): Parameters ---------- df : pandas.DataFrame - columns: like ['pa', 'pos', 'ffr', 'amount', 'value']. + columns: like ['pa', 'pos', 'ffr', 'deal_amount', 'value']. Necessary fields: - 'pa' is the price advantage in trade indicators - 'pos' is the positive rate in trade indicators - 'ffr' is the fulfill rate in trade indicators Optional fields: - - 'amount' is the total deal amount, only necessary when method is 'amount_weighted' + - 'deal_amount' is the total deal deal_amount, only necessary when method is 'amount_weighted' - 'value' is the total trade value, only necessary when method is 'value_weighted' index: Index(datetime) method : str, optional statistics method of pa/ffr, by default "mean" - if method is 'mean', count the mean statistical value of each trade indicator - - if method is 'amount_weighted', count the amount weighted mean statistical value of each trade indicator + - if method is 'amount_weighted', count the deal_amount weighted mean statistical value of each trade indicator - if method is 'value_weighted', count the value weighted mean statistical value of each trade indicator Note: statistics method of pos is always "mean" @@ -97,7 +97,7 @@ def indicator_analysis(df, method="mean"): """ weights_dict = { "mean": df["count"], - "amount_weighted": df["amount"].abs(), + "amount_weighted": df["deal_amount"].abs(), "value_weighted": df["value"].abs(), } if method not in weights_dict: diff --git a/qlib/contrib/strategy/model_strategy.py b/qlib/contrib/strategy/model_strategy.py index 17a13e155..48d96686a 100644 --- a/qlib/contrib/strategy/model_strategy.py +++ b/qlib/contrib/strategy/model_strategy.py @@ -194,7 +194,6 @@ class TopkDropoutStrategy(ModelStrategy): start_time=trade_start_time, end_time=trade_end_time, direction=Order.SELL, # 0 for sell, 1 for buy - factor=factor, ) # is order executable if self.trade_exchange.check_order(sell_order): @@ -231,7 +230,6 @@ class TopkDropoutStrategy(ModelStrategy): start_time=trade_start_time, end_time=trade_end_time, direction=Order.BUY, # 1 for buy - factor=factor, ) buy_order_list.append(buy_order) return TradeDecisionWO(sell_order_list + buy_order_list, self) diff --git a/qlib/contrib/strategy/rule_strategy.py b/qlib/contrib/strategy/rule_strategy.py index c30289daf..1ec054e45 100644 --- a/qlib/contrib/strategy/rule_strategy.py +++ b/qlib/contrib/strategy/rule_strategy.py @@ -63,7 +63,9 @@ class TWAPStrategy(BaseStrategy): stock_id=order.stock_id, start_time=trade_start_time, end_time=trade_end_time ): continue - _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(order.factor) + _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(stock_id=order.stock_id, + start_time=order.start_time, + end_time=order.end_time) _order_amount = None # considering trade unit if _amount_trade_unit is None: @@ -99,7 +101,6 @@ class TWAPStrategy(BaseStrategy): start_time=trade_start_time, end_time=trade_end_time, direction=order.direction, # 1 for buy - factor=order.factor, ) order_list.append(_order) return TradeDecisionWO(order_list=order_list, strategy=self) @@ -168,7 +169,9 @@ class SBBStrategyBase(BaseStrategy): self.trade_trend[order.stock_id] = _pred_trend continue # get amount of one trade unit - _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(order.factor) + _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(stock_id=order.stock_id, + start_time=order.start_time, + end_time=order.end_time) if _pred_trend == self.TREND_MID: _order_amount = None # considering trade unit @@ -201,7 +204,6 @@ class SBBStrategyBase(BaseStrategy): start_time=trade_start_time, end_time=trade_end_time, direction=order.direction, - factor=order.factor, ) order_list.append(_order) @@ -248,7 +250,6 @@ class SBBStrategyBase(BaseStrategy): start_time=trade_start_time, end_time=trade_end_time, direction=order.direction, # 1 for buy - factor=order.factor, ) order_list.append(_order) else: @@ -267,7 +268,6 @@ class SBBStrategyBase(BaseStrategy): start_time=trade_start_time, end_time=trade_end_time, direction=order.direction, # 1 for buy - factor=order.factor, ) order_list.append(_order) @@ -471,7 +471,9 @@ class ACStrategy(BaseStrategy): if sig_sam is None or np.isnan(sig_sam): # no signal, TWAP - _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(order.factor) + _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(stock_id=order.stock_id, + start_time=order.start_time, + end_time=order.end_time) if _amount_trade_unit is None: # divide the order into equal parts, and trade one part _order_amount = self.trade_amount[order.stock_id] / (trade_len - trade_step) @@ -492,7 +494,10 @@ class ACStrategy(BaseStrategy): np.sinh(kappa * (trade_len - trade_step)) - np.sinh(kappa * (trade_len - trade_step - 1)) ) / np.sinh(kappa * trade_len) _order_amount = order.amount * amount_ratio - _order_amount = self.trade_exchange.round_amount_by_trade_unit(_order_amount, order.factor) + _order_amount = self.trade_exchange.round_amount_by_trade_unit(_order_amount, + stock_id=order.stock_id, + start_time=order.start_time, + end_time=order.end_time) if order.direction == order.SELL: # sell all amount at last