投资组合策略:投资组合管理
介绍
投资组合策略 旨在采用不同的投资组合策略,这意味着用户可以根据 预测模型 的预测分数采用不同的算法生成投资组合。用户可以通过 工作流 模块在自动化工作流中使用 投资组合策略,请参阅 工作流:工作流管理。
由于 Qlib 中的组件以松耦合的方式设计,投资组合策略 也可以作为独立模块使用。
Qlib 提供了几种已实现的投资组合策略。此外,Qlib 支持自定义策略,用户可以根据自己的需求自定义策略。
在用户指定模型(预测信号)和策略后,运行回测将帮助用户检查自定义模型(预测信号)/策略的表现。
基类与接口
基础策略
Qlib 提供了一个基类 qlib.strategy.base.BaseStrategy。所有策略类需要继承该基类并实现其接口。
- generate_trade_decision
generate_trade_decision 是一个关键接口,用于在每个交易条中生成交易决策。调用此方法的频率取决于执行器的频率(默认情况下
time_per_step为day)。但交易频率可以由用户的实现决定。例如,如果用户希望在每周进行交易,而执行器中的 time_per_step 为day,用户可以每周返回非空的 TradeDecision(否则返回空,如 this)。
用户可以继承 BaseStrategy 来自定义他们的策略类。
权重策略基础
Qlib 还提供了一个类 qlib.contrib.strategy.WeightStrategyBase,它是 BaseStrategy 的子类。
WeightStrategyBase 仅关注目标头寸,并根据头寸自动生成订单列表。它提供了 generate_target_weight_position 接口。
- 生成目标权重位置
根据当前持仓和交易日期生成目标持仓。输出权重分布时不考虑现金。
返回目标持仓。
备注
这里的 目标持仓 指的是总资产的目标百分比。
WeightStrategyBase 实现了接口 generate_order_list,其流程如下。
调用 generate_target_weight_position 方法生成目标持仓。
根据目标持仓生成股票的目标数量。
根据目标数量生成订单列表。
用户可以继承 WeightStrategyBase 并实现接口 generate_target_weight_position 来定制他们的策略类,该类仅关注目标持仓。
已实现策略
Qlib 提供了一个名为 TopkDropoutStrategy 的已实现策略类。
TopkDropoutStrategy
TopkDropoutStrategy 是 BaseStrategy 的一个子类,并实现了接口 generate_order_list,其流程如下。
采用
Topk-Drop算法计算每只股票的目标数量。备注
Topk-Drop算法有两个参数:Topk: 持有的股票数量
Drop: 每个交易日卖出的股票数量
一般来说,当前持有的股票数量为 Topk,在交易初期除外,可能为零。对于每个交易日,设 $d$ 为当前持有的工具数量,并且在按预测分数从高到低排名时,排名 $gt K$。那么 d 只持有的股票中预测分数最差的将被卖出,而同样数量的未持有股票中预测分数最好的将被买入。
一般来说,$d=$`Drop`,特别是在候选工具池较大时,$K$ 较大,而 Drop 较小时。
在大多数情况下,
TopkDrop算法每天交易日卖出和买入 Drop 只股票,从而产生 2$times$`Drop`/$K$ 的换手率。以下图像展示了一个典型场景。
根据目标数量生成订单列表。
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
Forecast Model 模块可以进行预测,请参考 Forecast Model: Model Training & Prediction.
通常,预测得分是模型的输出。但某些模型是从不同尺度的标签中学习的。因此,预测得分的尺度可能与您的预期不同(例如,工具的回报)。
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
- 不含成本的超额收益
- mean
不考虑成本的 `CAR`(累积异常回报)的均值
- 标准
不含成本的 CAR`(累积异常收益)的 `标准差。
- 年化收益
不含成本的 CAR`(累积异常收益)的 `年化率。
- 信息比率
不考虑成本的 信息比率。请参考 信息比率 – IR.
- 最大回撤
无成本的 CAR`(累积异常收益)的 `最大回撤,请参阅 最大回撤 (MDD)。
- 含成本的超额收益
- mean
带成本的 `CAR`(累积异常收益)系列的均值
- 标准
带成本的 CAR`(累积异常收益)系列的 `标准差。
- 年化收益
含成本的 CAR`(累积异常收益)的 `年化率。
- 信息比率
带成本的 信息比率。请参阅 信息比率 – IR。
- 最大回撤
带成本的 CAR`(累积异常收益)的 `最大回撤,请参阅 最大回撤 (MDD)。
参考
要了解更多关于 预测分数 pred_score 由 预测模型 输出的信息,请参阅 预测模型:模型训练与预测。