投资组合策略:投资组合管理
简介
投资组合策略
旨在采用不同的投资组合策略,这意味着用户可以根据 预测模型
的预测得分采用不同的算法生成投资组合。用户可以通过 工作流程
模块自动化地使用 投资组合策略
,有关详细信息,请参阅 工作流程:工作流管理。
由于 Qlib
中的组件设计为松耦合方式,投资组合策略
也可以作为独立模块使用。
Qlib
提供了几种实现的投资组合策略。此外,Qlib
支持自定义策略,用户可以根据自己的需求自定义策略。
在用户指定模型(预测信号)和策略后,运行回测将帮助用户检查自定义模型(预测信号)/策略的性能。
基类和接口
BaseStrategy
Qlib 提供了基类 qlib.strategy.base.BaseStrategy
。所有的策略类都需要继承这个基类并实现其接口。
- generate_trade_decision
generate_trade_decision 是一个关键接口,用于在每个交易时间点生成交易决策。 调用此方法的频率取决于执行器的频率(默认情况下为 “day”)。但是交易的频率可以由用户的实现来决定。 例如,如果用户想要以每周进行交易,而执行器中的 time_per_step 是 “day”,用户可以返回非空的每周交易决策(否则返回空值,如 这里)。
用户可以继承 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
TopkDropoutStrategy 是 BaseStrategy 的子类,并实现了 generate_order_list 接口,其流程如下所述。
采用
Topk-Drop
算法计算每只股票的目标数量备注
Topk-Drop
算法有两个参数:Topk:需要持有的股票数量
Drop:每个交易日卖出的股票数量
通常情况下,当前持有的股票数量是 Topk,在交易初期除外,此时持有的股票数量为零。 对于每个交易日,假设$d$是当前持有的排名高于$K$的股票数量,根据预测得分从高到低进行排序。 那么将卖出目前持有股票中预测得分最差的$d$只股票,并且会买入相同数量的未持有股票中预测得分最好的股票。
通常情况下,$d=$`Drop`,尤其是在候选股票池规模较大、$K$较大且 Drop 较小的情况下。
大多数情况下,
TopkDrop
算法每个交易日都会卖出和买入 Drop 只股票,这样可以实现一个换手率为 $2times frac{Drop}{K}$ 。以下图片展示了一个典型情况。
从目标金额生成订单列表
EnhancedIndexingStrategy
EnhancedIndexingStrategy 增强型指数策略结合了主动管理和被动管理的技巧,旨在在控制风险暴露(即跟踪误差)的同时,以组合回报的角度胜过基准指数(如S&P 500)。
具体详细信息,请参考 qlib.contrib.strategy.signal_strategy.EnhancedIndexingStrategy 以及 qlib.contrib.strategy.optimizer.enhanced_indexing.EnhancedIndexingOptimizer。
使用方法和示例
首先,用户可以创建一个模型来获取交易信号(以下示例中的变量名为 pred_score
)。
预测分数
预测分数 是一个 pandas DataFrame。它的索引是 <datetime(pd.Timestamp), instrument(str)>,并且必须包含一个名为 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
对其投资组合管理策略进行回测。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 (信息比率)
无成本的信息比率。请参阅 信息比率(Information Ratio) 。
- max_drawdown (最大回撤率)
无成本 CAR (累积异常收益)的最大回撤率,请参阅 最大回撤率(Maximum Drawdown) 。
- excess_return_with_cost (有成本超额回报)
- mean (平均值)
有成本 CAR (累积异常收益)的均值
- std (标准差)
有成本 CAR (累积异常收益)的标准差。
- annualized_return (年化收益率)
有成本 CAR (累积异常收益)的年化收益率。
- information_ratio (信息比率)
有成本的信息比率。请参阅 信息比率(Information Ratio) 。
- max_drawdown (最大回撤率)
有成本 CAR (累积异常收益)的最大回撤率,请参阅 最大回撤率(Maximum Drawdown) 。
参考
要了解由“Forecast Model”产生的“prediction score”“pred_score”的更多信息,请参阅 Forecast Model: Model Training & Prediction 。