问题描述
解决方案
在PTrade中,cvxopt 是一个强大的凸优化库,常用于解决二次规划(Quadratic Programming, QP)问题。在量化投资中,最经典的应用场景就是马科维茨(Markowitz)均值-方差投资组合优化。
核心原理
投资组合优化的目标通常是在给定预期收益的情况下最小化风险(方差),或者在给定风险的情况下最大化收益。这是一个典型的二次规划问题。
标准形式如下:
$$ \text{minimize} \quad \frac{1}{2} x^T P x + q^T x $$
$$ \text{subject to} \quad G x \leq h $$
$$ \text{and} \quad A x = b $$
对应到投资组合优化(最小化方差模型):
- $x$:股票权重向量 ($w$)。
- $P$:资产收益率的协方差矩阵 ($\Sigma$)。
- $q$:零向量(仅最小化方差)或负的预期收益向量(若考虑最大化效用)。
- $G, h$:不等式约束。例如 $w \geq 0$(不允许做空),即 $-w \leq 0$,则 $G=-I, h=0$。
- $A, b$:等式约束。例如权重之和为1 ($\sum w = 1$),则 $A=[1, 1, ...], b=[1]$。
PTrade 策略代码实现
以下是一个完整的策略示例。该策略选取上证50成分股,每隔20个交易日,利用 cvxopt 计算最小方差投资组合的权重并进行调仓。
import numpy as np
import pandas as pd
from cvxopt import matrix, solvers
# 设置cvxopt输出不显示进度
solvers.options['show_progress'] = False
def initialize(context):
"""
初始化函数
"""
# 设定基准
set_benchmark('000016.SS')
# 设定滑点
set_slippage(0.002)
# 设定佣金
set_commission(commission_ratio=0.0003)
# 设定股票池:上证50
g.index = '000016.SS'
# 调仓周期(天)
g.rebalance_days = 20
# 计数器
g.days_counter = 0
# 历史数据窗口长度(用于计算协方差)
g.history_window = 60
# 初始股票池获取(也可以在before_trading_start中动态获取)
g.stocks = get_index_stocks(g.index)
set_universe(g.stocks)
def handle_data(context, data):
"""
盘中运行函数
"""
# 检查是否达到调仓周期
if g.days_counter % g.rebalance_days == 0:
rebalance_portfolio(context)
g.days_counter += 1
def rebalance_portfolio(context):
"""
执行投资组合优化与调仓
"""
# 1. 获取股票池
# 注意:get_index_stocks建议在before_trading_start调用,这里为了演示逻辑完整性放在一起
stocks = get_index_stocks(g.index)
if not stocks:
return
# 2. 获取历史收盘价数据
# PTrade Python3.5环境下,多只股票单字段返回DataFrame,列为股票代码
hist_data = get_history(g.history_window, '1d', 'close', security_list=stocks, fq='pre')
# 检查数据有效性
if hist_data is None or len(hist_data) < g.history_window:
log.info("历史数据不足,跳过本次调仓")
return
# 3. 计算收益率和协方差矩阵
# 计算日收益率
returns = hist_data.pct_change().dropna()
# 如果数据缺失过多,剔除缺失数据的股票
returns = returns.dropna(axis=1)
valid_stocks = returns.columns.tolist()
n = len(valid_stocks)
if n < 2:
log.info("有效股票数量不足,跳过调仓")
return
# 计算协方差矩阵 (P矩阵的核心)
cov_mat = returns.cov()
# 4. 构建 cvxopt 所需的矩阵参数
# 目标函数: minimize (1/2) * w.T * P * w
# P: 协方差矩阵
P = matrix(cov_mat.values)
# q: 线性项系数,最小化方差时为0向量
q = matrix(0.0, (n, 1))
# 不等式约束: w >= 0 => -w <= 0
# G: -I (负单位矩阵)
G = matrix(-np.eye(n))
# h: 0向量
h = matrix(0.0, (n, 1))
# 等式约束: sum(w) = 1
# A: 全1行向量
A = matrix(1.0, (1, n))
# b: 标量1.0
b = matrix(1.0)
# 5. 求解二次规划问题
try:
sol = solvers.qp(P, q, G, h, A, b)
# 获取权重,转换为list
weights = np.array(sol['x']).flatten()
except Exception as e:
log.error("优化求解失败: {}".format(e))
return
# 6. 执行调仓
log.info("优化成功,开始调仓...")
# 获取当前账户总资产
total_value = context.portfolio.portfolio_value
# 先卖出不在有效列表中的股票
current_positions = list(context.portfolio.positions.keys())
for stock in current_positions:
if stock not in valid_stocks:
order_target_value(stock, 0)
# 根据优化后的权重买入/调整
for i, stock in enumerate(valid_stocks):
target_weight = weights[i]
# 过滤掉极小的权重(例如小于0.1%),避免产生碎股或无意义交易
if target_weight < 0.001:
target_weight = 0.0
target_value = total_value * target_weight
order_target_value(stock, target_value)
log.info("调仓完成")
代码关键点解析
-
数据准备 (
get_history):- 我们获取了过去
g.history_window天的收盘价。 - 使用
pct_change()计算日收益率。 - 使用
cov()计算协方差矩阵,这是衡量资产间风险关联的核心数据。
- 我们获取了过去
-
矩阵构建 (
cvxopt.matrix):cvxopt的求解器需要特定的矩阵格式。- P: 协方差矩阵,代表风险项。
- q: 全0向量,因为我们在这个简单的例子中只追求最小化波动率(风险),不引入预期收益率的考量。
- G, h: 构成了 $Gx \leq h$。为了实现“不允许做空”(权重非负),我们设定 $-w \leq 0$。因此 $G$ 是负单位矩阵,$h$ 是零向量。
- A, b: 构成了 $Ax = b$。为了实现“全额投资”(权重之和为1),$A$ 是全1向量,$b$ 是1.0。
-
求解 (
solvers.qp):- 调用
solvers.qp(P, q, G, h, A, b)进行求解。 - 结果存储在
sol['x']中,即最优的权重向量。
- 调用
-
交易执行:
- 使用
order_target_value根据计算出的权重和当前总资产价值进行下单。 - 增加了一个阈值判断
if target_weight < 0.001,忽略过小的权重,避免产生过多的佣金损耗。
- 使用
注意事项
- Python 版本兼容性: 代码使用了
.format()而非 f-string,且使用了 PTrade 标准的 API 调用方式,兼容 Python 3.5 及以上环境。 - 数据清洗: 实际交易中,某些股票可能停牌或刚上市导致数据缺失。代码中使用了
.dropna()来确保协方差矩阵是正定的且计算不出错。 - 求解器异常: 凸优化求解可能会因为矩阵非正定(例如数据点少于股票数)或数值问题而失败,因此加上了
try...except块。
Q&A
Q: 为什么使用 cvxopt 而不是 scipy.optimize?
A: 虽然 scipy 也能做优化,但 cvxopt 是专门为凸优化设计的,对于二次规划(QP)问题,它的求解速度通常更快,且对于约束条件的处理(矩阵形式)更加规范和直观,非常适合处理资产数量较多的投资组合问题。
Q: 如何加入预期收益率约束(例如要求组合预期年化收益 > 10%)?
A: 你需要在不等式约束 $G, h$ 中增加一行。假设 $\mu$ 是预期收益率向量,要求 $\mu^T w \geq 0.10$,即 $-\mu^T w \leq -0.10$。你需要将这一行拼接到 $G$ 矩阵的底部,并将 $-0.10$ 拼接到 $h$ 向量的底部。
Q: 为什么代码中要设置 solvers.options['show_progress'] = False?
A: cvxopt 默认会在控制台打印求解过程的迭代日志。在回测或实盘日志中,这些大量的文本信息会干扰正常的策略日志查看,因此通常将其关闭。