组合策略:投资组合管理

简介

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
    • 根据当前仓位和交易日期生成目标仓位。输出权重分布中不考虑现金部分

    • 返回目标仓位

    备注

    这里的 目标仓位 指的是总资产的目标百分比

WeightStrategyBase 实现了 generate_order_list 接口,其处理流程如下:

  • 调用 generate_target_weight_position 方法生成目标仓位

  • 从目标仓位生成股票的目标数量

  • 根据目标数量生成订单列表

用户可以继承 WeightStrategyBase 并实现 generate_target_weight_position 接口来自定义策略类,该类仅需关注目标仓位。

已实现的策略

Qlib 提供了一个已实现的策略类 TopkDropoutStrategy

TopkDropoutStrategy

TopkDropoutStrategyBaseStrategy 的子类,实现了 generate_order_list 接口,其处理流程如下:

  • 采用 Topk-Drop 算法计算每只股票的目标数量

    备注

    Topk-Drop 算法有两个参数:

    • Topk: 持仓股票数量

    • Drop: 每个交易日卖出的股票数量

    通常情况下,当前持仓股票数量为 Topk,除了交易开始阶段可能为零。 对于每个交易日,设 $d$ 为当前持仓且按预测分数从高到低排名时排名 $gt K$ 的股票数量。 那么将卖出当前持仓中 预测分数 最差的 d 只股票,并买入相同数量未持仓但 预测分数 最好的股票。

    通常情况下 $d=$`Drop`,特别是当候选股票池较大、$K$ 较大且 Drop 较小时。

    在大多数情况下,TopkDrop 算法每个交易日会卖出和买入 Drop 只股票,产生的换手率为 2$times$`Drop`/$K$。

    下图展示了一个典型场景。

    Topk-Drop
  • Generate the order list from the target amount

EnhancedIndexingStrategy

EnhancedIndexingStrategy 增强型指数化策略结合了主动管理和被动管理的艺术, 旨在控制风险敞口(又称跟踪误差)的同时,在投资组合回报方面超越基准指数(如标普500)。

更多信息请参考 qlib.contrib.strategy.signal_strategy.EnhancedIndexingStrategyqlib.contrib.strategy.optimizer.enhanced_indexing.EnhancedIndexingOptimizer

使用与示例

首先,用户可以创建一个模型来获取交易信号(在以下案例中变量名为 pred_score)。

预测分数

prediction score 是一个pandas DataFrame。其索引为<datetime(pd.Timestamp), instrument(str)>,且必须 包含一个 score 列。

预测样本示例如下。

  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 来回测其投资组合管理策略。

    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=<qlib data dir>)
    
    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)
    
  • 如果用户希望更精细地控制策略(例如用户拥有更高级的执行器版本),可参考此示例。

    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=<qlib data dir>)
    
    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"])
    

结果

回测结果呈现以下形式:

                                                  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 的更多信息,请参阅 预测模型:模型训练与预测