mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-15 08:46:56 +08:00
[BUGFIX] allow sell in limit-up case and allow buy in limit-down case in topk strategy (#1407)
* 1) check limit_up/down should consider direction; 2) fix some typo, typehint etc * fix error * Update test_all_pipeline.py Believe it's just some arbitrary number. The excess return is expected to change when trading logic changes. * add flag forbid_all_trade_at_limit to keep previous behivour for backward compatibility
This commit is contained in:
@@ -7,6 +7,7 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from typing import Dict, List, Text, Tuple, Union
|
from typing import Dict, List, Text, Tuple, Union
|
||||||
|
from abc import ABC
|
||||||
|
|
||||||
from qlib.data import D
|
from qlib.data import D
|
||||||
from qlib.data.dataset import Dataset
|
from qlib.data.dataset import Dataset
|
||||||
@@ -17,11 +18,11 @@ from qlib.backtest.signal import Signal, create_signal_from
|
|||||||
from qlib.backtest.decision import Order, OrderDir, TradeDecisionWO
|
from qlib.backtest.decision import Order, OrderDir, TradeDecisionWO
|
||||||
from qlib.log import get_module_logger
|
from qlib.log import get_module_logger
|
||||||
from qlib.utils import get_pre_trading_date, load_dataset
|
from qlib.utils import get_pre_trading_date, load_dataset
|
||||||
from qlib.contrib.strategy.order_generator import OrderGenWOInteract
|
from qlib.contrib.strategy.order_generator import OrderGenerator, OrderGenWOInteract
|
||||||
from qlib.contrib.strategy.optimizer import EnhancedIndexingOptimizer
|
from qlib.contrib.strategy.optimizer import EnhancedIndexingOptimizer
|
||||||
|
|
||||||
|
|
||||||
class BaseSignalStrategy(BaseStrategy):
|
class BaseSignalStrategy(BaseStrategy, ABC):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -47,7 +48,7 @@ class BaseSignalStrategy(BaseStrategy):
|
|||||||
- If `trade_exchange` is None, self.trade_exchange will be set with common_infra
|
- If `trade_exchange` is None, self.trade_exchange will be set with common_infra
|
||||||
- It allowes different trade_exchanges is used in different executions.
|
- It allowes different trade_exchanges is used in different executions.
|
||||||
- For example:
|
- For example:
|
||||||
- In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it run faster.
|
- In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it runs faster.
|
||||||
- In minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
|
- In minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -64,7 +65,7 @@ class BaseSignalStrategy(BaseStrategy):
|
|||||||
|
|
||||||
def get_risk_degree(self, trade_step=None):
|
def get_risk_degree(self, trade_step=None):
|
||||||
"""get_risk_degree
|
"""get_risk_degree
|
||||||
Return the proportion of your total value you will used in investment.
|
Return the proportion of your total value you will use in investment.
|
||||||
Dynamically risk_degree will result in Market timing.
|
Dynamically risk_degree will result in Market timing.
|
||||||
"""
|
"""
|
||||||
# It will use 95% amount of your total value by default
|
# It will use 95% amount of your total value by default
|
||||||
@@ -76,6 +77,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
# 1. Supporting leverage the get_range_limit result from the decision
|
# 1. Supporting leverage the get_range_limit result from the decision
|
||||||
# 2. Supporting alter_outer_trade_decision
|
# 2. Supporting alter_outer_trade_decision
|
||||||
# 3. Supporting checking the availability of trade decision
|
# 3. Supporting checking the availability of trade decision
|
||||||
|
# 4. Regenerate results with forbid_all_trade_at_limit set to false and flip the default to false, as it is consistent with reality.
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -85,6 +87,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
method_buy="top",
|
method_buy="top",
|
||||||
hold_thresh=1,
|
hold_thresh=1,
|
||||||
only_tradable=False,
|
only_tradable=False,
|
||||||
|
forbid_all_trade_at_limit=True,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -111,6 +114,17 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
else:
|
else:
|
||||||
|
|
||||||
strategy will make buy sell decision without checking the tradable state of the stock.
|
strategy will make buy sell decision without checking the tradable state of the stock.
|
||||||
|
forbid_all_trade_at_limit : bool
|
||||||
|
if forbid all trades when limit_up or limit_down reached.
|
||||||
|
|
||||||
|
if forbid_all_trade_at_limit:
|
||||||
|
|
||||||
|
strategy will not do any trade when price reaches limit up/down, even not sell at limit up nor buy at
|
||||||
|
limit down, though allowed in reality.
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
strategy will sell at limit up and buy ad limit down.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.topk = topk
|
self.topk = topk
|
||||||
@@ -119,6 +133,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
self.method_buy = method_buy
|
self.method_buy = method_buy
|
||||||
self.hold_thresh = hold_thresh
|
self.hold_thresh = hold_thresh
|
||||||
self.only_tradable = only_tradable
|
self.only_tradable = only_tradable
|
||||||
|
self.forbid_all_trade_at_limit = forbid_all_trade_at_limit
|
||||||
|
|
||||||
def generate_trade_decision(self, execute_result=None):
|
def generate_trade_decision(self, execute_result=None):
|
||||||
# get the number of trading step finished, trade_step can be [0, 1, 2, ..., trade_len - 1]
|
# get the number of trading step finished, trade_step can be [0, 1, 2, ..., trade_len - 1]
|
||||||
@@ -161,7 +176,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
]
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Otherwise, the stock will make decision with out the stock tradable info
|
# Otherwise, the stock will make decision without the stock tradable info
|
||||||
def get_first_n(li, n):
|
def get_first_n(li, n):
|
||||||
return list(li)[:n]
|
return list(li)[:n]
|
||||||
|
|
||||||
@@ -171,7 +186,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
def filter_stock(li):
|
def filter_stock(li):
|
||||||
return li
|
return li
|
||||||
|
|
||||||
current_temp = copy.deepcopy(self.trade_position)
|
current_temp: Position = copy.deepcopy(self.trade_position)
|
||||||
# generate order list for this adjust date
|
# generate order list for this adjust date
|
||||||
sell_order_list = []
|
sell_order_list = []
|
||||||
buy_order_list = []
|
buy_order_list = []
|
||||||
@@ -216,7 +231,10 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
buy = today[: len(sell) + self.topk - len(last)]
|
buy = today[: len(sell) + self.topk - len(last)]
|
||||||
for code in current_stock_list:
|
for code in current_stock_list:
|
||||||
if not self.trade_exchange.is_stock_tradable(
|
if not self.trade_exchange.is_stock_tradable(
|
||||||
stock_id=code, start_time=trade_start_time, end_time=trade_end_time
|
stock_id=code,
|
||||||
|
start_time=trade_start_time,
|
||||||
|
end_time=trade_end_time,
|
||||||
|
direction=None if self.forbid_all_trade_at_limit else OrderDir.SELL,
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if code in sell:
|
if code in sell:
|
||||||
@@ -244,7 +262,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
cash += trade_val - trade_cost
|
cash += trade_val - trade_cost
|
||||||
# buy new stock
|
# buy new stock
|
||||||
# note the current has been changed
|
# note the current has been changed
|
||||||
current_stock_list = current_temp.get_stock_list()
|
# current_stock_list = current_temp.get_stock_list()
|
||||||
value = cash * self.risk_degree / len(buy) if len(buy) > 0 else 0
|
value = cash * self.risk_degree / len(buy) if len(buy) > 0 else 0
|
||||||
|
|
||||||
# open_cost should be considered in the real trading environment, while the backtest in evaluate.py does not
|
# open_cost should be considered in the real trading environment, while the backtest in evaluate.py does not
|
||||||
@@ -253,7 +271,10 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
|||||||
for code in buy:
|
for code in buy:
|
||||||
# check is stock suspended
|
# check is stock suspended
|
||||||
if not self.trade_exchange.is_stock_tradable(
|
if not self.trade_exchange.is_stock_tradable(
|
||||||
stock_id=code, start_time=trade_start_time, end_time=trade_end_time
|
stock_id=code,
|
||||||
|
start_time=trade_start_time,
|
||||||
|
end_time=trade_end_time,
|
||||||
|
direction=None if self.forbid_all_trade_at_limit else OrderDir.BUY,
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
# buy order
|
# buy order
|
||||||
@@ -296,15 +317,15 @@ class WeightStrategyBase(BaseSignalStrategy):
|
|||||||
- It allowes different trade_exchanges is used in different executions.
|
- It allowes different trade_exchanges is used in different executions.
|
||||||
- For example:
|
- For example:
|
||||||
|
|
||||||
- In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it run faster.
|
- In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it runs faster.
|
||||||
- In minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
|
- In minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
if isinstance(order_generator_cls_or_obj, type):
|
if isinstance(order_generator_cls_or_obj, type):
|
||||||
self.order_generator = order_generator_cls_or_obj()
|
self.order_generator: OrderGenerator = order_generator_cls_or_obj()
|
||||||
else:
|
else:
|
||||||
self.order_generator = order_generator_cls_or_obj
|
self.order_generator: OrderGenerator = order_generator_cls_or_obj
|
||||||
|
|
||||||
def generate_target_weight_position(self, score, current, trade_start_time, trade_end_time):
|
def generate_target_weight_position(self, score, current, trade_start_time, trade_end_time):
|
||||||
"""
|
"""
|
||||||
@@ -316,9 +337,8 @@ class WeightStrategyBase(BaseSignalStrategy):
|
|||||||
pred score for this trade date, index is stock_id, contain 'score' column.
|
pred score for this trade date, index is stock_id, contain 'score' column.
|
||||||
current : Position()
|
current : Position()
|
||||||
current position.
|
current position.
|
||||||
trade_exchange : Exchange()
|
trade_start_time: pd.Timestamp
|
||||||
trade_date : pd.Timestamp
|
trade_end_time: pd.Timestamp
|
||||||
trade date.
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class TestAllFlow(TestAutoData):
|
|||||||
analyze_df = backtest_analysis(TestAllFlow.PRED_SCORE, TestAllFlow.RID, self.URI_PATH)
|
analyze_df = backtest_analysis(TestAllFlow.PRED_SCORE, TestAllFlow.RID, self.URI_PATH)
|
||||||
self.assertGreaterEqual(
|
self.assertGreaterEqual(
|
||||||
analyze_df.loc(axis=0)["excess_return_with_cost", "annualized_return"].values[0],
|
analyze_df.loc(axis=0)["excess_return_with_cost", "annualized_return"].values[0],
|
||||||
0.10,
|
0.05,
|
||||||
"backtest failed",
|
"backtest failed",
|
||||||
)
|
)
|
||||||
self.assertTrue(not analyze_df.isna().any().any(), "backtest failed")
|
self.assertTrue(not analyze_df.isna().any().any(), "backtest failed")
|
||||||
|
|||||||
Reference in New Issue
Block a user