🚀 AI 一键生成 ptrade 策略代码

PTrade 量化实战:使用 cvxopt 实现均值方差投资组合优化

ptrade | 发布时间: 2025/12/23
以下内容由 EasyQuant 生成。

问题描述

怎么用 cvxopt 做投资组合优化?

解决方案

在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("调仓完成")

代码关键点解析

  1. 数据准备 (get_history):

    • 我们获取了过去 g.history_window 天的收盘价。
    • 使用 pct_change() 计算日收益率。
    • 使用 cov() 计算协方差矩阵,这是衡量资产间风险关联的核心数据。
  2. 矩阵构建 (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。
  3. 求解 (solvers.qp):

    • 调用 solvers.qp(P, q, G, h, A, b) 进行求解。
    • 结果存储在 sol['x'] 中,即最优的权重向量。
  4. 交易执行:

    • 使用 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 默认会在控制台打印求解过程的迭代日志。在回测或实盘日志中,这些大量的文本信息会干扰正常的策略日志查看,因此通常将其关闭。