77import pandas as pd
88
99from typing import Dict , List , Text , Tuple , Union
10+ from abc import ABC
1011
1112from qlib .data import D
1213from qlib .data .dataset import Dataset
1718from qlib .backtest .decision import Order , OrderDir , TradeDecisionWO
1819from qlib .log import get_module_logger
1920from qlib .utils import get_pre_trading_date , load_dataset
20- from qlib .contrib .strategy .order_generator import OrderGenWOInteract
21+ from qlib .contrib .strategy .order_generator import OrderGenerator , OrderGenWOInteract
2122from qlib .contrib .strategy .optimizer import EnhancedIndexingOptimizer
2223
2324
24- class BaseSignalStrategy (BaseStrategy ):
25+ class BaseSignalStrategy (BaseStrategy , ABC ):
2526 def __init__ (
2627 self ,
2728 * ,
@@ -47,7 +48,7 @@ def __init__(
4748 - If `trade_exchange` is None, self.trade_exchange will be set with common_infra
4849 - It allowes different trade_exchanges is used in different executions.
4950 - For example:
50- - In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it run faster.
51+ - In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it runs faster.
5152 - In minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
5253
5354 """
@@ -64,7 +65,7 @@ def __init__(
6465
6566 def get_risk_degree (self , trade_step = None ):
6667 """get_risk_degree
67- Return the proportion of your total value you will used in investment.
68+ Return the proportion of your total value you will use in investment.
6869 Dynamically risk_degree will result in Market timing.
6970 """
7071 # It will use 95% amount of your total value by default
@@ -76,6 +77,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
7677 # 1. Supporting leverage the get_range_limit result from the decision
7778 # 2. Supporting alter_outer_trade_decision
7879 # 3. Supporting checking the availability of trade decision
80+ # 4. Regenerate results with forbid_all_trade_at_limit set to false and flip the default to false, as it is consistent with reality.
7981 def __init__ (
8082 self ,
8183 * ,
@@ -85,6 +87,7 @@ def __init__(
8587 method_buy = "top" ,
8688 hold_thresh = 1 ,
8789 only_tradable = False ,
90+ forbid_all_trade_at_limit = True ,
8891 ** kwargs ,
8992 ):
9093 """
@@ -111,6 +114,17 @@ def __init__(
111114 else:
112115
113116 strategy will make buy sell decision without checking the tradable state of the stock.
117+ forbid_all_trade_at_limit : bool
118+ if forbid all trades when limit_up or limit_down reached.
119+
120+ if forbid_all_trade_at_limit:
121+
122+ strategy will not do any trade when price reaches limit up/down, even not sell at limit up nor buy at
123+ limit down, though allowed in reality.
124+
125+ else:
126+
127+ strategy will sell at limit up and buy ad limit down.
114128 """
115129 super ().__init__ (** kwargs )
116130 self .topk = topk
@@ -119,6 +133,7 @@ def __init__(
119133 self .method_buy = method_buy
120134 self .hold_thresh = hold_thresh
121135 self .only_tradable = only_tradable
136+ self .forbid_all_trade_at_limit = forbid_all_trade_at_limit
122137
123138 def generate_trade_decision (self , execute_result = None ):
124139 # get the number of trading step finished, trade_step can be [0, 1, 2, ..., trade_len - 1]
@@ -161,7 +176,7 @@ def filter_stock(li):
161176 ]
162177
163178 else :
164- # Otherwise, the stock will make decision with out the stock tradable info
179+ # Otherwise, the stock will make decision without the stock tradable info
165180 def get_first_n (li , n ):
166181 return list (li )[:n ]
167182
@@ -171,7 +186,7 @@ def get_last_n(li, n):
171186 def filter_stock (li ):
172187 return li
173188
174- current_temp = copy .deepcopy (self .trade_position )
189+ current_temp : Position = copy .deepcopy (self .trade_position )
175190 # generate order list for this adjust date
176191 sell_order_list = []
177192 buy_order_list = []
@@ -216,7 +231,10 @@ def filter_stock(li):
216231 buy = today [: len (sell ) + self .topk - len (last )]
217232 for code in current_stock_list :
218233 if not self .trade_exchange .is_stock_tradable (
219- stock_id = code , start_time = trade_start_time , end_time = trade_end_time
234+ stock_id = code ,
235+ start_time = trade_start_time ,
236+ end_time = trade_end_time ,
237+ direction = None if self .forbid_all_trade_at_limit else OrderDir .SELL ,
220238 ):
221239 continue
222240 if code in sell :
@@ -244,7 +262,7 @@ def filter_stock(li):
244262 cash += trade_val - trade_cost
245263 # buy new stock
246264 # note the current has been changed
247- current_stock_list = current_temp .get_stock_list ()
265+ # current_stock_list = current_temp.get_stock_list()
248266 value = cash * self .risk_degree / len (buy ) if len (buy ) > 0 else 0
249267
250268 # open_cost should be considered in the real trading environment, while the backtest in evaluate.py does not
@@ -253,7 +271,10 @@ def filter_stock(li):
253271 for code in buy :
254272 # check is stock suspended
255273 if not self .trade_exchange .is_stock_tradable (
256- stock_id = code , start_time = trade_start_time , end_time = trade_end_time
274+ stock_id = code ,
275+ start_time = trade_start_time ,
276+ end_time = trade_end_time ,
277+ direction = None if self .forbid_all_trade_at_limit else OrderDir .BUY ,
257278 ):
258279 continue
259280 # buy order
@@ -296,15 +317,15 @@ def __init__(
296317 - It allowes different trade_exchanges is used in different executions.
297318 - For example:
298319
299- - In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it run faster.
320+ - In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it runs faster.
300321 - In minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
301322 """
302323 super ().__init__ (** kwargs )
303324
304325 if isinstance (order_generator_cls_or_obj , type ):
305- self .order_generator = order_generator_cls_or_obj ()
326+ self .order_generator : OrderGenerator = order_generator_cls_or_obj ()
306327 else :
307- self .order_generator = order_generator_cls_or_obj
328+ self .order_generator : OrderGenerator = order_generator_cls_or_obj
308329
309330 def generate_target_weight_position (self , score , current , trade_start_time , trade_end_time ):
310331 """
@@ -316,9 +337,8 @@ def generate_target_weight_position(self, score, current, trade_start_time, trad
316337 pred score for this trade date, index is stock_id, contain 'score' column.
317338 current : Position()
318339 current position.
319- trade_exchange : Exchange()
320- trade_date : pd.Timestamp
321- trade date.
340+ trade_start_time: pd.Timestamp
341+ trade_end_time: pd.Timestamp
322342 """
323343 raise NotImplementedError ()
324344
0 commit comments