diff --git a/qlib/backtest/backtest.py b/qlib/backtest/backtest.py index 82397abdb..81395dc73 100644 --- a/qlib/backtest/backtest.py +++ b/qlib/backtest/backtest.py @@ -10,6 +10,8 @@ from tqdm.auto import tqdm def backtest_loop(start_time, end_time, trade_strategy: BaseStrategy, trade_executor: BaseExecutor): """backtest funciton for the interaction of the outermost strategy and executor in the nested decision execution + please refer to the docs of `collect_data_loop` + Returns ------- report: Report @@ -28,8 +30,11 @@ def collect_data_loop(start_time, end_time, trade_strategy: BaseStrategy, trade_ ---------- start_time : pd.Timestamp|str closed start time for backtest + **NOTE**: This will be applied to the outmost executor's calendar. end_time : pd.Timestamp|str closed end time for backtest + **NOTE**: This will be applied to the outmost executor's calendar. + E.g. Executor[day](Executor[1min]), setting `end_time == 20XX0301` will include all the minutes on 20XX0301 trade_strategy : BaseStrategy the outermost portfolio strategy trade_executor : BaseExecutor diff --git a/qlib/backtest/executor.py b/qlib/backtest/executor.py index b6d16d58f..3f7b2f4ed 100644 --- a/qlib/backtest/executor.py +++ b/qlib/backtest/executor.py @@ -3,6 +3,8 @@ import warnings import pandas as pd from typing import Union +from qlib.backtest.report import Indicator + from .order import Order, BaseTradeDecision from .exchange import Exchange from .utils import TradeCalendarManager, CommonInfrastructure, LevelInfrastructure @@ -174,7 +176,7 @@ class BaseExecutor: else: raise ValueError("generate_report should be True if you want to generate report") - def get_trade_indicator(self): + def get_trade_indicator(self) -> Indicator: """get the trade indicator instance, which has pa/pos/ffr info.""" return self.trade_account.indicator @@ -279,7 +281,7 @@ class NestedExecutor(BaseExecutor): trade_decision = updated_trade_decision # NEW UPDATE # create a hook for inner strategy to update outter decision - self.inner_strategy.alter_decision(trade_decision) + self.inner_strategy.alter_outer_trade_decision(trade_decision) _inner_trade_decision = self.inner_strategy.generate_trade_decision(_inner_execute_result) @@ -287,7 +289,7 @@ class NestedExecutor(BaseExecutor): _inner_execute_result = yield from self.inner_executor.collect_data(trade_decision=_inner_trade_decision) execute_result.extend(_inner_execute_result) - inner_order_indicators.append(self.inner_executor.get_trade_indicator().get_order_indicator) + inner_order_indicators.append(self.inner_executor.get_trade_indicator().get_order_indicator()) if hasattr(self, "trade_account"): trade_step = self.trade_calendar.get_trade_step() diff --git a/qlib/backtest/order.py b/qlib/backtest/order.py index d1b5f6d08..6324a9be9 100644 --- a/qlib/backtest/order.py +++ b/qlib/backtest/order.py @@ -56,7 +56,7 @@ class BaseTradeDecision: 2. After a period of time, the decision are updated and become available 3. The inner strategy try to get the decision and start to execute the decision according to `get_range_limit` Case 2: - 1. The strategy is available at the start of the interval + 1. The outer strategy's decision is available at the start of the interval 2. Same as `case 1.3` """ def __init__(self, strategy: BaseStrategy): @@ -133,14 +133,19 @@ class TradeDecisionWO(BaseTradeDecision): def get_range_limit(self) -> Tuple[int, int]: if self.idx_range is None: # Default to get full index - return 0, self.strategy.trade_calendar.get_trade_len() - 1 + raise NotImplementedError(f"The decision didn't provide an index range") return self.idx_range def get_decision(self) -> List[object]: return self.order_list + def __repr__(self) -> str: + return f"strategy: {self.strategy}; idx_range: {self.idx_range}; order_list[{len(self.order_list)}]" + # TODO: the orders below need to be discussed ------------------------------------ +# - The classes below are designed for Case 1 +# - However, Case 1 can't take `order_pool` as the an argument as the constructor function class TradeDecisionWithOrderPool: """trade decision that made by strategy""" diff --git a/qlib/backtest/report.py b/qlib/backtest/report.py index 70ebd724e..3f2649839 100644 --- a/qlib/backtest/report.py +++ b/qlib/backtest/report.py @@ -395,11 +395,9 @@ class Indicator: ) ) - @property def get_order_indicator(self): return self.order_indicator - @property def get_trade_indicator(self): return self.trade_indicator diff --git a/qlib/backtest/utils.py b/qlib/backtest/utils.py index d2441dd3a..720eb627e 100644 --- a/qlib/backtest/utils.py +++ b/qlib/backtest/utils.py @@ -103,6 +103,9 @@ class TradeCalendarManager: """Get the start_time and end_time for trading""" return self.start_time, self.end_time + def __repr__(self) -> str: + return f"{self.start_time}[{self.start_index}]~{self.end_time}[{self.end_index}]: [{self.trade_step}/{self.trade_len}]" + class BaseInfrastructure: def __init__(self, **kwargs): diff --git a/qlib/contrib/strategy/model_strategy.py b/qlib/contrib/strategy/model_strategy.py index 14e6f0810..2e72cb32c 100644 --- a/qlib/contrib/strategy/model_strategy.py +++ b/qlib/contrib/strategy/model_strategy.py @@ -6,12 +6,15 @@ import pandas as pd from ...utils.resam import resam_ts_data from ...strategy.base import ModelStrategy -from ...backtest.order import Order, BaseTradeDecision +from ...backtest.order import Order, BaseTradeDecision, TradeDecisionWO from .order_generator import OrderGenWInteract class TopkDropoutStrategy(ModelStrategy): + # TODO: + # 1. Supporting leverage the get_range_limit result from the decision + # 2. Supporting alter_outer_trade_decision def __init__( self, model, @@ -246,10 +249,13 @@ class TopkDropoutStrategy(ModelStrategy): factor=factor, ) buy_order_list.append(buy_order) - return TradeDecision(order_list=sell_order_list + buy_order_list, ori_strategy=self) + return TradeDecisionWO(sell_order_list + buy_order_list, self) class WeightStrategyBase(ModelStrategy): + # TODO: + # 1. Supporting leverage the get_range_limit result from the decision + # 2. Supporting alter_outer_trade_decision def __init__( self, model, @@ -343,4 +349,4 @@ class WeightStrategyBase(ModelStrategy): trade_start_time=trade_start_time, trade_end_time=trade_end_time, ) - return TradeDecision(order_list=order_list, ori_strategy=self) + return TradeDecisionWO(order_list, self) diff --git a/qlib/contrib/strategy/order_generator.py b/qlib/contrib/strategy/order_generator.py index f822609c8..c1be982cc 100644 --- a/qlib/contrib/strategy/order_generator.py +++ b/qlib/contrib/strategy/order_generator.py @@ -6,7 +6,7 @@ This order generator is for strategies based on WeightStrategyBase """ from ...backtest.position import Position from ...backtest.exchange import Exchange -from ...backtest.order import BaseTradeDecision +from ...backtest.order import BaseTradeDecision, TradeDecisionWO import pandas as pd import copy @@ -127,7 +127,7 @@ class OrderGenWInteract(OrderGenerator): trade_start_time=trade_start_time, trade_end_time=trade_end_time, ) - return TradeDecision(order_list=order_list, ori_strategy=self) + return TradeDecisionWO(order_list, self) class OrderGenWOInteract(OrderGenerator): @@ -191,4 +191,4 @@ class OrderGenWOInteract(OrderGenerator): trade_start_time=trade_start_time, trade_end_time=trade_end_time, ) - return TradeDecision(order_list=order_list, ori_strategy=self) + return TradeDecisionWO(order_list, self) diff --git a/qlib/contrib/strategy/rule_strategy.py b/qlib/contrib/strategy/rule_strategy.py index 9c024276a..b8a900b85 100644 --- a/qlib/contrib/strategy/rule_strategy.py +++ b/qlib/contrib/strategy/rule_strategy.py @@ -12,6 +12,29 @@ from ...backtest.exchange import Exchange from ...backtest.utils import CommonInfrastructure, LevelInfrastructure +def get_start_end_idx(strategy: BaseStrategy, outer_trade_decision: BaseTradeDecision) -> Union[int, int]: + """ + A helper function for getting the decision-level index range limitation for inner strategy + - NOTE: this function is not applicable to order-level + + Parameters + ---------- + strategy : BaseStrategy + the inner strawtegy + outer_trade_decision : BaseTradeDecision + the trade decision made by outer strategy + + Returns + ------- + Union[int, int]: + start index and end index + """ + try: + return outer_trade_decision.get_range_limit() + except NotImplementedError: + return 0, strategy.trade_calendar.get_trade_len() - 1 + + class TWAPStrategy(BaseStrategy): """TWAP Strategy for trading""" @@ -78,7 +101,7 @@ class TWAPStrategy(BaseStrategy): # get the number of trading step finished, trade_step can be [0, 1, 2, ..., trade_len - 1] trade_step = self.trade_calendar.get_trade_step() # get the total count of trading step - start_idx, end_idx = self.outer_trade_decision.get_range_limit() + start_idx, end_idx = get_start_end_idx(self, self.outer_trade_decision) trade_len = end_idx - start_idx + 1 if trade_step < start_idx: @@ -147,6 +170,10 @@ class SBBStrategyBase(BaseStrategy): TREND_SHORT = 1 TREND_LONG = 2 + # TODO: + # 1. Supporting leverage the get_range_limit result from the decision + # 2. Supporting alter_outer_trade_decision + def __init__( self, outer_trade_decision: BaseTradeDecision = None, @@ -345,13 +372,16 @@ class SBBStrategyBase(BaseStrategy): # in the first one of two adjacent bars, store the trend for the second one to use self.trade_trend[order.stock_id] = _pred_trend - return TradeDecision(order_list=order_list, ori_strategy=self) + return TradeDecisionWO(order_list, self) class SBBStrategyEMA(SBBStrategyBase): """ (S)elect the (B)etter one among every two adjacent trading (B)ars to sell or buy with (EMA) signal. """ + # TODO: + # 1. Supporting leverage the get_range_limit result from the decision + # 2. Supporting alter_outer_trade_decision def __init__( self, @@ -430,6 +460,9 @@ class SBBStrategyEMA(SBBStrategyBase): class ACStrategy(BaseStrategy): + # TODO: + # 1. Supporting leverage the get_range_limit result from the decision + # 2. Supporting alter_outer_trade_decision def __init__( self, lamb: float = 1e-6, @@ -601,7 +634,7 @@ class ACStrategy(BaseStrategy): factor=order.factor, ) order_list.append(_order) - return TradeDecision(order_list=order_list, ori_strategy=self) + return TradeDecisionWO(order_list, self) class RandomOrderStrategy(BaseStrategy): @@ -655,4 +688,4 @@ class RandomOrderStrategy(BaseStrategy): end_time=step_time_end, direction=direction, # 1 for buy )) - return TradeDecisionWO(order_list, self) + return TradeDecisionWO(order_list, self, self.index_range) diff --git a/qlib/strategy/base.py b/qlib/strategy/base.py index b20b0db66..734d25721 100644 --- a/qlib/strategy/base.py +++ b/qlib/strategy/base.py @@ -113,10 +113,9 @@ class BaseStrategy: outer_trade_decision : BaseTradeDecision the decision updated by the outer strategy """ - # default to reset the decision directly # NOTE: normally, user should do something to the strategy due to the change of outer decision - self.outer_trade_decision = outer_trade_decision + raise NotImplementedError(f"Please implement the `alter_outer_trade_decision` method") class ModelStrategy(BaseStrategy):