问题描述
解决方案
构建一个最小化跟踪误差(Tracking Error)的投资组合,通常用于指数增强或指数复制策略。其核心目标是使投资组合的收益率波动与基准指数的收益率波动尽可能一致。
在聚宽(JoinQuant)平台中,我们可以利用 jqlib.optimizer 库中的 portfolio_optimizer 函数,配合 MinTrackingError 优化目标来实现这一需求。
策略实现思路
- 确定基准:设定要追踪的指数(如沪深300
000300.XSHG)。 - 确定股票池:通常选择该指数的成分股作为候选股票池。
- 设置优化目标:使用
MinTrackingError,旨在最小化组合收益率与基准收益率之间的差异的标准差。 - 设置约束条件:
- 权重和约束:通常要求总仓位为 100%(
WeightEqualConstraint(limit=1.0))。 - 个股权重边界:通常限制为只做多,不做空(
Bound(0.0, 1.0))。
- 权重和约束:通常要求总仓位为 100%(
- 定期调仓:例如每月调仓一次,根据优化器计算出的新权重调整持仓。
策略代码实现
以下是一个完整的策略代码示例,该策略每月初运行一次,旨在最小化与沪深300指数的跟踪误差。
# -*- coding: utf-8 -*-
# 导入函数库
from jqdata import *
from jqlib.optimizer import *
import pandas as pd
def initialize(context):
# 设定沪深300作为基准
g.benchmark = '000300.XSHG'
set_benchmark(g.benchmark)
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 设定交易费率:买入万分之三,卖出万分之三加千分之一印花税,最低5元
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 设定每月第一个交易日进行调仓
run_monthly(rebalance, monthday=1, time='09:30')
def rebalance(context):
"""
调仓函数
"""
log.info("开始进行最小化跟踪误差优化...")
# 1. 获取待优化的股票池:这里直接使用基准指数的成分股
# 注意:为了减少计算量和避免停牌股,实际操作中可能需要过滤掉停牌或ST股票
target_stock_list = get_index_stocks(g.benchmark)
# 简单过滤停牌股票(可选,优化器通常能处理,但过滤后效率更高)
current_data = get_current_data()
target_stock_list = [stock for stock in target_stock_list if not current_data[stock].paused]
if len(target_stock_list) == 0:
log.warn("股票池为空,跳过本次调仓")
return
# 2. 设置优化目标:最小化跟踪误差
# count=250 表示使用过去250个交易日的数据来计算协方差矩阵和跟踪误差
target = MinTrackingError(benchmark=g.benchmark, count=250)
# 3. 设置约束条件
# 约束1:组合总权重等于 1.0 (满仓)
constraints = [
WeightEqualConstraint(limit=1.0)
]
# 4. 设置边界条件
# 每只股票的权重在 0.0 到 1.0 之间 (不做空,不加杠杆)
bounds = [Bound(0.0, 1.0)]
# 5. 运行优化器
# date 使用 context.previous_date,因为优化需要基于历史数据
try:
optimized_weights = portfolio_optimizer(
date=context.previous_date,
securities=target_stock_list,
target=target,
constraints=constraints,
bounds=bounds,
ftol=1e-9, # 精度控制
return_none_if_fail=True
)
except Exception as e:
log.error("优化器运行异常: %s" % e)
optimized_weights = None
# 6. 执行调仓
if optimized_weights is None:
log.warn("优化失败,本次不调整仓位")
else:
log.info("优化成功,开始调整仓位")
adjust_portfolio(context, optimized_weights)
def adjust_portfolio(context, weights):
"""
根据权重调整仓位
"""
# 获取当前持仓
current_positions = context.portfolio.positions
# 1. 先卖出:不在目标池中的,或者权重降低的
# 获取当前总资产
total_value = context.portfolio.total_value
# 卖出不在优化结果中的股票
for stock in current_positions:
if stock not in weights.index:
order_target_value(stock, 0)
# 2. 买入或调整:根据优化后的权重买入
# weights 是一个 pandas.Series,index是股票代码,value是权重
for stock, weight in weights.items():
# 计算目标持仓价值
target_value = total_value * weight
# 下单调整
# 注意:为了防止微小的权重变动导致频繁交易,可以加一个阈值判断,这里为了演示直接调整
if target_value > 0:
order_target_value(stock, target_value)
代码关键点解析
-
MinTrackingError:- 这是核心目标函数。它计算投资组合收益率与基准指数收益率之差的标准差。
count=250参数指定了用于计算协方差矩阵的历史数据长度(通常取一年左右的数据)。
-
portfolio_optimizer:- 这是聚宽提供的凸优化求解器。
securities: 传入指数成分股,意味着我们试图通过调整成分股的权重来拟合指数。return_none_if_fail=True: 如果优化器无法找到满足所有约束条件的解(例如数据不足或约束冲突),它将返回None,策略中需要对此进行处理以防报错。
-
约束条件 (
constraints):WeightEqualConstraint(limit=1.0): 这是一个等式约束,强制所有股票的权重之和严格等于 1。这代表我们希望始终保持满仓状态来追踪指数。
-
边界条件 (
bounds):Bound(0.0, 1.0): 限制单只股票的权重不能为负(不做空),也不能超过 100%(不加杠杆)。
常见问题 Q&A
Q: 为什么优化器有时会返回 None (优化失败)?
A: 常见原因包括:
- 数据不足:新上市的股票没有足够的历史数据(例如代码中设置了
count=250,如果股票上市不足250天,可能会导致计算协方差矩阵时出错)。 - 约束冲突:设置的约束条件在数学上无法同时满足。
- 停牌处理:如果股票池中包含大量停牌股票,可能会影响优化求解。
Q: 如何进一步增强收益(Alpha)?
A: 上述代码是纯粹的“最小化跟踪误差”,即完全复制指数。如果想要获得超额收益(Alpha),可以在 constraints 中添加因子暴露限制(例如 ExposureConstraint),或者将目标函数改为 MaxSharpeRatio 并添加 TrackingErrorConstraint(限制跟踪误差在一定范围内,同时最大化夏普比率)。
Q: 为什么不直接买入指数基金?
A: 在实盘中,直接买入ETF确实是最小化跟踪误差的最简单方法。但在量化策略中,构建这样的组合通常是为了作为底仓,然后在此基础上进行Smart Beta调整(例如低配高估值股票,超配低估值股票),或者用于对冲操作。