1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-06-06 05:51:17 +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:
YQ Tsui
2023-01-10 09:46:18 +08:00
committed by GitHub
parent 7f08e6c7b3
commit d8764660dc
2 changed files with 36 additions and 16 deletions

View File

@@ -7,6 +7,7 @@ import numpy as np
import pandas as pd
from typing import Dict, List, Text, Tuple, Union
from abc import ABC
from qlib.data import D
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.log import get_module_logger
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
class BaseSignalStrategy(BaseStrategy):
class BaseSignalStrategy(BaseStrategy, ABC):
def __init__(
self,
*,
@@ -47,7 +48,7 @@ class BaseSignalStrategy(BaseStrategy):
- If `trade_exchange` is None, self.trade_exchange will be set with common_infra
- It allowes different trade_exchanges is used in different executions.
- 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.
"""
@@ -64,7 +65,7 @@ class BaseSignalStrategy(BaseStrategy):
def get_risk_degree(self, trade_step=None):
"""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.
"""
# 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
# 2. Supporting alter_outer_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__(
self,
*,
@@ -85,6 +87,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
method_buy="top",
hold_thresh=1,
only_tradable=False,
forbid_all_trade_at_limit=True,
**kwargs,
):
"""
@@ -111,6 +114,17 @@ class TopkDropoutStrategy(BaseSignalStrategy):
else:
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)
self.topk = topk
@@ -119,6 +133,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
self.method_buy = method_buy
self.hold_thresh = hold_thresh
self.only_tradable = only_tradable
self.forbid_all_trade_at_limit = forbid_all_trade_at_limit
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]
@@ -161,7 +176,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
]
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):
return list(li)[:n]
@@ -171,7 +186,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
def filter_stock(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
sell_order_list = []
buy_order_list = []
@@ -216,7 +231,10 @@ class TopkDropoutStrategy(BaseSignalStrategy):
buy = today[: len(sell) + self.topk - len(last)]
for code in current_stock_list:
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
if code in sell:
@@ -244,7 +262,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
cash += trade_val - trade_cost
# buy new stock
# 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
# 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:
# check is stock suspended
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
# buy order
@@ -296,15 +317,15 @@ class WeightStrategyBase(BaseSignalStrategy):
- It allowes different trade_exchanges is used in different executions.
- 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.
"""
super().__init__(**kwargs)
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:
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):
"""
@@ -316,9 +337,8 @@ class WeightStrategyBase(BaseSignalStrategy):
pred score for this trade date, index is stock_id, contain 'score' column.
current : Position()
current position.
trade_exchange : Exchange()
trade_date : pd.Timestamp
trade date.
trade_start_time: pd.Timestamp
trade_end_time: pd.Timestamp
"""
raise NotImplementedError()

View File

@@ -165,7 +165,7 @@ class TestAllFlow(TestAutoData):
analyze_df = backtest_analysis(TestAllFlow.PRED_SCORE, TestAllFlow.RID, self.URI_PATH)
self.assertGreaterEqual(
analyze_df.loc(axis=0)["excess_return_with_cost", "annualized_return"].values[0],
0.10,
0.05,
"backtest failed",
)
self.assertTrue(not analyze_df.isna().any().any(), "backtest failed")