.. _strategy: ======================================== 组合策略:投资组合管理 ======================================== .. currentmodule:: qlib 简介 ============ ``Portfolio Strategy`` 旨在采用不同的投资组合策略,这意味着用户可以根据 ``Forecast Model`` 的预测分数采用不同算法生成投资组合。用户可以通过 ``Workflow`` 模块在自动化工作流中使用 ``Portfolio Strategy``,详情请参阅 `工作流:工作流管理 `_。 由于 ``Qlib`` 的组件采用松耦合方式设计,``Portfolio Strategy`` 也可以作为独立模块使用。 ``Qlib`` 提供了多种已实现的组合策略。同时 ``Qlib`` 支持自定义策略,用户可根据自身需求定制策略。 当用户指定模型(预测信号)和策略后,运行回测将帮助用户检验自定义模型(预测信号)/策略的表现。 基类与接口 ====================== BaseStrategy ------------ Qlib 提供了基类 ``qlib.strategy.base.BaseStrategy``。所有策略类都需要继承该基类并实现其接口。 - `generate_trade_decision` generate_trade_decision 是关键接口,用于在每个交易时段生成交易决策。 该方法的调用频率取决于执行器频率(默认 "time_per_step"="day")。但实际交易频率可由用户实现决定。 例如,若用户希望按周交易而执行器中 `time_per_step` 为 "day",用户可每周返回非空的 TradeDecision(否则返回空值如 `此例 `_)。 用户可继承 `BaseStrategy` 来自定义策略类。 WeightStrategyBase ------------------ Qlib 还提供了一个类 ``qlib.contrib.strategy.WeightStrategyBase``,它是 `BaseStrategy` 的子类。 `WeightStrategyBase` 仅关注目标仓位,并基于仓位自动生成订单列表。它提供了 `generate_target_weight_position` 接口。 - `generate_target_weight_position` - 根据当前仓位和交易日期生成目标仓位。输出权重分布中不考虑现金部分 - 返回目标仓位 .. note:: 这里的 `目标仓位` 指的是总资产的目标百分比 `WeightStrategyBase` 实现了 `generate_order_list` 接口,其处理流程如下: - 调用 `generate_target_weight_position` 方法生成目标仓位 - 从目标仓位生成股票的目标数量 - 根据目标数量生成订单列表 用户可以继承 `WeightStrategyBase` 并实现 `generate_target_weight_position` 接口来自定义策略类,该类仅需关注目标仓位。 已实现的策略 ==================== Qlib 提供了一个已实现的策略类 `TopkDropoutStrategy`。 TopkDropoutStrategy ------------------- `TopkDropoutStrategy` 是 `BaseStrategy` 的子类,实现了 `generate_order_list` 接口,其处理流程如下: - 采用 ``Topk-Drop`` 算法计算每只股票的目标数量 .. note:: ``Topk-Drop`` 算法有两个参数: - `Topk`: 持仓股票数量 - `Drop`: 每个交易日卖出的股票数量 通常情况下,当前持仓股票数量为 `Topk`,除了交易开始阶段可能为零。 对于每个交易日,设 $d$ 为当前持仓且按预测分数从高到低排名时排名 $\gt K$ 的股票数量。 那么将卖出当前持仓中 `预测分数` 最差的 `d` 只股票,并买入相同数量未持仓但 `预测分数` 最好的股票。 通常情况下 $d=$`Drop`,特别是当候选股票池较大、$K$ 较大且 `Drop` 较小时。 在大多数情况下,``TopkDrop`` 算法每个交易日会卖出和买入 `Drop` 只股票,产生的换手率为 2$\times$`Drop`/$K$。 下图展示了一个典型场景。 .. image:: ../_static/img/topk_drop.png :alt: Topk-Drop - Generate the order list from the target amount EnhancedIndexingStrategy ------------------------ `EnhancedIndexingStrategy` 增强型指数化策略结合了主动管理和被动管理的艺术, 旨在控制风险敞口(又称跟踪误差)的同时,在投资组合回报方面超越基准指数(如标普500)。 更多信息请参考 `qlib.contrib.strategy.signal_strategy.EnhancedIndexingStrategy` 和 `qlib.contrib.strategy.optimizer.enhanced_indexing.EnhancedIndexingOptimizer`。 使用与示例 =============== 首先,用户可以创建一个模型来获取交易信号(在以下案例中变量名为 ``pred_score``)。 预测分数 ---------------- `prediction score` 是一个pandas DataFrame。其索引为,且必须 包含一个 `score` 列。 预测样本示例如下。 .. code-block:: python datetime instrument score 2019-01-04 SH600000 -0.505488 2019-01-04 SZ002531 -0.320391 2019-01-04 SZ000999 0.583808 2019-01-04 SZ300569 0.819628 2019-01-04 SZ001696 -0.137140 ... ... 2019-04-30 SZ000996 -1.027618 2019-04-30 SH603127 0.225677 2019-04-30 SH603126 0.462443 2019-04-30 SH603133 -0.302460 2019-04-30 SZ300760 -0.126383 ``Forecast Model`` 模块可以进行预测,详情请参阅 `预测模型:模型训练与预测 `_。 通常情况下,预测分数是模型的输出结果。但某些模型是从不同量级的标签中学习得到的,因此预测分数的量级可能与您的预期不符(例如标的资产的收益率)。 Qlib 没有添加将预测分数统一量纲化的步骤,原因如下: - 并非所有交易策略都关注量级(例如 TopkDropoutStrategy 只关心排序)。因此策略本身需要负责对预测分数进行重新标度(例如某些基于组合优化的策略可能需要有实际意义的量级)。 - 模型可以灵活定义目标函数、损失函数和数据处理方式。因此我们认为不存在一种万能方法可以直接基于模型输出来进行反向标度。如果您希望将其标度回有实际意义的值(例如股票收益率),直观的解决方案是为模型的近期输出和您的近期目标值创建一个回归模型。 运行回测 ---------------- - 在大多数情况下,用户可以使用 ``backtest_daily`` 来回测其投资组合管理策略。 .. code-block:: python from pprint import pprint import qlib import pandas as pd from qlib.utils.time import Freq from qlib.utils import flatten_dict from qlib.contrib.evaluate import backtest_daily from qlib.contrib.evaluate import risk_analysis from qlib.contrib.strategy import TopkDropoutStrategy # init qlib qlib.init(provider_uri=) CSI300_BENCH = "SH000300" STRATEGY_CONFIG = { "topk": 50, "n_drop": 5, # pred_score, pd.Series "signal": pred_score, } strategy_obj = TopkDropoutStrategy(**STRATEGY_CONFIG) report_normal, positions_normal = backtest_daily( start_time="2017-01-01", end_time="2020-08-01", strategy=strategy_obj ) analysis = dict() # default frequency will be daily (i.e. "day") analysis["excess_return_without_cost"] = risk_analysis(report_normal["return"] - report_normal["bench"]) analysis["excess_return_with_cost"] = risk_analysis(report_normal["return"] - report_normal["bench"] - report_normal["cost"]) analysis_df = pd.concat(analysis) # type: pd.DataFrame pprint(analysis_df) - 如果用户希望更精细地控制策略(例如用户拥有更高级的执行器版本),可参考此示例。 .. code-block:: python from pprint import pprint import qlib import pandas as pd from qlib.utils.time import Freq from qlib.utils import flatten_dict from qlib.backtest import backtest, executor from qlib.contrib.evaluate import risk_analysis from qlib.contrib.strategy import TopkDropoutStrategy # init qlib qlib.init(provider_uri=) CSI300_BENCH = "SH000300" # Benchmark is for calculating the excess return of your strategy. # Its data format will be like **ONE normal instrument**. # For example, you can query its data with the code below # `D.features(["SH000300"], ["$close"], start_time='2010-01-01', end_time='2017-12-31', freq='day')` # It is different from the argument `market`, which indicates a universe of stocks (e.g. **A SET** of stocks like csi300) # For example, you can query all data from a stock market with the code below. # ` D.features(D.instruments(market='csi300'), ["$close"], start_time='2010-01-01', end_time='2017-12-31', freq='day')` FREQ = "day" STRATEGY_CONFIG = { "topk": 50, "n_drop": 5, # pred_score, pd.Series "signal": pred_score, } EXECUTOR_CONFIG = { "time_per_step": "day", "generate_portfolio_metrics": True, } backtest_config = { "start_time": "2017-01-01", "end_time": "2020-08-01", "account": 100000000, "benchmark": CSI300_BENCH, "exchange_kwargs": { "freq": FREQ, "limit_threshold": 0.095, "deal_price": "close", "open_cost": 0.0005, "close_cost": 0.0015, "min_cost": 5, }, } # strategy object strategy_obj = TopkDropoutStrategy(**STRATEGY_CONFIG) # executor object executor_obj = executor.SimulatorExecutor(**EXECUTOR_CONFIG) # backtest portfolio_metric_dict, indicator_dict = backtest(executor=executor_obj, strategy=strategy_obj, **backtest_config) analysis_freq = "{0}{1}".format(*Freq.parse(FREQ)) # backtest info report_normal, positions_normal = portfolio_metric_dict.get(analysis_freq) # analysis analysis = dict() analysis["excess_return_without_cost"] = risk_analysis( report_normal["return"] - report_normal["bench"], freq=analysis_freq ) analysis["excess_return_with_cost"] = risk_analysis( report_normal["return"] - report_normal["bench"] - report_normal["cost"], freq=analysis_freq ) analysis_df = pd.concat(analysis) # type: pd.DataFrame # log metrics analysis_dict = flatten_dict(analysis_df["risk"].unstack().T.to_dict()) # print out results pprint(f"The following are analysis results of benchmark return({analysis_freq}).") pprint(risk_analysis(report_normal["bench"], freq=analysis_freq)) pprint(f"The following are analysis results of the excess return without cost({analysis_freq}).") pprint(analysis["excess_return_without_cost"]) pprint(f"The following are analysis results of the excess return with cost({analysis_freq}).") pprint(analysis["excess_return_with_cost"]) 结果 ------ 回测结果呈现以下形式: .. code-block:: python risk excess_return_without_cost mean 0.000605 std 0.005481 annualized_return 0.152373 information_ratio 1.751319 max_drawdown -0.059055 excess_return_with_cost mean 0.000410 std 0.005478 annualized_return 0.103265 information_ratio 1.187411 max_drawdown -0.075024 - `excess_return_without_cost` - `mean` 无成本情况下的 `CAR`(累计异常收益)均值 - `std` 无成本情况下 `CAR`(累计异常收益)的 `标准差` - `annualized_return` 无成本情况下 `CAR`(累计异常收益)的 `年化收益率` - `information_ratio` 无成本情况下的 `信息比率`,详见 `信息比率 – IR `_ - `max_drawdown` 无成本情况下 `CAR`(累计异常收益)的 `最大回撤`,详见 `最大回撤 (MDD) `_ - `excess_return_with_cost` - `mean` 含成本情况下的 `CAR`(累计异常收益)均值 - `std` 含成本情况下 `CAR`(累计异常收益)的 `标准差` - `annualized_return` 含成本情况下 `CAR`(累计异常收益)的 `年化收益率` - `information_ratio` 含成本情况下的 `信息比率`,详见 `信息比率 – IR `_ - `max_drawdown` 含成本情况下 `CAR`(累计异常收益)的 `最大回撤`,详见 `最大回撤 (MDD) `_ 参考 ========= 如需了解 ``Forecast Model`` 输出的 `预测分数` `pred_score` 的更多信息,请参阅 `预测模型:模型训练与预测 `_。