diff --git a/qlib/backtest/exchange.py b/qlib/backtest/exchange.py index 69262fcbbad..e24b6e57ee3 100644 --- a/qlib/backtest/exchange.py +++ b/qlib/backtest/exchange.py @@ -69,12 +69,23 @@ def __init__( :param subscribe_fields: list, subscribe fields. This expressions will be added to the query and `self.quote`. 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]: (, - ) - `False` value indicates the stock is tradable - `True` value indicates the stock is limited and not tradable + Controls whether stocks hit price-limit restrictions. + Three modes are supported: + + 1) ``None``: no price-limit restrictions are applied (default). + Only suspension status affects tradability. + + 2) ``float`` (e.g. ``0.095``): a **static threshold** based on + the daily price change (``$change``). A stock is marked as + buy-limited when ``$change >= threshold`` and sell-limited + when ``$change <= -threshold``. + Default values by region: China 0.095, US None, Taiwan 0.1. + + 3) ``Tuple[str, str]``: a pair of **qlib data expressions** + ``(buy_limit_expr, sell_limit_expr)``. Each expression is + evaluated and cast to bool — ``True`` means the stock is + **limited** (not tradable), ``False`` means tradable. + Example: ``("$ask == 0", "$bid == 0")``. :param volume_threshold: Union[ Dict[ "all": ("cum" or "current", limit_str), @@ -519,12 +530,22 @@ def get_factor( start_time: pd.Timestamp, end_time: pd.Timestamp, ) -> Optional[float]: - """ + """Return the adjustment factor for a stock in the given time range. + + The ``$factor`` field represents the **cumulative adjustment factor** + for stock splits, dividends, and other corporate actions. It is used + by ``round_amount_by_trade_unit`` to convert between adjusted and + unadjusted share amounts so that orders can be rounded to the + market's minimum trading unit (e.g. 100 shares for China A-shares). + + Without ``$factor`` in the quote data, ``round_amount_by_trade_unit`` + will raise an error when ``trade_unit`` is set. + Returns ------- Optional[float]: - `None`: if the stock is suspended `None` may be returned - `float`: return factor if the factor exists + ``None`` if the stock is suspended or not found. + ``float`` the adjustment factor value otherwise. """ 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(): diff --git a/qlib/contrib/strategy/order_generator.py b/qlib/contrib/strategy/order_generator.py index fe0d048bfdd..a9a7f11a947 100644 --- a/qlib/contrib/strategy/order_generator.py +++ b/qlib/contrib/strategy/order_generator.py @@ -12,6 +12,29 @@ class OrderGenerator: + """Base class for order generators used by ``WeightStrategyBase``. + + An order generator converts a target weight position (a mapping from + stock_id to portfolio weight) into a concrete list of ``Order`` objects + that can be executed by the ``Exchange``. + + Two built-in implementations are provided: + + * ``OrderGenWInteract`` – uses trade-date market information (prices, + tradability) when building orders. It automatically **re-normalises** + weights across *tradable* stocks so that the full allocatable capital + is utilised. Use this when the executor can interact with the exchange + at execution time. + * ``OrderGenWOInteract`` – generates orders **without** accessing + trade-date information. It relies on the prediction-date close price + (or the price recorded in the current position) to estimate order + amounts. This is the default used by ``WeightStrategyBase``. + + Subclass this and override + ``generate_order_list_from_target_weight_position`` to implement custom + order generation logic. + """ + def generate_order_list_from_target_weight_position( self, current: Position, @@ -23,32 +46,56 @@ def generate_order_list_from_target_weight_position( trade_start_time: pd.Timestamp, trade_end_time: pd.Timestamp, ) -> list: - """generate_order_list_from_target_weight_position + """Generate a list of orders from the target weight position. - :param current: The current position + :param current: The current portfolio position. :type current: Position - :param trade_exchange: + :param trade_exchange: The exchange instance providing market data, + tradability checks, and order-rounding utilities. :type trade_exchange: Exchange - :param target_weight_position: {stock_id : weight} + :param target_weight_position: Mapping ``{stock_id: weight}`` where + each weight is a float in ``(0, 1)`` representing the desired + portfolio proportion for that stock. :type target_weight_position: dict - :param risk_degree: + :param risk_degree: Fraction of total portfolio value that may be + allocated to risky assets (stocks). ``1.0`` means fully invested. :type risk_degree: float - :param pred_start_time: + :param pred_start_time: Start of the prediction time window. :type pred_start_time: pd.Timestamp - :param pred_end_time: + :param pred_end_time: End of the prediction time window. :type pred_end_time: pd.Timestamp - :param trade_start_time: + :param trade_start_time: Start of the actual trading time window. :type trade_start_time: pd.Timestamp - :param trade_end_time: + :param trade_end_time: End of the actual trading time window. :type trade_end_time: pd.Timestamp :rtype: list + :returns: A list of ``Order`` objects. """ raise NotImplementedError() class OrderGenWInteract(OrderGenerator): - """Order Generator With Interact""" + """Order generator that uses trade-date market information. + + This generator **interacts** with the exchange at execution time to + obtain accurate trade-date prices and tradability status. It + re-normalises the target weights so that the full tradable capital is + distributed only among stocks that are actually tradable on the trade + date. + + Key behaviour: + * Calls ``Exchange.generate_amount_position_from_weight_position`` which + divides cash among tradable stocks proportionally to their weights, + effectively **ignoring suspended or limited stocks** and + redistributing their weight to the remaining tradable stocks. + * This ensures full capital utilisation when some stocks become + untradable between the prediction date and the trade date. + + See Also + -------- + OrderGenWOInteract : Alternative that does **not** use trade-date data. + """ def generate_order_list_from_target_weight_position( self, @@ -61,31 +108,32 @@ def generate_order_list_from_target_weight_position( trade_start_time: pd.Timestamp, trade_end_time: pd.Timestamp, ) -> list: - """generate_order_list_from_target_weight_position + """Generate orders using trade-date prices and tradability data. - No adjustment for for the nontradable share. - All the tadable value is assigned to the tadable stock according to the weight. - if interact == True, will use the price at trade date to generate order list - else, will only use the price before the trade date to generate order list + The tradable portfolio value is computed from the current position + valued at trade-date prices. Weights are then allocated only across + stocks that are tradable on the trade date, so the full allocatable + capital is utilised even when some target stocks are suspended. - :param current: + :param current: The current portfolio position. :type current: Position - :param trade_exchange: + :param trade_exchange: Exchange providing trade-date market data. :type trade_exchange: Exchange - :param target_weight_position: + :param target_weight_position: ``{stock_id: weight}`` mapping. :type target_weight_position: dict - :param risk_degree: + :param risk_degree: Fraction of portfolio allocated to stocks. :type risk_degree: float - :param pred_start_time: + :param pred_start_time: Start of the prediction window. :type pred_start_time: pd.Timestamp - :param pred_end_time: + :param pred_end_time: End of the prediction window. :type pred_end_time: pd.Timestamp - :param trade_start_time: + :param trade_start_time: Start of the trading window. :type trade_start_time: pd.Timestamp - :param trade_end_time: + :param trade_end_time: End of the trading window. :type trade_end_time: pd.Timestamp :rtype: list + :returns: A list of ``Order`` objects. """ if target_weight_position is None: return [] @@ -140,7 +188,29 @@ def generate_order_list_from_target_weight_position( class OrderGenWOInteract(OrderGenerator): - """Order Generator Without Interact""" + """Order generator that does **not** use trade-date market information. + + This is the **default** order generator for ``WeightStrategyBase``. + + Because trade-date prices are unavailable at decision time, this + generator estimates order amounts using: + + 1. The **prediction-date close price** (``$close`` at ``pred_date``) for + stocks that are tradable on both the prediction date and the trade + date. + 2. The **price recorded in the current position** for stocks that are + currently held but not tradable on the prediction date. + + Unlike ``OrderGenWInteract``, this generator does **not** re-normalise + weights across tradable stocks. Stocks that are untradable on the trade + date are simply skipped, which may result in less than full capital + utilisation. + + See Also + -------- + OrderGenWInteract : Alternative that re-normalises weights using + trade-date data for full capital utilisation. + """ def generate_order_list_from_target_weight_position( self, @@ -153,33 +223,32 @@ def generate_order_list_from_target_weight_position( trade_start_time: pd.Timestamp, trade_end_time: pd.Timestamp, ) -> list: - """generate_order_list_from_target_weight_position + """Generate orders without accessing trade-date information. - generate order list directly not using the information (e.g. whether can be traded, the accurate trade price) - at trade date. - In target weight position, generating order list need to know the price of objective stock in trade date, - but we cannot get that - value when do not interact with exchange, so we check the %close price at pred_date or price recorded - in current position. + Order amounts are estimated from prediction-date close prices or + prices recorded in the current position. Stocks that are untradable + on either the prediction date or the trade date are skipped (not + re-allocated to other stocks). - :param current: + :param current: The current portfolio position. :type current: Position - :param trade_exchange: + :param trade_exchange: Exchange providing market data. :type trade_exchange: Exchange - :param target_weight_position: + :param target_weight_position: ``{stock_id: weight}`` mapping. :type target_weight_position: dict - :param risk_degree: + :param risk_degree: Fraction of portfolio allocated to stocks. :type risk_degree: float - :param pred_start_time: + :param pred_start_time: Start of the prediction window. :type pred_start_time: pd.Timestamp - :param pred_end_time: + :param pred_end_time: End of the prediction window. :type pred_end_time: pd.Timestamp - :param trade_start_time: + :param trade_start_time: Start of the trading window. :type trade_start_time: pd.Timestamp - :param trade_end_time: + :param trade_end_time: End of the trading window. :type trade_end_time: pd.Timestamp - :rtype: list of generated orders + :rtype: list + :returns: A list of ``Order`` objects. """ if target_weight_position is None: return [] diff --git a/qlib/contrib/strategy/signal_strategy.py b/qlib/contrib/strategy/signal_strategy.py index bad19ddfdc9..af5c0cc43de 100644 --- a/qlib/contrib/strategy/signal_strategy.py +++ b/qlib/contrib/strategy/signal_strategy.py @@ -296,6 +296,35 @@ def filter_stock(li): class WeightStrategyBase(BaseSignalStrategy): + """Base class for portfolio strategies that express decisions as target weights. + + Subclasses must implement ``generate_target_weight_position`` to return + a ``{stock_id: weight}`` dict. The base class then delegates to an + **order generator** to convert weights into executable ``Order`` objects. + + Order generators + ~~~~~~~~~~~~~~~~ + The ``order_generator_cls_or_obj`` parameter controls how weights are + translated into orders. Two built-in options are provided: + + * ``OrderGenWOInteract`` (**default**) – generates orders using + prediction-date prices only. Untradable stocks are skipped, so + capital may not be fully utilised when stocks become suspended between + prediction and execution. + * ``OrderGenWInteract`` – uses trade-date prices and **re-normalises** + weights across tradable stocks, ensuring full capital utilisation. Use + this when the executor has access to real-time market data. + + ``factor`` field and round-lot trading + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + For markets that require integer-lot trading (e.g. 100-share lots in + China A-shares), the ``Exchange`` uses the ``$factor`` field from quote + data together with ``trade_unit`` (set via region config, e.g. 100 for + China) to round order amounts. Without ``$factor`` in the data, orders + may contain fractional share amounts that fail during execution. Ensure + that your data includes a ``$factor`` column when ``trade_unit`` is set. + """ + # TODO: # 1. Supporting leverage the get_range_limit result from the decision # 2. Supporting alter_outer_trade_decision @@ -307,6 +336,14 @@ def __init__( **kwargs, ): """ + Parameters + ---------- + order_generator_cls_or_obj : type or OrderGenerator, optional + The order generator class or instance used to convert target + weight positions into order lists. Defaults to + ``OrderGenWOInteract``. Pass ``OrderGenWInteract`` (or an + instance of it) for weight re-normalisation across tradable + stocks. signal : the information to describe a signal. Please refer to the docs of `qlib.backtest.signal.create_signal_from` the decision of the strategy will base on the given signal @@ -314,7 +351,7 @@ def __init__( 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. + - It allows different trade_exchanges in different executions. - For example: - In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it runs faster.