Section outline

  • 投资组合策略:投资组合管理


    简介

    投资组合策略旨在采用不同的投资组合策略,这意味着用户可以基于预测模型的预测分数采用不同的算法来生成投资组合。用户可以通过 Workflow 模块在自动化工作流中使用投资组合策略,请参阅工作流:工作流管理

    由于 Qlib 中的组件采用松散耦合的设计,投资组合策略也可以作为一个独立的模块使用。

    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 接口,其处理过程如下。

    1. 调用 generate_target_weight_position 方法生成目标头寸。

    2. 从目标头寸生成股票的目标数量。

    3. 从目标数量生成订单列表。

    用户可以继承 WeightStrategyBase 并实现 generate_target_weight_position 接口来自定义他们的策略类,该策略类只关注目标头寸。


    已实现的策略

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

    TopkDropoutStrategy

    TopkDropoutStrategyBaseStrategy 的子类,并实现了 generate_order_list 接口,其过程如下。

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

      注意

      Topk-Drop 算法有两个参数:

      • Topk:持有的股票数量。

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

        通常,当前持有的股票数量是 Topk,除了交易开始时期为零。对于每个交易日,设 d 是当前持有的股票中,按预测分数从高到低排名时排名 gt K 的股票数量。然后将卖出当前持有的 d 只预测分数最差的股票,并买入相同数量的未持有但预测分数最佳的股票。

        通常,d = Drop,尤其是在候选股票池很大,K 很大且 Drop 很小的情况下。

        在大多数情况下,TopkDrop 算法每天卖出和买入 Drop 只股票,这使得换手率为 2 * Drop / K。

        下图说明了一个典型的场景。

    2. 从目标数量生成订单列表。

    EnhancedIndexingStrategy

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

    更多信息请参阅 qlib.contrib.strategy.signal_strategy.EnhancedIndexingStrategy 和 qlib.contrib.strategy.optimizer.enhanced_indexing.EnhancedIndexingOptimizer。


    用法与示例

    首先,用户可以创建一个模型来获取交易信号(在以下情况下变量名为 pred_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

    预测模型模块可以进行预测,请参阅预测模型:模型训练与预测。

    通常,预测分数是模型的输出。但有些模型是从不同尺度的标签中学习的。因此,预测分数的尺度可能与您的预期(例如,成分股的收益)不同。

    Qlib 没有添加一个步骤来将预测分数统一缩放到一个尺度,原因如下。

    • 因为并非每个交易策略都关心尺度(例如,TopkDropoutStrategy 只关心排名)。因此,策略有责任重新缩放预测分数(例如,一些基于投资组合优化的策略可能需要有意义的尺度)。

    • 模型可以灵活地定义目标、损失和数据处理。因此,我们不认为仅仅基于模型的输出来直接重新缩放它有一个万能的方法。如果您想将其重新缩放到一些有意义的值(例如,股票收益),一个直观的解决方案是为您模型的近期输出和您近期的目标值创建一个回归模型。

    运行回测

    在大多数情况下,用户可以使用 backtest_daily 回测他们的投资组合管理策略。

    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=<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)
    

    如果用户希望以更详细的方式控制他们的策略(例如,用户有一个更高级的执行器版本),用户可以遵循这个示例。

    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=<qlib data dir>)
    
    CSI300_BENCH = "SH000300"
    # Benchmark 用于计算您的策略的超额收益。
    # 它的数据格式将像**一个普通成分股**。
    # 例如,您可以使用以下代码查询其数据
    # `D.features(["SH000300"], ["$close"], start_time='2010-01-01', end_time='2017-12-31', freq='day')`
    # 它与参数 `market` 不同,`market` 表示一个股票池(例如,**一组**股票,如 csi300)
    # 例如,您可以使用以下代码查询股票市场的所有数据。
    # `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_obj = TopkDropoutStrategy(**STRATEGY_CONFIG)
    # 执行器对象
    executor_obj = executor.SimulatorExecutor(**EXECUTOR_CONFIG)
    # 回测
    portfolio_metric_dict, indicator_dict = backtest(executor=executor_obj, strategy=strategy_obj, **backtest_config)
    analysis_freq = "{0}{1}".format(*Freq.parse(FREQ))
    # 回测信息
    report_normal, positions_normal = portfolio_metric_dict.get(analysis_freq)
    # 分析
    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
    # 记录指标
    analysis_dict = flatten_dict(analysis_df["risk"].unstack().T.to_dict())
    # 打印结果
    pprint(f"以下是基准收益({analysis_freq})的分析结果。")
    pprint(risk_analysis(report_normal["bench"], freq=analysis_freq))
    pprint(f"以下是无成本超额收益({analysis_freq})的分析结果。")
    pprint(analysis["excess_return_without_cost"])
    pprint(f"以下是有成本超额收益({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)


    参考

    要了解更多关于预测模型输出的预测分数 pred_score 的信息,请参阅预测模型:模型训练与预测