1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-10 14:26:56 +08:00

Merge branch 'nested_decision_exe' into nested_decision_exe

This commit is contained in:
you-n-g
2021-07-23 12:15:45 +08:00
committed by GitHub
12 changed files with 343 additions and 234 deletions

View File

@@ -160,7 +160,7 @@ class Account:
self.accum_info.add_return_value(profit) # note here do not consider cost self.accum_info.add_return_value(profit) # note here do not consider cost
def update_order(self, order, trade_val, cost, trade_price): def update_order(self, order, trade_val, cost, trade_price):
if not self.is_port_metr_enabled(): if self.current.skip_update():
# TODO: supporting polymorphism for account # TODO: supporting polymorphism for account
# updating order for infinite position is meaningless # updating order for infinite position is meaningless
return return

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
from qlib.backtest.position import Position
import random import random
import logging import logging
from typing import List, Tuple, Union, Callable, Iterable from typing import List, Tuple, Union, Callable, Iterable
@@ -281,6 +282,8 @@ class Exchange:
""" """
Deal order when the actual transaction Deal order when the actual transaction
the results section in `Order` will be changed.
:param order: Deal the order. :param order: Deal the order.
:param trade_account: Trade account to be updated after dealing the order. :param trade_account: Trade account to be updated after dealing the order.
:param position: position to be updated after dealing the order. :param position: position to be updated after dealing the order.
@@ -343,6 +346,7 @@ class Exchange:
`None`: if the stock is suspended `None` may be returned `None`: if the stock is suspended `None` may be returned
`float`: return factor if the factor exists `float`: return factor if the factor exists
""" """
assert (start_time is not None and end_time is not None, "the time range must be given")
if stock_id not in self.quote.get_all_stock(): if stock_id not in self.quote.get_all_stock():
return None return None
return self.quote.get_data(stock_id, start_time, end_time, fields="$factor", method=ts_data_last) return self.quote.get_data(stock_id, start_time, end_time, fields="$factor", method=ts_data_last)
@@ -505,20 +509,56 @@ class Exchange:
) )
return value return value
def get_amount_of_trade_unit(self, factor): def _get_factor_or_raise_erorr(self, factor: float = None, stock_id: str = None, start_time=None, end_time=None):
"""Please refer to the docs of get_amount_of_trade_unit"""
if factor is None:
if stock_id is not None and start_time is not None and end_time is not None:
factor = self.get_factor(stock_id=stock_id, start_time=start_time, end_time=end_time)
else:
raise ValueError(f"`factor` and (`stock_id`, `start_time`, `end_time`) can't both be None")
return factor
def get_amount_of_trade_unit(self, factor: float = None, stock_id: str = None, start_time=None, end_time=None):
"""
get the trade unit of amount based on **factor**
the factor can be given directly or calculated in given time range and stock id.
`factor` has higher priority than `stock_id`, `start_time` and `end_time`
Parameters
----------
factor : float
the adjusted factor
stock_id : str
the id of the stock
start_time :
the start time of trading range
end_time :
the end time of trading range
"""
if not self.trade_w_adj_price and self.trade_unit is not None: if not self.trade_w_adj_price and self.trade_unit is not None:
factor = self._get_factor_or_raise_erorr(
factor=factor, stock_id=stock_id, start_time=start_time, end_time=end_time
)
return self.trade_unit / factor return self.trade_unit / factor
else: else:
return None return None
def round_amount_by_trade_unit(self, deal_amount, factor): def round_amount_by_trade_unit(
self, deal_amount, factor: float = None, stock_id: str = None, start_time=None, end_time=None
):
"""Parameter """Parameter
Please refer to the docs of get_amount_of_trade_unit
deal_amount : float, adjusted amount deal_amount : float, adjusted amount
factor : float, adjusted factor factor : float, adjusted factor
return : float, real amount return : float, real amount
""" """
if not self.trade_w_adj_price and self.trade_unit is not None: if not self.trade_w_adj_price and self.trade_unit is not None:
# the minimal amount is 1. Add 0.1 for solving precision problem. # the minimal amount is 1. Add 0.1 for solving precision problem.
factor = self._get_factor_or_raise_erorr(
factor=factor, stock_id=stock_id, start_time=start_time, end_time=end_time
)
return (deal_amount * factor + 0.1) // self.trade_unit * self.trade_unit / factor return (deal_amount * factor + 0.1) // self.trade_unit * self.trade_unit / factor
return deal_amount return deal_amount
@@ -529,7 +569,7 @@ class Exchange:
else: else:
return deal_amount return deal_amount
def _calc_trade_info_by_order(self, order, position): def _calc_trade_info_by_order(self, order, position: Position):
""" """
Calculation of trade info Calculation of trade info
@@ -541,6 +581,7 @@ class Exchange:
""" """
trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, direction=order.direction) trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, direction=order.direction)
order.factor = self.get_factor(order.stock_id, order.start_time, order.end_time)
if order.direction == Order.SELL: if order.direction == Order.SELL:
# sell # sell
if position is not None: if position is not None:

View File

@@ -1,5 +1,6 @@
from abc import abstractclassmethod, abstractmethod from abc import abstractclassmethod, abstractmethod
import copy import copy
from qlib.log import get_module_logger
from types import GeneratorType from types import GeneratorType
from qlib.backtest.account import Account from qlib.backtest.account import Account
import warnings import warnings
@@ -102,7 +103,10 @@ class BaseExecutor:
self.track_data = track_data self.track_data = track_data
self._trade_exchange = trade_exchange self._trade_exchange = trade_exchange
self.level_infra = LevelInfrastructure() self.level_infra = LevelInfrastructure()
self.level_infra.reset_infra(common_infra=common_infra)
self.reset(start_time=start_time, end_time=end_time, common_infra=common_infra) self.reset(start_time=start_time, end_time=end_time, common_infra=common_infra)
if common_infra is None:
get_module_logger("BaseExecutor").warning(f"`common_infra` is not set for {self}")
def reset_common_infra(self, common_infra): def reset_common_infra(self, common_infra):
""" """
@@ -239,7 +243,7 @@ class BaseExecutor:
# Some concrete executor don't have inner decisions # Some concrete executor don't have inner decisions
res, kwargs = obj res, kwargs = obj
trade_start_time, trade_end_time = self.trade_calendar.get_cur_step_time() trade_start_time, trade_end_time = self.trade_calendar.get_step_time()
# Account will not be changed in this function # Account will not be changed in this function
self.trade_account.update_bar_end( self.trade_account.update_bar_end(
trade_start_time, trade_start_time,
@@ -332,7 +336,7 @@ class NestedExecutor(BaseExecutor):
self.inner_strategy.reset_common_infra(common_infra) self.inner_strategy.reset_common_infra(common_infra)
def _init_sub_trading(self, trade_decision): def _init_sub_trading(self, trade_decision):
trade_start_time, trade_end_time = self.trade_calendar.get_cur_step_time() trade_start_time, trade_end_time = self.trade_calendar.get_step_time()
self.inner_executor.reset(start_time=trade_start_time, end_time=trade_end_time) self.inner_executor.reset(start_time=trade_start_time, end_time=trade_end_time)
sub_level_infra = self.inner_executor.get_level_infra() sub_level_infra = self.inner_executor.get_level_infra()
self.level_infra.set_sub_level_infra(sub_level_infra) self.level_infra.set_sub_level_infra(sub_level_infra)
@@ -379,8 +383,8 @@ class NestedExecutor(BaseExecutor):
) )
trade_decision.mod_inner_decision(_inner_trade_decision) # propagate part of decision information trade_decision.mod_inner_decision(_inner_trade_decision) # propagate part of decision information
# NOTE sub_cal.get_cur_step_time() must be called before collect_data in case of step shifting # NOTE sub_cal.get_step_time() must be called before collect_data in case of step shifting
decision_list.append((_inner_trade_decision, *sub_cal.get_cur_step_time())) decision_list.append((_inner_trade_decision, *sub_cal.get_step_time()))
# NOTE: Trade Calendar will step forward in the follow line # NOTE: Trade Calendar will step forward in the follow line
_inner_execute_result = yield from self.inner_executor.collect_data( _inner_execute_result = yield from self.inner_executor.collect_data(
@@ -478,7 +482,7 @@ class SimulatorExecutor(BaseExecutor):
def _collect_data(self, trade_decision: BaseTradeDecision, level: int = 0): def _collect_data(self, trade_decision: BaseTradeDecision, level: int = 0):
trade_start_time, _ = self.trade_calendar.get_cur_step_time() trade_start_time, _ = self.trade_calendar.get_step_time()
execute_result = [] execute_result = []
for order in self._get_order_iterator(trade_decision): for order in self._get_order_iterator(trade_decision):
@@ -491,30 +495,22 @@ class SimulatorExecutor(BaseExecutor):
execute_result.append((order, trade_val, trade_cost, trade_price)) execute_result.append((order, trade_val, trade_cost, trade_price))
if self.verbose: if self.verbose:
if order.direction == Order.SELL: # sell if order.direction == Order.SELL: # sell
print( action = "sell"
"[I {:%Y-%m-%d %H:%M:%S}]: sell {}, price {:.2f}, amount {}, deal_amount {}, factor {}, value {:.2f}.".format(
trade_start_time,
order.stock_id,
trade_price,
order.amount,
order.deal_amount,
order.factor,
trade_val,
)
)
else: else:
print( action = "buy"
"[I {:%Y-%m-%d %H:%M:%S}]: buy {}, price {:.2f}, amount {}, deal_amount {}, factor {}, value {:.2f}.".format( print(
trade_start_time, "[I {:%Y-%m-%d %H:%M:%S}]: {} {}, price {:.2f}, amount {}, deal_amount {}, factor {}, value {:.2f}, cach {:.2f}.".format(
order.stock_id, trade_start_time,
trade_price, action,
order.amount, order.stock_id,
order.deal_amount, trade_price,
order.factor, order.amount,
trade_val, order.deal_amount,
) order.factor,
trade_val,
self.trade_account.get_cash(),
) )
)
else: else:
if self.verbose: if self.verbose:
print("[W {:%Y-%m-%d %H:%M:%S}]: {} wrong.".format(trade_start_time, order.stock_id)) print("[W {:%Y-%m-%d %H:%M:%S}]: {} wrong.".format(trade_start_time, order.stock_id))

View File

@@ -3,7 +3,8 @@
# TODO: rename it with decision.py # TODO: rename it with decision.py
from __future__ import annotations from __future__ import annotations
from enum import IntEnum from enum import IntEnum
from qlib.utils.time import concat_date_time from qlib.data.data import Cal
from qlib.utils.time import concat_date_time, epsilon_change
from qlib.log import get_module_logger from qlib.log import get_module_logger
# try to fix circular imports when enabling type hints # try to fix circular imports when enabling type hints
@@ -41,16 +42,24 @@ class Order:
presents the weight factor assigned in Exchange() presents the weight factor assigned in Exchange()
""" """
# 1) time invariant values
# - they are set by users and is time-invariant.
stock_id: str stock_id: str
amount: float # `amount` is a non-negative value amount: float # `amount` is a non-negative and adjusted value
direction: int
# 2) time variant values:
# - Users may want to set these values when using lower level APIs
# - If users don't, TradeDecisionWO will help users to set them
# The interval of the order which belongs to (NOTE: this is not the expected order dealing range time) # The interval of the order which belongs to (NOTE: this is not the expected order dealing range time)
start_time: pd.Timestamp start_time: pd.Timestamp
end_time: pd.Timestamp end_time: pd.Timestamp
direction: int # 3) results
factor: float # - users should not care about these values
# - they are set by the backtest system after finishing the results.
deal_amount: float = field(init=False) # `deal_amount` is a non-negative value deal_amount: float = field(init=False) # `deal_amount` is a non-negative value
factor: float = field(init=False)
# FIXME: # FIXME:
# for compatible now. # for compatible now.
@@ -127,8 +136,8 @@ class OrderHelper:
code: str, code: str,
amount: float, amount: float,
direction: OrderDir, direction: OrderDir,
start_time: Union[str, pd.Timestamp], start_time: Union[str, pd.Timestamp] = None,
end_time: Union[str, pd.Timestamp], end_time: Union[str, pd.Timestamp] = None,
) -> Order: ) -> Order:
""" """
help to create a order help to create a order
@@ -143,9 +152,9 @@ class OrderHelper:
**adjusted trading amount** **adjusted trading amount**
direction : OrderDir direction : OrderDir
trading direction trading direction
start_time : Union[str, pd.Timestamp] start_time : Union[str, pd.Timestamp] (optional)
The interval of the order which belongs to The interval of the order which belongs to
end_time : Union[str, pd.Timestamp] end_time : Union[str, pd.Timestamp] (optional)
The interval of the order which belongs to The interval of the order which belongs to
Returns Returns
@@ -153,15 +162,17 @@ class OrderHelper:
Order: Order:
The created order The created order
""" """
start_time = pd.Timestamp(start_time) if start_time is not None:
end_time = pd.Timestamp(end_time) start_time = pd.Timestamp(start_time)
if end_time is not None:
end_time = pd.Timestamp(end_time)
# NOTE: factor is a value belongs to the results section. User don't have to care about it when creating orders
return Order( return Order(
stock_id=code, stock_id=code,
amount=amount, amount=amount,
start_time=start_time, start_time=start_time,
end_time=end_time, end_time=end_time,
direction=direction, direction=direction,
factor=self.exchange.get_factor(code, start_time, end_time),
) )
@@ -291,6 +302,7 @@ class BaseTradeDecision:
""" """
self.strategy = strategy self.strategy = strategy
self.start_time, self.end_time = strategy.trade_calendar.get_step_time()
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`
if isinstance(trade_range, Tuple): if isinstance(trade_range, Tuple):
# for Tuple[int, int] # for Tuple[int, int]
@@ -406,6 +418,62 @@ class BaseTradeDecision:
_start_idx, _end_idx = max(0, _start_idx), min(self.total_step - 1, _end_idx) _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 get_data_cal_range_limit(self, rtype: str = "full", raise_error: bool = False) -> Tuple[int, int]:
"""
get the range limit based on data calendar
NOTE: it is **total** range limit instead of a single step
The following assumptions are made
1) The frequency of the exchange in common_infra is the same as the data calendar
2) Users want the index mod by **day** (i.e. 240 min)
Parameters
----------
rtype: str
- "full": return the full limitation of the deicsion in the day
- "step": return the limitation of current step
raise_error: bool
True: raise error if no trade_range is set
False: return full trade calendar.
It is useful in following cases
- users want to follow the order specific trading time range when decision level trade range is not
available. Raising NotImplementedError to indicates that range limit is not available
Returns
-------
Tuple[int, int]:
the range limit in data calendar
Raises
------
NotImplementedError:
If the following criteria meet
1) the decision can't provide a unified start and end
2) raise_error is True
"""
# potential performance issue
day_start = pd.Timestamp(self.start_time.date())
day_end = epsilon_change(day_start + pd.Timedelta(days=1))
freq = self.strategy.trade_exchange.freq
_, _, day_start_idx, day_end_idx = Cal.locate_index(day_start, day_end, freq=freq)
if self.trade_range is None:
if raise_error:
raise NotImplementedError(f"There is no trade_range in this case")
else:
return 0, day_end_idx - day_start_idx
else:
if rtype == "full":
val_start, val_end = self.trade_range.clip_time_range(day_start, day_end)
elif rtype == "step":
val_start, val_end = self.trade_range.clip_time_range(self.start_time, self.end_time)
else:
raise ValueError(f"This type of input {rtype} is not supported")
_, _, start_idx, end_index = Cal.locate_index(val_start, val_end, freq=freq)
return start_idx - day_start_idx, end_index - day_start_idx
def empty(self) -> bool: def empty(self) -> bool:
for obj in self.get_decision(): for obj in self.get_decision():
if isinstance(obj, Order): if isinstance(obj, Order):
@@ -452,9 +520,15 @@ class TradeDecisionWO(BaseTradeDecision):
def __init__(self, order_list: List[Order], strategy: BaseStrategy, trade_range: Tuple[int, int] = None): def __init__(self, order_list: List[Order], strategy: BaseStrategy, trade_range: Tuple[int, int] = None):
super().__init__(strategy, trade_range=trade_range) super().__init__(strategy, trade_range=trade_range)
self.order_list = order_list self.order_list = order_list
start, end = strategy.trade_calendar.get_step_time()
for o in order_list:
if o.start_time is None:
o.start_time = start
if o.end_time is None:
o.end_time = end
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}; trade_range: {self.trade_range}; order_list[{len(self.order_list)}]" return f"class: {self.__class__.__name__}; strategy: {self.strategy}; trade_range: {self.trade_range}; order_list[{len(self.order_list)}]"

View File

@@ -351,7 +351,10 @@ class Indicator:
trade_exchange: Exchange, trade_exchange: Exchange,
pa_config: dict = {}, pa_config: dict = {},
): ):
"""Get the base volume and price information""" """
Get the base volume and price information
All the base price values are rooted from this function
"""
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()
@@ -374,10 +377,12 @@ class Indicator:
# NOTE: there are some zeros in the trading price. These cases are known meaningless # NOTE: there are some zeros in the trading price. These cases are known meaningless
# for aligning the previous logic, remove it. # for aligning the previous logic, remove it.
# price_s = price_s.mask(np.isclose(price_s, 0)) price_s = price_s[~(price_s < 1e-08)] # remove zero and negative values.
# NOTE ~(price_s < 1e-08) is different from price_s >= 1e-8
if agg == "vwap": if agg == "vwap":
volume_s = trade_exchange.get_volume(inst, trade_start_time, trade_end_time, method=None) volume_s = trade_exchange.get_volume(inst, trade_start_time, trade_end_time, method=None)
volume_s = volume_s.reindex(price_s.index)
elif agg == "twap": elif agg == "twap":
volume_s = pd.Series(1, index=price_s.index) volume_s = pd.Series(1, index=price_s.index)
else: else:

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
from __future__ import annotations from __future__ import annotations
import bisect import bisect
from qlib.utils.time import epsilon_change
from typing import Union, TYPE_CHECKING, Tuple, Union, List, Set from typing import Union, TYPE_CHECKING, Tuple, Union, List, Set
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -22,7 +23,11 @@ class TradeCalendarManager:
""" """
def __init__( def __init__(
self, freq: str, start_time: Union[str, pd.Timestamp] = None, end_time: Union[str, pd.Timestamp] = None self,
freq: str,
start_time: Union[str, pd.Timestamp] = None,
end_time: Union[str, pd.Timestamp] = None,
level_infra: "LevelInfrastructure" = None,
): ):
""" """
Parameters Parameters
@@ -36,6 +41,7 @@ class TradeCalendarManager:
closed end of the trade time range, by default None closed end of the trade time range, by default None
If `end_time` is None, it must be reset before trading. If `end_time` is None, it must be reset before trading.
""" """
self.level_infra = level_infra
self.reset(freq=freq, start_time=start_time, end_time=end_time) self.reset(freq=freq, start_time=start_time, end_time=end_time)
def reset(self, freq, start_time, end_time): def reset(self, freq, start_time, end_time):
@@ -82,19 +88,19 @@ class TradeCalendarManager:
def get_trade_step(self): def get_trade_step(self):
return self.trade_step return self.trade_step
def get_step_time(self, trade_step=0, shift=0): def get_step_time(self, trade_step=None, shift=0):
""" """
Get the left and right endpoints of the trade_step'th trading interval Get the left and right endpoints of the trade_step'th trading interval
About the endpoints: About the endpoints:
- Qlib uses the closed interval in time-series data selection, which has the same performance as pandas.Series.loc - Qlib uses the closed interval in time-series data selection, which has the same performance as pandas.Series.loc
- The returned right endpoints should minus 1 seconds becasue of the closed interval representation in Qlib. # - The returned right endpoints should minus 1 seconds becasue of the closed interval representation in Qlib.
Note: Qlib supports up to minutely decision execution, so 1 seconds is less than any trading time interval. # Note: Qlib supports up to minutely decision execution, so 1 seconds is less than any trading time interval.
Parameters Parameters
---------- ----------
trade_step : int, optional trade_step : int, optional
the number of trading step finished, by default 0 the number of trading step finished, by default None to indicate current step
shift : int, optional shift : int, optional
shift bars , by default 0 shift bars , by default 0
@@ -105,15 +111,43 @@ class TradeCalendarManager:
- If shift > 0, return the trading time range of the earlier shift bars - If shift > 0, return the trading time range of the earlier shift bars
- If shift < 0, return the trading time range of the later shift bar - If shift < 0, return the trading time range of the later shift bar
""" """
if trade_step is None:
trade_step = self.get_trade_step()
trade_step = trade_step - shift trade_step = trade_step - shift
calendar_index = self.start_index + trade_step calendar_index = self.start_index + trade_step
return self._calendar[calendar_index], self._calendar[calendar_index + 1] - pd.Timedelta(seconds=1) return self._calendar[calendar_index], epsilon_change(self._calendar[calendar_index + 1])
def get_cur_step_time(self): def get_data_cal_range(self, rtype: str = "full") -> Tuple[int, int]:
""" """
get current step time get the calendar range
The following assumptions are made
1) The frequency of the exchange in common_infra is the same as the data calendar
2) Users want the **data index** mod by **day** (i.e. 240 min)
Parameters
----------
rtype: str
- "full": return the full limitation of the deicsion in the day
- "step": return the limitation of current step
Returns
-------
Tuple[int, int]:
""" """
return self.get_step_time(self.get_trade_step()) # potential performance issue
day_start = pd.Timestamp(self.start_time.date())
day_end = epsilon_change(day_start + pd.Timedelta(days=1))
freq = self.level_infra.get("common_infra").get("trade_exchange").freq
_, _, day_start_idx, _ = Cal.locate_index(day_start, day_end, freq=freq)
if rtype == "full":
_, _, start_idx, end_index = Cal.locate_index(self.start_time, self.end_time, freq=freq)
elif rtype == "step":
_, _, start_idx, end_index = Cal.locate_index(*self.get_step_time(), freq=freq)
else:
raise ValueError(f"This type of input {rtype} is not supported")
return start_idx - day_start_idx, end_index - day_start_idx
def get_all_time(self): def get_all_time(self):
"""Get the start_time and end_time for trading""" """Get the start_time and end_time for trading"""
@@ -147,7 +181,7 @@ class TradeCalendarManager:
return clip(left), clip(right) return clip(left), clip(right)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.start_time}[{self.start_index}]~{self.end_time}[{self.end_index}]: [{self.trade_step}/{self.trade_len}]" return f"class: {self.__class__.__name__}; {self.start_time}[{self.start_index}]~{self.end_time}[{self.end_index}]: [{self.trade_step}/{self.trade_len}]"
class BaseInfrastructure: class BaseInfrastructure:
@@ -198,14 +232,16 @@ class LevelInfrastructure(BaseInfrastructure):
sub_level_infra: sub_level_infra:
- **NOTE**: this will only work after _init_sub_trading !!! - **NOTE**: this will only work after _init_sub_trading !!!
""" """
return ["trade_calendar", "sub_level_infra"] return ["trade_calendar", "sub_level_infra", "common_infra"]
def reset_cal(self, freq, start_time, end_time): def reset_cal(self, freq, start_time, end_time):
"""reset trade calendar manager""" """reset trade calendar manager"""
if self.has("trade_calendar"): if self.has("trade_calendar"):
self.get("trade_calendar").reset(freq, start_time=start_time, end_time=end_time) self.get("trade_calendar").reset(freq, start_time=start_time, end_time=end_time)
else: else:
self.reset_infra(trade_calendar=TradeCalendarManager(freq, start_time=start_time, end_time=end_time)) self.reset_infra(
trade_calendar=TradeCalendarManager(freq, start_time=start_time, end_time=end_time, level_infra=self)
)
def set_sub_level_infra(self, sub_level_infra: LevelInfrastructure): def set_sub_level_infra(self, sub_level_infra: LevelInfrastructure):
"""this will make the calendar access easier when acrossing multi-levels""" """this will make the calendar access easier when acrossing multi-levels"""

View File

@@ -73,20 +73,20 @@ def indicator_analysis(df, method="mean"):
Parameters Parameters
---------- ----------
df : pandas.DataFrame df : pandas.DataFrame
columns: like ['pa', 'pos', 'ffr', 'amount', 'value']. columns: like ['pa', 'pos', 'ffr', 'deal_amount', 'value'].
Necessary fields: Necessary fields:
- 'pa' is the price advantage in trade indicators - 'pa' is the price advantage in trade indicators
- 'pos' is the positive rate in trade indicators - 'pos' is the positive rate in trade indicators
- 'ffr' is the fulfill rate in trade indicators - 'ffr' is the fulfill rate in trade indicators
Optional fields: Optional fields:
- 'amount' is the total deal amount, only necessary when method is 'amount_weighted' - 'deal_amount' is the total deal deal_amount, only necessary when method is 'amount_weighted'
- 'value' is the total trade value, only necessary when method is 'value_weighted' - 'value' is the total trade value, only necessary when method is 'value_weighted'
index: Index(datetime) index: Index(datetime)
method : str, optional method : str, optional
statistics method of pa/ffr, by default "mean" statistics method of pa/ffr, by default "mean"
- if method is 'mean', count the mean statistical value of each trade indicator - if method is 'mean', count the mean statistical value of each trade indicator
- if method is 'amount_weighted', count the amount weighted mean statistical value of each trade indicator - if method is 'amount_weighted', count the deal_amount weighted mean statistical value of each trade indicator
- if method is 'value_weighted', count the value weighted mean statistical value of each trade indicator - if method is 'value_weighted', count the value weighted mean statistical value of each trade indicator
Note: statistics method of pos is always "mean" Note: statistics method of pos is always "mean"
@@ -97,7 +97,7 @@ def indicator_analysis(df, method="mean"):
""" """
weights_dict = { weights_dict = {
"mean": df["count"], "mean": df["count"],
"amount_weighted": df["amount"].abs(), "amount_weighted": df["deal_amount"].abs(),
"value_weighted": df["value"].abs(), "value_weighted": df["value"].abs(),
} }
if method not in weights_dict: if method not in weights_dict:

View File

@@ -64,7 +64,7 @@ class TopkDropoutStrategy(ModelStrategy):
""" """
super(TopkDropoutStrategy, self).__init__( super(TopkDropoutStrategy, self).__init__(
model, dataset, level_infra=level_infra, common_infra=common_infra, **kwargs model, dataset, level_infra=level_infra, common_infra=common_infra, trade_exchange=trade_exchange, **kwargs
) )
self.topk = topk self.topk = topk
self.n_drop = n_drop self.n_drop = n_drop
@@ -73,22 +73,6 @@ class TopkDropoutStrategy(ModelStrategy):
self.risk_degree = risk_degree self.risk_degree = risk_degree
self.hold_thresh = hold_thresh self.hold_thresh = hold_thresh
self.only_tradable = only_tradable self.only_tradable = only_tradable
if trade_exchange is not None:
self.trade_exchange = trade_exchange
def reset_common_infra(self, common_infra):
"""
Parameters
----------
common_infra : dict, optional
common infrastructure for backtesting, by default None
- It should include `trade_account`, used to get position
- It should include `trade_exchange`, used to provide market info
"""
super(TopkDropoutStrategy, self).reset_common_infra(common_infra)
if common_infra.has("trade_exchange"):
self.trade_exchange = common_infra.get("trade_exchange")
def get_risk_degree(self, trade_step=None): def get_risk_degree(self, trade_step=None):
"""get_risk_degree """get_risk_degree
@@ -210,7 +194,6 @@ class TopkDropoutStrategy(ModelStrategy):
start_time=trade_start_time, start_time=trade_start_time,
end_time=trade_end_time, end_time=trade_end_time,
direction=Order.SELL, # 0 for sell, 1 for buy direction=Order.SELL, # 0 for sell, 1 for buy
factor=factor,
) )
# is order executable # is order executable
if self.trade_exchange.check_order(sell_order): if self.trade_exchange.check_order(sell_order):
@@ -247,7 +230,6 @@ class TopkDropoutStrategy(ModelStrategy):
start_time=trade_start_time, start_time=trade_start_time,
end_time=trade_end_time, end_time=trade_end_time,
direction=Order.BUY, # 1 for buy direction=Order.BUY, # 1 for buy
factor=factor,
) )
buy_order_list.append(buy_order) buy_order_list.append(buy_order)
return TradeDecisionWO(sell_order_list + buy_order_list, self) return TradeDecisionWO(sell_order_list + buy_order_list, self)
@@ -278,28 +260,12 @@ class WeightStrategyBase(ModelStrategy):
- 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(WeightStrategyBase, self).__init__( super(WeightStrategyBase, self).__init__(
model, dataset, level_infra=level_infra, common_infra=common_infra, **kwargs model, dataset, level_infra=level_infra, common_infra=common_infra, trade_exchange=trade_exchange, **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 = order_generator_cls_or_obj()
else: else:
self.order_generator = order_generator_cls_or_obj self.order_generator = order_generator_cls_or_obj
if trade_exchange is not None:
self.trade_exchange = trade_exchange
def reset_common_infra(self, common_infra):
"""
Parameters
----------
common_infra : dict, optional
common infrastructure for backtesting, by default None
- It should include `trade_account`, used to get position
- It should include `trade_exchange`, used to provide market info
"""
super(WeightStrategyBase, self).reset_common_infra(common_infra)
if common_infra.has("trade_exchange"):
self.trade_exchange = common_infra.get("trade_exchange")
def get_risk_degree(self, trade_step=None): def get_risk_degree(self, trade_step=None):
"""get_risk_degree """get_risk_degree

View File

@@ -20,48 +20,6 @@ from qlib.backtest.utils import get_start_end_idx
class TWAPStrategy(BaseStrategy): class TWAPStrategy(BaseStrategy):
"""TWAP Strategy for trading""" """TWAP Strategy for trading"""
def __init__(
self,
outer_trade_decision: BaseTradeDecision = None,
trade_exchange: Exchange = None,
level_infra: LevelInfrastructure = None,
common_infra: CommonInfrastructure = None,
):
"""
Parameters
----------
outer_trade_decision : BaseTradeDecision
the trade decision of outer strategy which this startegy relies
trade_exchange : Exchange
exchange that provides market info, used to deal order and generate report
- 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 minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
"""
super(TWAPStrategy, self).__init__(
outer_trade_decision=outer_trade_decision, level_infra=level_infra, common_infra=common_infra
)
if trade_exchange is not None:
self.trade_exchange = trade_exchange
def reset_common_infra(self, common_infra):
"""
Parameters
----------
common_infra : CommonInfrastructure, optional
common infrastructure for backtesting, by default None
- It should include `trade_account`, used to get position
- It should include `trade_exchange`, used to provide market info
"""
super(TWAPStrategy, self).reset_common_infra(common_infra)
if common_infra.has("trade_exchange"):
self.trade_exchange = common_infra.get("trade_exchange")
def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs): def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs):
""" """
Parameters Parameters
@@ -105,7 +63,9 @@ class TWAPStrategy(BaseStrategy):
stock_id=order.stock_id, start_time=trade_start_time, end_time=trade_end_time stock_id=order.stock_id, start_time=trade_start_time, end_time=trade_end_time
): ):
continue continue
_amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(order.factor) _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(
stock_id=order.stock_id, start_time=order.start_time, end_time=order.end_time
)
_order_amount = None _order_amount = None
# considering trade unit # considering trade unit
if _amount_trade_unit is None: if _amount_trade_unit is None:
@@ -141,7 +101,6 @@ class TWAPStrategy(BaseStrategy):
start_time=trade_start_time, start_time=trade_start_time,
end_time=trade_end_time, end_time=trade_end_time,
direction=order.direction, # 1 for buy direction=order.direction, # 1 for buy
factor=order.factor,
) )
order_list.append(_order) order_list.append(_order)
return TradeDecisionWO(order_list=order_list, strategy=self) return TradeDecisionWO(order_list=order_list, strategy=self)
@@ -161,46 +120,6 @@ class SBBStrategyBase(BaseStrategy):
# 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
def __init__(
self,
outer_trade_decision: BaseTradeDecision = None,
trade_exchange: Exchange = None,
level_infra: LevelInfrastructure = None,
common_infra: CommonInfrastructure = None,
):
"""
Parameters
----------
outer_trade_decision : BaseTradeDecision
the trade decision of outer strategy which this startegy relies
trade_exchange : Exchange
exchange that provides market info, used to deal order and generate report
- 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 minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
"""
super(SBBStrategyBase, self).__init__(
outer_trade_decision=outer_trade_decision, level_infra=level_infra, common_infra=common_infra
)
if trade_exchange is not None:
self.trade_exchange = trade_exchange
def reset_common_infra(self, common_infra):
"""
Parameters
----------
common_infra : dict, optional
common infrastructure for backtesting, by default None
- It should include `trade_account`, used to get position
- It should include `trade_exchange`, used to provide market info
"""
super(SBBStrategyBase, self).reset_common_infra(common_infra)
if common_infra.has("trade_exchange"):
self.trade_exchange = common_infra.get("trade_exchange")
def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs): def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs):
""" """
Parameters Parameters
@@ -250,7 +169,9 @@ class SBBStrategyBase(BaseStrategy):
self.trade_trend[order.stock_id] = _pred_trend self.trade_trend[order.stock_id] = _pred_trend
continue continue
# get amount of one trade unit # get amount of one trade unit
_amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(order.factor) _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(
stock_id=order.stock_id, start_time=order.start_time, end_time=order.end_time
)
if _pred_trend == self.TREND_MID: if _pred_trend == self.TREND_MID:
_order_amount = None _order_amount = None
# considering trade unit # considering trade unit
@@ -283,7 +204,6 @@ class SBBStrategyBase(BaseStrategy):
start_time=trade_start_time, start_time=trade_start_time,
end_time=trade_end_time, end_time=trade_end_time,
direction=order.direction, direction=order.direction,
factor=order.factor,
) )
order_list.append(_order) order_list.append(_order)
@@ -330,7 +250,6 @@ class SBBStrategyBase(BaseStrategy):
start_time=trade_start_time, start_time=trade_start_time,
end_time=trade_end_time, end_time=trade_end_time,
direction=order.direction, # 1 for buy direction=order.direction, # 1 for buy
factor=order.factor,
) )
order_list.append(_order) order_list.append(_order)
else: else:
@@ -349,7 +268,6 @@ class SBBStrategyBase(BaseStrategy):
start_time=trade_start_time, start_time=trade_start_time,
end_time=trade_end_time, end_time=trade_end_time,
direction=order.direction, # 1 for buy direction=order.direction, # 1 for buy
factor=order.factor,
) )
order_list.append(_order) order_list.append(_order)
@@ -395,7 +313,9 @@ class SBBStrategyEMA(SBBStrategyBase):
if isinstance(instruments, str): if isinstance(instruments, str):
self.instruments = D.instruments(instruments) self.instruments = D.instruments(instruments)
self.freq = freq self.freq = freq
super(SBBStrategyEMA, self).__init__(outer_trade_decision, trade_exchange, level_infra, common_infra, **kwargs) super(SBBStrategyEMA, self).__init__(
outer_trade_decision, level_infra, common_infra, trade_exchange=trade_exchange, **kwargs
)
def _reset_signal(self): def _reset_signal(self):
trade_len = self.trade_calendar.get_trade_len() trade_len = self.trade_calendar.get_trade_len()
@@ -417,14 +337,8 @@ class SBBStrategyEMA(SBBStrategyBase):
reset level-shared infra reset level-shared infra
- After reset the trade calendar, the signal will be changed - After reset the trade calendar, the signal will be changed
""" """
if not hasattr(self, "level_infra"): super().reset_level_infra(level_infra)
self.level_infra = level_infra self._reset_signal()
else:
self.level_infra.update(level_infra)
if level_infra.has("trade_calendar"):
self.trade_calendar = level_infra.get("trade_calendar")
self._reset_signal()
def _pred_price_trend(self, stock_id, pred_start_time=None, pred_end_time=None): def _pred_price_trend(self, stock_id, pred_start_time=None, pred_end_time=None):
# if no signal, return mid trend # if no signal, return mid trend
@@ -484,10 +398,9 @@ class ACStrategy(BaseStrategy):
if isinstance(instruments, str): if isinstance(instruments, str):
self.instruments = D.instruments(instruments) self.instruments = D.instruments(instruments)
self.freq = freq self.freq = freq
super(ACStrategy, self).__init__(outer_trade_decision, level_infra, common_infra, **kwargs) super(ACStrategy, self).__init__(
outer_trade_decision, level_infra, common_infra, trade_exchange=trade_exchange, **kwargs
if trade_exchange is not None: )
self.trade_exchange = trade_exchange
def _reset_signal(self): def _reset_signal(self):
trade_len = self.trade_calendar.get_trade_len() trade_len = self.trade_calendar.get_trade_len()
@@ -506,33 +419,13 @@ class ACStrategy(BaseStrategy):
for stock_id, stock_val in signal_df.groupby(level="instrument"): for stock_id, stock_val in signal_df.groupby(level="instrument"):
self.signal[stock_id] = stock_val["volatility"].droplevel(level="instrument") self.signal[stock_id] = stock_val["volatility"].droplevel(level="instrument")
def reset_common_infra(self, common_infra):
"""
Parameters
----------
common_infra : CommonInfrastructure, optional
common infrastructure for backtesting, by default None
- It should include `trade_account`, used to get position
- It should include `trade_exchange`, used to provide market info
"""
super(ACStrategy, self).reset_common_infra(common_infra)
if common_infra.has("trade_exchange"):
self.trade_exchange = common_infra.get("trade_exchange")
def reset_level_infra(self, level_infra): def reset_level_infra(self, level_infra):
""" """
reset level-shared infra reset level-shared infra
- After reset the trade calendar, the signal will be changed - After reset the trade calendar, the signal will be changed
""" """
if not hasattr(self, "level_infra"): super().reset_level_infra(level_infra)
self.level_infra = level_infra self._reset_signal()
else:
self.level_infra.update(level_infra)
if level_infra.has("trade_calendar"):
self.trade_calendar = level_infra.get("trade_calendar")
self._reset_signal()
def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs): def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs):
""" """
@@ -578,7 +471,9 @@ class ACStrategy(BaseStrategy):
if sig_sam is None or np.isnan(sig_sam): if sig_sam is None or np.isnan(sig_sam):
# no signal, TWAP # no signal, TWAP
_amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(order.factor) _amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(
stock_id=order.stock_id, start_time=order.start_time, end_time=order.end_time
)
if _amount_trade_unit is None: if _amount_trade_unit is None:
# divide the order into equal parts, and trade one part # divide the order into equal parts, and trade one part
_order_amount = self.trade_amount[order.stock_id] / (trade_len - trade_step) _order_amount = self.trade_amount[order.stock_id] / (trade_len - trade_step)
@@ -599,7 +494,9 @@ class ACStrategy(BaseStrategy):
np.sinh(kappa * (trade_len - trade_step)) - np.sinh(kappa * (trade_len - trade_step - 1)) np.sinh(kappa * (trade_len - trade_step)) - np.sinh(kappa * (trade_len - trade_step - 1))
) / np.sinh(kappa * trade_len) ) / np.sinh(kappa * trade_len)
_order_amount = order.amount * amount_ratio _order_amount = order.amount * amount_ratio
_order_amount = self.trade_exchange.round_amount_by_trade_unit(_order_amount, order.factor) _order_amount = self.trade_exchange.round_amount_by_trade_unit(
_order_amount, stock_id=order.stock_id, start_time=order.start_time, end_time=order.end_time
)
if order.direction == order.SELL: if order.direction == order.SELL:
# sell all amount at last # sell all amount at last
@@ -673,8 +570,6 @@ class RandomOrderStrategy(BaseStrategy):
.create( .create(
code=stock_id, code=stock_id,
amount=volume * self.volume_ratio, amount=volume * self.volume_ratio,
start_time=step_time_start,
end_time=step_time_end,
direction=self.direction, direction=self.direction,
) )
) )
@@ -734,9 +629,7 @@ class FileOrderStrategy(BaseStrategy):
execute_result will be ignored in FileOrderStrategy execute_result will be ignored in FileOrderStrategy
""" """
oh: OrderHelper = self.common_infra.get("trade_exchange").get_order_helper() oh: OrderHelper = self.common_infra.get("trade_exchange").get_order_helper()
tc = self.trade_calendar start, _ = self.trade_calendar.get_step_time()
step = tc.get_trade_step()
start, end = tc.get_step_time(step)
# CONVERSION: the bar is indexed by the time # CONVERSION: the bar is indexed by the time
try: try:
df = self.order_df.loc(axis=0)[start] df = self.order_df.loc(axis=0)[start]
@@ -750,8 +643,6 @@ class FileOrderStrategy(BaseStrategy):
code=idx, code=idx,
amount=row["amount"], amount=row["amount"],
direction=Order.parse_dir(row["direction"]), direction=Order.parse_dir(row["direction"]),
start_time=start,
end_time=end,
) )
) )
return TradeDecisionWO(order_list, self, self.trade_range) return TradeDecisionWO(order_list, self, self.trade_range)

View File

@@ -1,7 +1,8 @@
# Copyright (c) Microsoft Corporation. # Copyright (c) Microsoft Corporation.
# Licensed under the MIT License. # Licensed under the MIT License.
from qlib.backtest.exchange import Exchange
from qlib.backtest.position import BasePosition from qlib.backtest.position import BasePosition
from typing import List, Union from typing import List, Tuple, Union
from ..model.base import BaseModel from ..model.base import BaseModel
from ..data.dataset import DatasetH from ..data.dataset import DatasetH
@@ -22,6 +23,7 @@ class BaseStrategy:
outer_trade_decision: BaseTradeDecision = None, outer_trade_decision: BaseTradeDecision = None,
level_infra: LevelInfrastructure = None, level_infra: LevelInfrastructure = None,
common_infra: CommonInfrastructure = None, common_infra: CommonInfrastructure = None,
trade_exchange: Exchange = None,
): ):
""" """
Parameters Parameters
@@ -34,9 +36,18 @@ class BaseStrategy:
level shared infrastructure for backtesting, including trade calendar level shared infrastructure for backtesting, including trade calendar
common_infra : CommonInfrastructure, optional common_infra : CommonInfrastructure, optional
common infrastructure for backtesting, including trade_account, trade_exchange, .etc common infrastructure for backtesting, including trade_account, trade_exchange, .etc
trade_exchange : Exchange
exchange that provides market info, used to deal order and generate report
- 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 minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
""" """
self.reset(level_infra=level_infra, common_infra=common_infra, outer_trade_decision=outer_trade_decision) self._reset(level_infra=level_infra, common_infra=common_infra, outer_trade_decision=outer_trade_decision)
self._trade_exchange = trade_exchange
@property @property
def trade_calendar(self) -> TradeCalendarManager: def trade_calendar(self) -> TradeCalendarManager:
@@ -46,6 +57,11 @@ class BaseStrategy:
def trade_position(self) -> BasePosition: def trade_position(self) -> BasePosition:
return self.common_infra.get("trade_account").current return self.common_infra.get("trade_account").current
@property
def trade_exchange(self) -> Exchange:
"""get trade exchange in a prioritized order"""
return getattr(self, "_trade_exchange", None) or self.common_infra.get("trade_exchange")
def reset_level_infra(self, level_infra: LevelInfrastructure): def reset_level_infra(self, level_infra: LevelInfrastructure):
if not hasattr(self, "level_infra"): if not hasattr(self, "level_infra"):
self.level_infra = level_infra self.level_infra = level_infra
@@ -69,6 +85,24 @@ class BaseStrategy:
- reset `level_infra`, used to reset trade calendar, .etc - reset `level_infra`, used to reset trade calendar, .etc
- reset `common_infra`, used to reset `trade_account`, `trade_exchange`, .etc - reset `common_infra`, used to reset `trade_account`, `trade_exchange`, .etc
- reset `outer_trade_decision`, used to make split decision - reset `outer_trade_decision`, used to make split decision
**NOTE**:
split this function into `reset` and `_reset` will make following cases more convenient
1. Users want to initialize his strategy by overriding `reset`, but they don't want to affect the `_reset` called
when initialization
"""
self._reset(
level_infra=level_infra, common_infra=common_infra, outer_trade_decision=outer_trade_decision, **kwargs
)
def _reset(
self,
level_infra: LevelInfrastructure = None,
common_infra: CommonInfrastructure = None,
outer_trade_decision=None,
):
"""
Please refer to the docs of `reset`
""" """
if level_infra is not None: if level_infra is not None:
self.reset_level_infra(level_infra) self.reset_level_infra(level_infra)
@@ -124,6 +158,36 @@ class BaseStrategy:
# NOTE: normally, user should do something to the strategy due to the change of outer decision # NOTE: normally, user should do something to the strategy due to the change of outer decision
raise NotImplementedError(f"Please implement the `alter_outer_trade_decision` method") raise NotImplementedError(f"Please implement the `alter_outer_trade_decision` method")
# helper methods: not necessary but for convenience
def get_data_cal_avail_range(self, rtype: str = "full") -> Tuple[int, int]:
"""
return data calendar's available decision range for `self` strategy
the range consider following factors
- data calendar in the charge of `self` strategy
- trading range limitation from the decision of outer strategy
related methods
- TradeCalendarManager.get_data_cal_range
- BaseTradeDecision.get_data_cal_range_limit
Parameters
----------
rtype: str
- "full": return the available data index range of the strategy from `start_time` to `end_time`
- "step": return the available data index range of the strategy of current step
Returns
-------
Tuple[int, int]:
the available range both sides are closed
"""
cal_range = self.trade_calendar.get_data_cal_range(rtype=rtype)
if self.outer_trade_decision is None:
raise ValueError(f"There is not limitation for strategy {self}")
range_limit = self.outer_trade_decision.get_data_cal_range_limit(rtype=rtype)
return max(cal_range[0], range_limit[0]), min(cal_range[1], range_limit[1])
class ModelStrategy(BaseStrategy): class ModelStrategy(BaseStrategy):
"""Model-based trading strategy, use model to make predictions for trading""" """Model-based trading strategy, use model to make predictions for trading"""

View File

@@ -210,10 +210,13 @@ def get_cls_kwargs(config: Union[dict, str], default_module: Union[str, ModuleTy
the class object and it's arguments. the class object and it's arguments.
""" """
if isinstance(config, dict): if isinstance(config, dict):
module = get_module_by_module_path(config.get("module_path", default_module)) if isinstance(config["class"], str):
module = get_module_by_module_path(config.get("module_path", default_module))
# raise AttributeError # raise AttributeError
klass = getattr(module, config["class"]) klass = getattr(module, config["class"])
else:
klass = config["class"] # the class type itself is passed in
kwargs = config.get("kwargs", {}) kwargs = config.get("kwargs", {})
elif isinstance(config, str): elif isinstance(config, str):
module = get_module_by_module_path(default_module) module = get_module_by_module_path(default_module)
@@ -235,11 +238,17 @@ def init_instance_by_config(
---------- ----------
config : Union[str, dict, object] config : Union[str, dict, object]
dict example. dict example.
case 1)
{ {
'class': 'ClassName', 'class': 'ClassName',
'kwargs': dict, # It is optional. {} will be used if not given 'kwargs': dict, # It is optional. {} will be used if not given
'model_path': path, # It is optional if module is given 'model_path': path, # It is optional if module is given
} }
case 2)
{
'class': <The class it self>,
'kwargs': dict, # It is optional. {} will be used if not given
}
str example. str example.
1) specify a pickle object 1) specify a pickle object
- path like 'file:///<path to pickle file>/obj.pkl' - path like 'file:///<path to pickle file>/obj.pkl'

View File

@@ -160,5 +160,32 @@ def cal_sam_minute(x: pd.Timestamp, sam_minutes: int) -> pd.Timestamp:
return concat_date_time(date, new_time) return concat_date_time(date, new_time)
def epsilon_change(datetime: pd.Timestamp, direction: str = "backward") -> pd.Timestamp:
"""
change the time by infinitely small quantity.
Parameters
----------
datetime : pd.Timestamp
the original time
direction : str
the direction the time are going to
- "backward" for going to history
- "forward" for going to the future
Returns
-------
pd.Timestamp:
the shifted time
"""
if direction == "backward":
return datetime - pd.Timedelta(seconds=1)
elif direction == "forward":
return datetime + pd.Timedelta(seconds=1)
else:
raise ValueError("Wrong input")
if __name__ == "__main__": if __name__ == "__main__":
print(get_day_min_idx_range("8:30", "14:59", "10min")) print(get_day_min_idx_range("8:30", "14:59", "10min"))