Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions qlib/backtest/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]: (<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
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),
Expand Down Expand Up @@ -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():
Expand Down
149 changes: 109 additions & 40 deletions qlib/contrib/strategy/order_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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 []
Expand Down Expand Up @@ -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,
Expand All @@ -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 []
Expand Down
39 changes: 38 additions & 1 deletion qlib/contrib/strategy/signal_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -307,14 +336,22 @@ 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
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.
- 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.
Expand Down