mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-16 01:06:56 +08:00
Merge branch 'nested_decision_exe' of https://github.com/microsoft/qlib into rl-dummy
This commit is contained in:
@@ -247,7 +247,7 @@ def collect_data(
|
|||||||
"""initialize the strategy and executor, then collect the trade decision data for rl training
|
"""initialize the strategy and executor, then collect the trade decision data for rl training
|
||||||
|
|
||||||
please refer to the docs of the backtest for the explanation of the parameters
|
please refer to the docs of the backtest for the explanation of the parameters
|
||||||
|
|
||||||
Yields
|
Yields
|
||||||
-------
|
-------
|
||||||
object
|
object
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class Exchange:
|
|||||||
codes="all",
|
codes="all",
|
||||||
deal_price: Union[str, Tuple[str], List[str]] = None,
|
deal_price: Union[str, Tuple[str], List[str]] = None,
|
||||||
subscribe_fields=[],
|
subscribe_fields=[],
|
||||||
limit_threshold=None,
|
limit_threshold: Union[Tuple[str, str], float, None] = None,
|
||||||
volume_threshold=None,
|
volume_threshold=None,
|
||||||
open_cost=0.0015,
|
open_cost=0.0015,
|
||||||
close_cost=0.0025,
|
close_cost=0.0025,
|
||||||
@@ -41,7 +41,7 @@ class Exchange:
|
|||||||
:param end_time: closed end time for backtest
|
:param end_time: closed end time for backtest
|
||||||
:param codes: list stock_id list or a string of instruments(i.e. all, csi500, sse50)
|
:param codes: list stock_id list or a string of instruments(i.e. all, csi500, sse50)
|
||||||
|
|
||||||
:param deal_price: Union[str, Tuple[str], List[str]]
|
:param deal_price: Union[str, Tuple[str, str], List[str]]
|
||||||
The `deal_price` supports following two types of input
|
The `deal_price` supports following two types of input
|
||||||
- <deal_price> : str
|
- <deal_price> : str
|
||||||
- (<buy_price>, <sell_price>): Tuple[str] or List[str]
|
- (<buy_price>, <sell_price>): Tuple[str] or List[str]
|
||||||
@@ -51,8 +51,16 @@ class Exchange:
|
|||||||
- for example '$close', '$open', '$vwap' ("close" is OK. `Exchange` will help to prepend
|
- for example '$close', '$open', '$vwap' ("close" is OK. `Exchange` will help to prepend
|
||||||
"$" to the expression)
|
"$" to the expression)
|
||||||
|
|
||||||
:param subscribe_fields: list, subscribe fields
|
:param subscribe_fields: list, subscribe fields. This expressions will be added to the query and `self.quote`.
|
||||||
:param limit_threshold: float, 0.1 for example, default None
|
It is useful when users want more fields to be queried
|
||||||
|
|
||||||
|
:param limit_threshold: Union[Tuple[str, str], float, None]
|
||||||
|
1) `None`: no limitation
|
||||||
|
2) float, 0.1 for example, default None
|
||||||
|
3) Tuple[str, str]: (<the expression for buying stock limitation>,
|
||||||
|
<the expression for sell stock limitation>)
|
||||||
|
`False` value indicates the stock is tradable
|
||||||
|
`True` value indicates the stock is limited and not tradable
|
||||||
:param volume_threshold: float, 0.1 for example, default None
|
:param volume_threshold: float, 0.1 for example, default None
|
||||||
:param open_cost: cost rate for open, default 0.0015
|
:param open_cost: cost rate for open, default 0.0015
|
||||||
:param close_cost: cost rate for close, default 0.0025
|
:param close_cost: cost rate for close, default 0.0025
|
||||||
@@ -97,7 +105,7 @@ class Exchange:
|
|||||||
if limit_threshold is None:
|
if limit_threshold is None:
|
||||||
if C.region == REG_CN:
|
if C.region == REG_CN:
|
||||||
self.logger.warning(f"limit_threshold not set. The stocks hit the limit may be bought/sold")
|
self.logger.warning(f"limit_threshold not set. The stocks hit the limit may be bought/sold")
|
||||||
elif abs(limit_threshold) > 0.1:
|
elif self._get_limit_type(limit_threshold) == self.LT_FLT and abs(limit_threshold) > 0.1:
|
||||||
if C.region == REG_CN:
|
if C.region == REG_CN:
|
||||||
self.logger.warning(f"limit_threshold may not be set to a reasonable value")
|
self.logger.warning(f"limit_threshold may not be set to a reasonable value")
|
||||||
|
|
||||||
@@ -119,13 +127,17 @@ class Exchange:
|
|||||||
# $change is for calculating the limit of the stock
|
# $change is for calculating the limit of the stock
|
||||||
|
|
||||||
necessary_fields = {self.buy_price, self.sell_price, "$close", "$change", "$factor", "$volume"}
|
necessary_fields = {self.buy_price, self.sell_price, "$close", "$change", "$factor", "$volume"}
|
||||||
|
if self._get_limit_type(limit_threshold) == self.LT_TP_EXP:
|
||||||
|
for exp in limit_threshold:
|
||||||
|
necessary_fields.add(exp)
|
||||||
subscribe_fields = list(necessary_fields | set(subscribe_fields))
|
subscribe_fields = list(necessary_fields | set(subscribe_fields))
|
||||||
all_fields = list(necessary_fields | set(subscribe_fields))
|
all_fields = list(necessary_fields | set(subscribe_fields))
|
||||||
|
|
||||||
self.all_fields = all_fields
|
self.all_fields = all_fields
|
||||||
self.open_cost = open_cost
|
self.open_cost = open_cost
|
||||||
self.close_cost = close_cost
|
self.close_cost = close_cost
|
||||||
self.min_cost = min_cost
|
self.min_cost = min_cost
|
||||||
self.limit_threshold = limit_threshold
|
self.limit_threshold: Union[Tuple[str, str], float, None] = limit_threshold
|
||||||
self.volume_threshold = volume_threshold
|
self.volume_threshold = volume_threshold
|
||||||
self.extra_quote = extra_quote
|
self.extra_quote = extra_quote
|
||||||
self.set_quote(codes, start_time, end_time)
|
self.set_quote(codes, start_time, end_time)
|
||||||
@@ -133,6 +145,7 @@ class Exchange:
|
|||||||
def set_quote(self, codes, start_time, end_time):
|
def set_quote(self, codes, start_time, end_time):
|
||||||
if len(codes) == 0:
|
if len(codes) == 0:
|
||||||
codes = D.instruments()
|
codes = D.instruments()
|
||||||
|
|
||||||
self.quote = D.features(codes, self.all_fields, start_time, end_time, freq=self.freq, disk_cache=True).dropna(
|
self.quote = D.features(codes, self.all_fields, start_time, end_time, freq=self.freq, disk_cache=True).dropna(
|
||||||
subset=["$close"]
|
subset=["$close"]
|
||||||
)
|
)
|
||||||
@@ -157,13 +170,7 @@ class Exchange:
|
|||||||
self.trade_w_adj_price = False
|
self.trade_w_adj_price = False
|
||||||
|
|
||||||
# update limit
|
# update limit
|
||||||
# check limit_threshold
|
self._update_limit()
|
||||||
if self.limit_threshold is None:
|
|
||||||
self.quote["limit_buy"] = False
|
|
||||||
self.quote["limit_sell"] = False
|
|
||||||
else:
|
|
||||||
# set limit
|
|
||||||
self._update_limit(buy_limit=self.limit_threshold, sell_limit=self.limit_threshold)
|
|
||||||
|
|
||||||
quote_df = self.quote
|
quote_df = self.quote
|
||||||
if self.extra_quote is not None:
|
if self.extra_quote is not None:
|
||||||
@@ -194,9 +201,33 @@ class Exchange:
|
|||||||
|
|
||||||
self.quote = quote_dict
|
self.quote = quote_dict
|
||||||
|
|
||||||
def _update_limit(self, buy_limit, sell_limit):
|
LT_TP_EXP = "(exp)" # Tuple[str, str]
|
||||||
self.quote["limit_buy"] = self.quote["$change"].ge(buy_limit)
|
LT_FLT = "float" # float
|
||||||
self.quote["limit_sell"] = self.quote["$change"].le(-sell_limit)
|
LT_NONE = "none" # none
|
||||||
|
|
||||||
|
def _get_limit_type(self, limit_threshold):
|
||||||
|
if isinstance(limit_threshold, Tuple):
|
||||||
|
return self.LT_TP_EXP
|
||||||
|
elif isinstance(limit_threshold, float):
|
||||||
|
return self.LT_FLT
|
||||||
|
elif limit_threshold is None:
|
||||||
|
return self.LT_NONE
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"This type of `limit_threshold` is not supported")
|
||||||
|
|
||||||
|
def _update_limit(self):
|
||||||
|
# check limit_threshold
|
||||||
|
lt_type = self._get_limit_type(self.limit_threshold)
|
||||||
|
if lt_type == self.LT_NONE:
|
||||||
|
self.quote["limit_buy"] = False
|
||||||
|
self.quote["limit_sell"] = False
|
||||||
|
elif lt_type == self.LT_TP_EXP:
|
||||||
|
# set limit
|
||||||
|
self.quote["limit_buy"] = self.quote[self.limit_threshold[0]]
|
||||||
|
self.quote["limit_sell"] = self.quote[self.limit_threshold[1]]
|
||||||
|
elif lt_type == self.LT_FLT:
|
||||||
|
self.quote["limit_buy"] = self.quote["$change"].ge(self.limit_threshold)
|
||||||
|
self.quote["limit_sell"] = self.quote["$change"].le(-self.limit_threshold) # pylint: disable=E1130
|
||||||
|
|
||||||
def check_stock_limit(self, stock_id, start_time, end_time, direction=None):
|
def check_stock_limit(self, stock_id, start_time, end_time, direction=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ class NestedExecutor(BaseExecutor):
|
|||||||
- The decisions may be updated by steps
|
- The decisions may be updated by steps
|
||||||
- The inner executor may not follow the decisions from the outer strategy
|
- The inner executor may not follow the decisions from the outer strategy
|
||||||
align_range_limit: bool
|
align_range_limit: bool
|
||||||
force to align the index_range decision
|
force to align the trade_range decision
|
||||||
It is only for nested executor, because range_limit is given by outer strategy
|
It is only for nested executor, because range_limit is given by outer strategy
|
||||||
"""
|
"""
|
||||||
self.inner_executor: BaseExecutor = init_instance_by_config(
|
self.inner_executor: BaseExecutor = init_instance_by_config(
|
||||||
|
|||||||
@@ -166,7 +166,60 @@ class OrderHelper:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IndexRangeByTime:
|
class TradeRange:
|
||||||
|
def __call__(self, trade_calendar: TradeCalendarManager) -> Tuple[int, int]:
|
||||||
|
"""
|
||||||
|
This method will be call with following way
|
||||||
|
|
||||||
|
The outer strategy give a decision with with `TradeRange`
|
||||||
|
The decision will be checked by the inner decision.
|
||||||
|
inner decision will pass its trade_calendar as parameter when getting the trading range
|
||||||
|
- The framework's step is integer-index based.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
trade_calendar : TradeCalendarManager
|
||||||
|
the trade_calendar is from inner strategy
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[int, int]:
|
||||||
|
the start index and end index which are tradable
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
NotImplementedError:
|
||||||
|
Exceptions are raised when no range limitation
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(f"Please implement the `__call__` method")
|
||||||
|
|
||||||
|
def clip_time_range(self, start_time: pd.Timestamp, end_time: pd.Timestamp) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
start_time : pd.Timestamp
|
||||||
|
end_time : pd.Timestamp
|
||||||
|
Both sides (start_time, end_time) are closed
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[pd.Timestamp, pd.Timestamp]:
|
||||||
|
The tradable time range.
|
||||||
|
- It is intersection of [start_time, end_time] and the rule of TradeRange itself
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(f"Please implement the `clip_time_range` method")
|
||||||
|
|
||||||
|
|
||||||
|
class IdxTradeRange(TradeRange):
|
||||||
|
def __init__(self, start_idx: int, end_idx: int):
|
||||||
|
self._start_idx = start_idx
|
||||||
|
self._end_idx = end_idx
|
||||||
|
|
||||||
|
def __call__(self, trade_calendar: TradeCalendarManager = None) -> Tuple[int, int]:
|
||||||
|
return self._start_idx, self._end_idx
|
||||||
|
|
||||||
|
|
||||||
|
class TradeRangeByTime(TradeRange):
|
||||||
"""This is a helper function for make decisions"""
|
"""This is a helper function for make decisions"""
|
||||||
|
|
||||||
def __init__(self, start_time: str, end_time: str):
|
def __init__(self, start_time: str, end_time: str):
|
||||||
@@ -186,14 +239,24 @@ class IndexRangeByTime:
|
|||||||
"""
|
"""
|
||||||
self.start_time = pd.Timestamp(start_time).time()
|
self.start_time = pd.Timestamp(start_time).time()
|
||||||
self.end_time = pd.Timestamp(end_time).time()
|
self.end_time = pd.Timestamp(end_time).time()
|
||||||
|
assert self.start_time < self.end_time
|
||||||
|
|
||||||
def __call__(self, trade_calendar: TradeCalendarManager) -> Tuple[int, int]:
|
def __call__(self, trade_calendar: TradeCalendarManager = None) -> Tuple[int, int]:
|
||||||
|
if trade_calendar is None:
|
||||||
|
raise NotImplementedError("trade_calendar is necessary for getting TradeRangeByTime.")
|
||||||
start = trade_calendar.start_time
|
start = trade_calendar.start_time
|
||||||
val_start, val_end = concat_date_time(start.date(), self.start_time), concat_date_time(
|
val_start, val_end = concat_date_time(start.date(), self.start_time), concat_date_time(
|
||||||
start.date(), self.end_time
|
start.date(), self.end_time
|
||||||
)
|
)
|
||||||
return trade_calendar.get_range_idx(val_start, val_end)
|
return trade_calendar.get_range_idx(val_start, val_end)
|
||||||
|
|
||||||
|
def clip_time_range(self, start_time: pd.Timestamp, end_time: pd.Timestamp) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
||||||
|
start_date = start_time.date()
|
||||||
|
val_start, val_end = concat_date_time(start_date, self.start_time), concat_date_time(start_date, self.end_time)
|
||||||
|
# NOTE: `end_date` should not be used. Because the `end_date` is for slicing. It may be in the next day
|
||||||
|
# Assumption: start_time and end_time is for intraday trading. So it is OK for only using start_date
|
||||||
|
return max(val_start, start_time), min(val_end, end_time)
|
||||||
|
|
||||||
|
|
||||||
class BaseTradeDecision:
|
class BaseTradeDecision:
|
||||||
"""
|
"""
|
||||||
@@ -211,54 +274,29 @@ class BaseTradeDecision:
|
|||||||
2. Same as `case 1.3`
|
2. Same as `case 1.3`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, strategy: BaseStrategy, idx_range: Union[Tuple[int, int], Callable] = None):
|
def __init__(self, strategy: BaseStrategy, trade_range: Union[Tuple[int, int], TradeRange] = None):
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
strategy : BaseStrategy
|
strategy : BaseStrategy
|
||||||
The strategy who make the decision
|
The strategy who make the decision
|
||||||
idx_range: Union[Tuple[int, int], Callable] (optional)
|
trade_range: Union[Tuple[int, int], Callable] (optional)
|
||||||
The index range for underlying strategy.
|
The index range for underlying strategy.
|
||||||
|
|
||||||
Here are two examples of idx_range for each type
|
Here are two examples of trade_range for each type
|
||||||
|
|
||||||
1) Tuple[int, int]
|
1) Tuple[int, int]
|
||||||
start_index and end_index of the underlying factor(both sides are closed)
|
start_index and end_index of the underlying strategy(both sides are closed)
|
||||||
|
|
||||||
|
2) TradeRange
|
||||||
2) Callable
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
def idx_range(time_per_step: str) -> Tuple[int, int]:
|
|
||||||
# time_per_step is the strategy's time_per_step (not inner strategy. It's the `self` strategy in
|
|
||||||
# `self._idx_range` )
|
|
||||||
# e.g.
|
|
||||||
# For example, strategy A with 30min each step and strategy B with 1min each step
|
|
||||||
# strategy A's will use "30min" when calling `idx_range`.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.strategy = strategy
|
self.strategy = strategy
|
||||||
self.total_step = None # upper strategy has no knowledge about the sub executor before `_init_sub_trading`
|
self.total_step = None # upper strategy has no knowledge about the sub executor before `_init_sub_trading`
|
||||||
self._idx_range = idx_range
|
if isinstance(trade_range, Tuple):
|
||||||
|
# for Tuple[int, int]
|
||||||
@staticmethod
|
trade_range = IdxTradeRange(**trade_range)
|
||||||
def _calc_idx_range(
|
self.trade_range: TradeRange = trade_range
|
||||||
idx_range: Union[Tuple[int, int], Callable], inner_calendar: TradeCalendarManager = None
|
|
||||||
) -> Tuple[int, int]:
|
|
||||||
"""calculate index range for `idx_range` in different cases"""
|
|
||||||
if idx_range is None:
|
|
||||||
# not set, return nothing
|
|
||||||
return None, None
|
|
||||||
elif isinstance(idx_range, tuple):
|
|
||||||
return idx_range
|
|
||||||
elif isinstance(idx_range, Callable):
|
|
||||||
if inner_calendar is None:
|
|
||||||
# time_per_step is a required parameter for `def idx_range`
|
|
||||||
return None, None
|
|
||||||
else:
|
|
||||||
return idx_range(inner_calendar)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(f"This type of input is not supported")
|
|
||||||
|
|
||||||
def get_decision(self) -> List[object]:
|
def get_decision(self) -> List[object]:
|
||||||
"""
|
"""
|
||||||
@@ -303,12 +341,18 @@ class BaseTradeDecision:
|
|||||||
# purpose 2)
|
# purpose 2)
|
||||||
return self.strategy.update_trade_decision(self, trade_calendar)
|
return self.strategy.update_trade_decision(self, trade_calendar)
|
||||||
|
|
||||||
|
def _get_range_limit(self, **kwargs) -> Tuple[int, int]:
|
||||||
|
if self.trade_range is not None:
|
||||||
|
return self.trade_range(trade_calendar=kwargs.get("inner_calendar"))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("The decision didn't provide an index range")
|
||||||
|
|
||||||
def get_range_limit(self, **kwargs) -> Tuple[int, int]:
|
def get_range_limit(self, **kwargs) -> Tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
return the expected step range for limiting the decision execution time
|
return the expected step range for limiting the decision execution time
|
||||||
Both left and right are **closed**
|
Both left and right are **closed**
|
||||||
|
|
||||||
if no available _idx_range, `default_value` will be returned
|
if no available trade_range, `default_value` will be returned
|
||||||
|
|
||||||
It is only used in `NestedExecutor`
|
It is only used in `NestedExecutor`
|
||||||
- The outmost strategy will not follow any range limit (but it may give range_limit)
|
- The outmost strategy will not follow any range limit (but it may give range_limit)
|
||||||
@@ -328,7 +372,7 @@ class BaseTradeDecision:
|
|||||||
"default_value": <default_value>, # using dict is for distinguish no value provided or None provided
|
"default_value": <default_value>, # using dict is for distinguish no value provided or None provided
|
||||||
"inner_calendar": <trade calendar of inner strategy>
|
"inner_calendar": <trade calendar of inner strategy>
|
||||||
# because the range limit will control the step range of inner strategy, inner calendar will be a
|
# because the range limit will control the step range of inner strategy, inner calendar will be a
|
||||||
# important parameter when _idx_range is callable
|
# important parameter when trade_range is callable
|
||||||
}
|
}
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -342,29 +386,25 @@ class BaseTradeDecision:
|
|||||||
1) the decision can't provide a unified start and end
|
1) the decision can't provide a unified start and end
|
||||||
2) default_value is not provided
|
2) default_value is not provided
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
# get index
|
_start_idx, _end_idx = self._get_range_limit(**kwargs)
|
||||||
_start_idx, _end_idx = self._calc_idx_range(self._idx_range, inner_calendar=kwargs.get("inner_calendar"))
|
except NotImplementedError:
|
||||||
if _start_idx is None or _end_idx is None:
|
|
||||||
# handle case without decision
|
|
||||||
# TODO: time range in the order should be checked.
|
|
||||||
|
|
||||||
# _start_idx and _end_idx should be used instead of _idx_range
|
|
||||||
# because it is possible that no limitation when _idx_range is callable and return None
|
|
||||||
if "default_value" in kwargs:
|
if "default_value" in kwargs:
|
||||||
return kwargs["default_value"]
|
return kwargs["default_value"]
|
||||||
else:
|
else:
|
||||||
# Default to get full index
|
# Default to get full index
|
||||||
raise NotImplementedError(f"The decision didn't provide an index range")
|
raise NotImplementedError(f"The decision didn't provide an index range")
|
||||||
else:
|
|
||||||
# clip index
|
# clip index
|
||||||
if getattr(self, "total_step", None) is not None:
|
if getattr(self, "total_step", None) is not None:
|
||||||
# if `self.update` is called.
|
# if `self.update` is called.
|
||||||
# Then the _start_idx, _end_idx should be clipped
|
# Then the _start_idx, _end_idx should be clipped
|
||||||
if _start_idx < 0 or _end_idx >= self.total_step:
|
if _start_idx < 0 or _end_idx >= self.total_step:
|
||||||
logger = get_module_logger("decision")
|
logger = get_module_logger("decision")
|
||||||
logger.warning(f"{self._idx_range} go beyoud the total_step({self.total_step}), it will be clipped")
|
logger.warning(
|
||||||
_start_idx, _end_idx = max(0, _start_idx), min(self.total_step - 1, _end_idx)
|
f"[{_start_idx},{_end_idx}] go beyoud the total_step({self.total_step}), it will be clipped"
|
||||||
|
)
|
||||||
|
_start_idx, _end_idx = max(0, _start_idx), min(self.total_step - 1, _end_idx)
|
||||||
return _start_idx, _end_idx
|
return _start_idx, _end_idx
|
||||||
|
|
||||||
def empty(self) -> bool:
|
def empty(self) -> bool:
|
||||||
@@ -394,9 +434,9 @@ class BaseTradeDecision:
|
|||||||
inner_trade_decision : BaseTradeDecision
|
inner_trade_decision : BaseTradeDecision
|
||||||
"""
|
"""
|
||||||
# base class provide a default behaviour to modify inner_trade_decision
|
# base class provide a default behaviour to modify inner_trade_decision
|
||||||
# callable _idx_range should be propagated when inner _idx_range is not set
|
# trade_range should be propagated when inner trade_range is not set
|
||||||
if isinstance(self._idx_range, Callable) and inner_trade_decision._idx_range is None:
|
if inner_trade_decision.trade_range is None:
|
||||||
inner_trade_decision._idx_range = self._idx_range
|
inner_trade_decision.trade_range = self.trade_range
|
||||||
|
|
||||||
|
|
||||||
class EmptyTradeDecision(BaseTradeDecision):
|
class EmptyTradeDecision(BaseTradeDecision):
|
||||||
@@ -410,106 +450,12 @@ class TradeDecisionWO(BaseTradeDecision):
|
|||||||
Besides, the time_range is also included.
|
Besides, the time_range is also included.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, order_list: List[Order], strategy: BaseStrategy, idx_range: Tuple[int, int] = None):
|
def __init__(self, order_list: List[Order], strategy: BaseStrategy, trade_range: Tuple[int, int] = None):
|
||||||
super().__init__(strategy, idx_range=idx_range)
|
super().__init__(strategy, trade_range=trade_range)
|
||||||
self.order_list = order_list
|
self.order_list = order_list
|
||||||
|
|
||||||
def get_decision(self) -> List[object]:
|
def get_decision(self) -> List[object]:
|
||||||
return self.order_list
|
return self.order_list
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"strategy: {self.strategy}; idx_range: {self._idx_range}; order_list[{len(self.order_list)}]"
|
return f"strategy: {self.strategy}; trade_range: {self.trade_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"""
|
|
||||||
|
|
||||||
def __init__(self, strategy, order_pool):
|
|
||||||
"""
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
strategy : BaseStrategy
|
|
||||||
the original strategy that make the decision
|
|
||||||
order_pool : list, optional
|
|
||||||
the candinate order pool for generate trade decision
|
|
||||||
"""
|
|
||||||
super(TradeDecisionWithOrderPool, self).__init__(strategy)
|
|
||||||
self.order_pool = order_pool
|
|
||||||
self.order_list = []
|
|
||||||
|
|
||||||
def pop_order_pool(self, pop_len):
|
|
||||||
if pop_len > len(self.order_pool):
|
|
||||||
warnings.warn(
|
|
||||||
f"pop len {pop_len} is too much length than order pool, cut it as pool length {len(self.order_pool)}"
|
|
||||||
)
|
|
||||||
pop_len = len(self.order_pool)
|
|
||||||
res = self.order_pool[:pop_len]
|
|
||||||
del self.order_pool[:pop_len]
|
|
||||||
return res
|
|
||||||
|
|
||||||
def push_order_list(self, order_list):
|
|
||||||
self.order_list.extend(order_list)
|
|
||||||
|
|
||||||
def get_decision(self):
|
|
||||||
"""get the order list
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
only_enable : bool, optional
|
|
||||||
wether to ignore disabled order, by default False
|
|
||||||
only_disable : bool, optional
|
|
||||||
wether to ignore enabled order, by default False
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
List[Order]
|
|
||||||
the order list
|
|
||||||
"""
|
|
||||||
return self.order_list
|
|
||||||
|
|
||||||
def update(self, trade_calendar):
|
|
||||||
"""make the original strategy update the enabled status of orders."""
|
|
||||||
self.ori_strategy.update_trade_decision(self, trade_calendar)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseDecisionUpdater:
|
|
||||||
def update_decision(self, decision, trade_calendar) -> BaseTradeDecision:
|
|
||||||
"""
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
decision : BaseTradeDecision
|
|
||||||
the trade decision to be updated
|
|
||||||
trade_calendar : BaseTradeCalendar
|
|
||||||
the trade calendar of inner execution
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
BaseTradeDecision
|
|
||||||
the updated decision
|
|
||||||
"""
|
|
||||||
raise NotImplementedError(f"This method is not implemented")
|
|
||||||
|
|
||||||
|
|
||||||
class DecisionUpdaterWithOrderPool:
|
|
||||||
def __init__(self, plan_config=None):
|
|
||||||
"""
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
plan_config : Dict[Tuple(int, float)], optional
|
|
||||||
the plan config, by default None
|
|
||||||
"""
|
|
||||||
if plan_config is None:
|
|
||||||
self.plan_config = [(0, 1)]
|
|
||||||
else:
|
|
||||||
self.plan_config = plan_config
|
|
||||||
|
|
||||||
def update_decision(self, decision, trade_calendar) -> BaseTradeDecision:
|
|
||||||
# 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()
|
|
||||||
for _index, _ratio in self.plan_config:
|
|
||||||
if trade_step == _index:
|
|
||||||
pop_len = len(decision.order_pool) * _ratio
|
|
||||||
pop_order_list = decision.pop_order_pool(pop_len)
|
|
||||||
decision.push_order_list(pop_order_list)
|
|
||||||
|
|||||||
@@ -364,6 +364,11 @@ class Indicator:
|
|||||||
agg = pa_config.get("agg", "twap").lower()
|
agg = pa_config.get("agg", "twap").lower()
|
||||||
price = pa_config.get("price", "deal_price").lower()
|
price = pa_config.get("price", "deal_price").lower()
|
||||||
|
|
||||||
|
# NOTE: IndexTradeRange is not supported!!!!! Because inner index is not available
|
||||||
|
trade_start_time, trade_end_time = decision.trade_range.clip_time_range(
|
||||||
|
start_time=trade_start_time, end_time=trade_end_time
|
||||||
|
)
|
||||||
|
|
||||||
if price == "deal_price":
|
if price == "deal_price":
|
||||||
price_s = trade_exchange.get_deal_price(
|
price_s = trade_exchange.get_deal_price(
|
||||||
inst, trade_start_time, trade_end_time, direction=direction, method=None
|
inst, trade_start_time, trade_end_time, direction=direction, method=None
|
||||||
@@ -386,21 +391,6 @@ class Indicator:
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"This type of input is not supported")
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
|
|
||||||
# no sub executor on the lowest level
|
|
||||||
# So range_limit an total step will all be None
|
|
||||||
total_step = decision.total_step
|
|
||||||
if total_step is None:
|
|
||||||
total_step = 1
|
|
||||||
range_limit = decision.get_range_limit(default_value=(0, total_step - 1))
|
|
||||||
|
|
||||||
assert volume_s.shape[0] % total_step == 0, "The price series can't be divided by step length"
|
|
||||||
factor = volume_s.shape[0] // total_step
|
|
||||||
|
|
||||||
slc = slice(range_limit[0] * factor, (range_limit[1] + 1) * factor)
|
|
||||||
|
|
||||||
volume_s = volume_s.iloc[slc]
|
|
||||||
price_s = price_s.iloc[slc]
|
|
||||||
|
|
||||||
base_volume = volume_s.sum().item()
|
base_volume = volume_s.sum().item()
|
||||||
base_price = ((price_s * volume_s).sum() / base_volume).item()
|
base_price = ((price_s * volume_s).sum() / base_volume).item()
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from qlib.utils import lazy_sort_index
|
|||||||
from ...utils.resam import resam_ts_data, ts_data_last
|
from ...utils.resam import resam_ts_data, ts_data_last
|
||||||
from ...data.data import D
|
from ...data.data import D
|
||||||
from ...strategy.base import BaseStrategy
|
from ...strategy.base import BaseStrategy
|
||||||
from ...backtest.order import BaseTradeDecision, Order, TradeDecisionWO
|
from ...backtest.order import BaseTradeDecision, Order, TradeDecisionWO, TradeRange
|
||||||
from ...backtest.exchange import Exchange, OrderHelper
|
from ...backtest.exchange import Exchange, OrderHelper
|
||||||
from ...backtest.utils import CommonInfrastructure, LevelInfrastructure
|
from ...backtest.utils import CommonInfrastructure, LevelInfrastructure
|
||||||
from qlib.utils.file import get_io_object
|
from qlib.utils.file import get_io_object
|
||||||
@@ -625,7 +625,7 @@ class ACStrategy(BaseStrategy):
|
|||||||
class RandomOrderStrategy(BaseStrategy):
|
class RandomOrderStrategy(BaseStrategy):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
index_range: Tuple[int, int], # The range is closed on both left and right.
|
trade_range: Union[Tuple[int, int], TradeRange], # The range is closed on both left and right.
|
||||||
sample_ratio: float = 1.0,
|
sample_ratio: float = 1.0,
|
||||||
volume_ratio: float = 0.01,
|
volume_ratio: float = 0.01,
|
||||||
market: str = "all",
|
market: str = "all",
|
||||||
@@ -636,13 +636,8 @@ class RandomOrderStrategy(BaseStrategy):
|
|||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
index_range : Tuple
|
trade_range : Tuple
|
||||||
the intra day time index range of the orders
|
please refer to the `trade_range` parameter of BaseStrategy
|
||||||
the left and right is closed.
|
|
||||||
|
|
||||||
If you want to get the index_range in intra-day
|
|
||||||
- `qlib/utils/time.py:def get_day_min_idx_range` can help you create the index range easier
|
|
||||||
# TODO: this is a index_range level limitation. We'll implement a more detailed limitation later.
|
|
||||||
sample_ratio : float
|
sample_ratio : float
|
||||||
the ratio of all orders are sampled
|
the ratio of all orders are sampled
|
||||||
volume_ratio : float
|
volume_ratio : float
|
||||||
@@ -653,7 +648,6 @@ class RandomOrderStrategy(BaseStrategy):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.index_range = index_range
|
|
||||||
self.sample_ratio = sample_ratio
|
self.sample_ratio = sample_ratio
|
||||||
self.volume_ratio = volume_ratio
|
self.volume_ratio = volume_ratio
|
||||||
self.market = market
|
self.market = market
|
||||||
@@ -664,6 +658,7 @@ class RandomOrderStrategy(BaseStrategy):
|
|||||||
D.instruments(market), ["Mean(Ref($volume, 1), 10)"], start_time=exch.start_time, end_time=exch.end_time
|
D.instruments(market), ["Mean(Ref($volume, 1), 10)"], start_time=exch.start_time, end_time=exch.end_time
|
||||||
)
|
)
|
||||||
self.volume_df = self.volume.iloc[:, 0].unstack()
|
self.volume_df = self.volume.iloc[:, 0].unstack()
|
||||||
|
self.trade_range = trade_range
|
||||||
|
|
||||||
def generate_trade_decision(self, execute_result=None):
|
def generate_trade_decision(self, execute_result=None):
|
||||||
trade_step = self.trade_calendar.get_trade_step()
|
trade_step = self.trade_calendar.get_trade_step()
|
||||||
@@ -683,7 +678,7 @@ class RandomOrderStrategy(BaseStrategy):
|
|||||||
direction=self.direction,
|
direction=self.direction,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return TradeDecisionWO(order_list, self, self.index_range)
|
return TradeDecisionWO(order_list, self, self.trade_range)
|
||||||
|
|
||||||
|
|
||||||
class FileOrderStrategy(BaseStrategy):
|
class FileOrderStrategy(BaseStrategy):
|
||||||
@@ -692,7 +687,8 @@ class FileOrderStrategy(BaseStrategy):
|
|||||||
- This class provides an interface for user to read orders from csv files.
|
- This class provides an interface for user to read orders from csv files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, file: Union[IO, str, Path, pd.DataFrame], index_range: Tuple[int, int] = None, *args, **kwargs):
|
def __init__(self, file: Union[IO, str, Path, pd.DataFrame],
|
||||||
|
trade_range: Union[Tuple[int, int], TradeRange]= None, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -709,13 +705,13 @@ class FileOrderStrategy(BaseStrategy):
|
|||||||
20200103, SH600519, 1000, buy
|
20200103, SH600519, 1000, buy
|
||||||
20200106, SH600519, 1000, sell
|
20200106, SH600519, 1000, sell
|
||||||
|
|
||||||
index_range : Tuple[int, int]
|
trade_range : Tuple[int, int]
|
||||||
the intra day time index range of the orders
|
the intra day time index range of the orders
|
||||||
the left and right is closed.
|
the left and right is closed.
|
||||||
|
|
||||||
If you want to get the index_range in intra-day
|
If you want to get the trade_range in intra-day
|
||||||
- `qlib/utils/time.py:def get_day_min_idx_range` can help you create the index range easier
|
- `qlib/utils/time.py:def get_day_min_idx_range` can help you create the index range easier
|
||||||
# TODO: this is a index_range level limitation. We'll implement a more detailed limitation later.
|
# TODO: this is a trade_range level limitation. We'll implement a more detailed limitation later.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -730,7 +726,7 @@ class FileOrderStrategy(BaseStrategy):
|
|||||||
|
|
||||||
# make sure the datetime is the first level for fast indexing
|
# make sure the datetime is the first level for fast indexing
|
||||||
self.order_df = lazy_sort_index(convert_index_format(self.order_df, level="datetime"))
|
self.order_df = lazy_sort_index(convert_index_format(self.order_df, level="datetime"))
|
||||||
self.index_range = index_range
|
self.trade_range = trade_range
|
||||||
|
|
||||||
def generate_trade_decision(self, execute_result=None) -> TradeDecisionWO:
|
def generate_trade_decision(self, execute_result=None) -> TradeDecisionWO:
|
||||||
"""
|
"""
|
||||||
@@ -760,4 +756,4 @@ class FileOrderStrategy(BaseStrategy):
|
|||||||
end_time=end,
|
end_time=end,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return TradeDecisionWO(order_list, self, self.index_range)
|
return TradeDecisionWO(order_list, self, self.trade_range)
|
||||||
|
|||||||
Reference in New Issue
Block a user