第三章 投资组合的多目标模型:收益、风险、夏普比率、最大回撤等多目标函数构建

好,咱们进入正题。

上一章我们聊了多目标优化的基础概念,Pareto前沿、支配关系这些。但说实话,那些都是通用理论。真正到了投资组合这个场景,你得先回答一个核心问题:到底要优化哪些目标?

我见过不少新手,上来就把十几个指标扔进模型,结果跑出来一堆不靠谱的解。嗯,这里有个关键点——目标函数的选择,直接决定了你的投资组合长什么样

3.1 四个核心目标函数

我个人习惯,先从最经典的四个目标入手。它们几乎覆盖了投资组合最关心的维度:

  • 收益:赚钱能力,越高越好
  • 风险:波动程度,越低越好
  • 夏普比率:风险调整后的收益,越高越好
  • 最大回撤:最惨的时候亏多少,越低越好

你想想看,这四个目标其实代表了两种视角:收益端风险端。夏普比率是两者的比值,最大回撤则是极端风险。

核心思路:多目标优化不是找一个"最优解",而是找一组"不差解"——也就是Pareto前沿上的解。每个解代表一种收益-风险的权衡。

3.2 目标函数的数学定义

咱们一个一个来拆。先定义一些符号:

  • \( w \):权重向量,\( w_i \) 表示第 i 只股票的权重
  • \( r \):预期收益率向量
  • \( \Sigma \):协方差矩阵
  • \( T \):回测期长度(天数)
  • \( P_t \):第 t 天的组合净值

3.2.1 收益目标

最简单的,组合预期收益:

def objective_return(weights, expected_returns):
    return np.dot(weights, expected_returns)  # 最大化

注意,这里是最大化目标。但在多目标框架里,我们通常把所有目标统一成最小化方向。所以实际代码里,我会取负号:

def objective_return_min(weights, expected_returns):
    return -np.dot(weights, expected_returns)  # 最小化负收益

3.2.2 风险目标

用组合方差来衡量风险:

def objective_risk(weights, cov_matrix):
    return np.dot(weights.T, np.dot(cov_matrix, weights))  # 最小化

这里用的是方差,不是标准差。为什么?因为方差在优化时更平滑,梯度好算。我在项目中遇到过,用标准差做目标函数,优化器经常震荡不收敛。后来换成方差,问题就解决了。

3.2.3 夏普比率目标

夏普比率 = (组合收益 - 无风险利率) / 组合波动率。但注意,在多目标框架里,我们通常把它拆成两个目标:收益和风险。不过有时候,直接优化夏普比率也有用:

def objective_sharpe(weights, expected_returns, cov_matrix, rf=0.02):
    port_return = np.dot(weights, expected_returns)
    port_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
    sharpe = (port_return - rf) / port_vol
    return -sharpe  # 最小化负夏普

避坑指南:我曾经直接把夏普比率作为唯一目标,结果优化出来的组合权重极度集中,回撤大得吓人。后来才明白,夏普比率只考虑了波动率,没考虑尾部风险。所以我现在做多目标时,一定会把最大回撤也加进去。

3.2.4 最大回撤目标

最大回撤的计算稍微复杂一点,需要遍历整个回测期:

def objective_max_drawdown(weights, price_data):
    # price_data: DataFrame, 每列是股票价格
    portfolio_value = price_data.dot(weights)
    cumulative_max = np.maximum.accumulate(portfolio_value)
    drawdown = (portfolio_value - cumulative_max) / cumulative_max
    max_dd = np.min(drawdown)  # 负值,比如 -0.3 表示亏了30%
    return -max_dd  # 最小化最大回撤的绝对值

这里有个细节:最大回撤是路径依赖的。同样的收益和波动率,不同的路径会产生不同的最大回撤。所以它捕捉的是"最坏情况",这是方差做不到的。

3.3 多目标函数组合策略

四个目标都有了,怎么组合?我常用的有三种方式:

策略 方法 适用场景
加权求和 给每个目标赋权重,合成单目标 快速得到一个解,但权重难定
ε-约束法 选一个主目标,其他设约束 有明确偏好时好用
Pareto前沿法 用NSGA-II等算法直接找前沿 需要权衡多个解时首选

我个人最推荐第三种。为什么?因为加权求和太主观了——你给收益赋0.6,风险赋0.4,凭什么?而Pareto前沿法能给你一整条曲线,你自己选。

3.4 完整的多目标模型代码

下面是一个完整的例子,用NSGA-II算法优化四个目标:

import numpy as np
import pandas as pd
from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.core.problem import ElementwiseProblem
from pymoo.optimize import minimize

class PortfolioProblem(ElementwiseProblem):
    def __init__(self, expected_returns, cov_matrix, price_data):
        self.expected_returns = expected_returns
        self.cov_matrix = cov_matrix
        self.price_data = price_data
        n_assets = len(expected_returns)
        
        super().__init__(
            n_var=n_assets,
            n_obj=4,  # 四个目标
            xl=0.0,   # 权重下限
            xu=1.0,   # 权重上限
            constr_ieq=[lambda x: np.sum(x) - 1.0]  # 权重和为1
        )
    
    def _evaluate(self, x, out, *args, **kwargs):
        # 目标1:负收益(最小化)
        f1 = -np.dot(x, self.expected_returns)
        
        # 目标2:方差(最小化)
        f2 = np.dot(x.T, np.dot(self.cov_matrix, x))
        
        # 目标3:负夏普比率(最小化)
        port_return = np.dot(x, self.expected_returns)
        port_vol = np.sqrt(f2)
        f3 = -(port_return - 0.02) / port_vol
        
        # 目标4:最大回撤(最小化)
        portfolio_value = self.price_data.dot(x)
        cumulative_max = np.maximum.accumulate(portfolio_value)
        drawdown = (portfolio_value - cumulative_max) / cumulative_max
        f4 = -np.min(drawdown)
        
        out["F"] = [f1, f2, f3, f4]

# 使用示例
# problem = PortfolioProblem(expected_returns, cov_matrix, price_data)
# algorithm = NSGA2(pop_size=100)
# res = minimize(problem, algorithm, ('n_gen', 100), verbose=True)
# pareto_front = res.F  # 得到Pareto前沿上的解

注意:实际运行时,最大回撤的计算会显著拖慢优化速度。我建议先用方差和收益跑几轮,筛选出候选解,再计算最大回撤做二次筛选。这样效率高很多。

3.5 知识体系结构图

下面这张图,把本章的核心逻辑串起来了:

多目标投资组合模型知识体系 多目标优化模型 收益(最大化) 风险(最小化) 夏普比率(最大化) 最大回撤(最小化) 加权求和法 ε-约束法 Pareto前沿法 输出:Pareto前沿解集 每个解代表一种收益-风险-回撤的权衡 四个目标 + 三种组合策略 → 最终得到一组非支配解

3.6 实战中的坑与经验

最后,分享几个我在实战中踩过的坑:

  1. 目标函数量纲不一致:收益是百分比,方差是百分比的平方,夏普是无量纲。直接放一起优化,数值大的目标会主导搜索方向。我习惯先做归一化,或者用自适应权重。
  2. 最大回撤的计算频率:用日频数据算最大回撤,和用周频数据算,结果可能差很多。我个人建议用日频,因为回撤是极端事件,日频能捕捉到更细的波动。
  3. 夏普比率的陷阱:夏普比率假设收益是正态分布,但实际中不是。所以夏普高的组合,可能只是运气好,而不是真的优秀。我一般会同时看偏度和峰度。
  4. 不要贪多:目标函数不是越多越好。4-5个是黄金数量。超过6个,Pareto前沿会变得非常稀疏,解的质量反而下降。

一个小技巧:如果你发现优化结果总是集中在某个区域,比如全是高收益高风险的解,那说明你的目标函数设置有问题。试着调整一下约束条件,或者给某些目标加惩罚项。

好了,这一章就到这里。四个目标函数,三种组合策略,再加上一张结构图,应该够你上手了。记住,多目标优化的核心不是找"最好",而是找"不后悔"——你选的每个解,都是权衡后的结果。


专注资料整理