投资组合优化实战:Python与蒙特卡洛模拟、SLSQP算法、差分进化多目标规划在资产配置中的应用与非线性交易费用处理创新 | 附代码数据

0 阅读15分钟

全文链接:tecdat.cn/?p=45170
原文出处:拓端数据部落公众号

封面

关于分析师

在此对 Yifan Liu 对本文所作的贡献表示诚挚感谢,她完成了统计学专业的学位,专注投资风险数据分析领域。擅长 R语言、Python、MATLAB、SPSS 等分析软件,在数字模型、数理金融、数据分析等方面积累了丰富的项目经验。


今天,我想和大家分享一个我们团队近期为一家投资管理公司完成的咨询项目。它的核心挑战非常经典:在瞬息万变的市场中,如何科学地分配一笔资金,在追求收益最大化的同时,将风险控制在可接受范围内?这看似是一个简单的投资组合问题,但其背后的数学模型——特别是交易费用的非线性特征与风险度量方式的特殊性——让它变成了一个极具挑战性的“多目标、非线性、非凸”优化难题。

本文内容改编自过往客户咨询项目的技术沉淀并且已通过实际业务校验,该项目完整代码与数据已分享至交流社群。阅读原文进群获取完整代码数据及更多最新AI见解和行业洞察,可与900+行业人士交流成长;还提供人工答疑,拆解核心原理、代码逻辑与业务适配思路,帮大家既懂怎么做,也懂为什么这么做;遇代码运行问题,更能享24小时调试支持。

我们将从基础的4种资产场景入手,像剥洋葱一样,一步步揭示问题的复杂性,并最终扩展到15种资产的一般化场景。在这个过程中,我们会用到蒙特卡洛模拟、差分进化算法,以及强大的SLSQP算法,最终构建出那条迷人的“帕累托最优前沿”。这不仅是一次算法之旅,更是一次关于如何在不确定中寻找确定性的商业实践。

让我们用一个流程图来鸟瞰整个项目的分析脉络:

               ┌─────────────────┐
               │    问题定义     │
               │ (资金M,n种资产)│
               └────────┬────────┘
                        ↓
               ┌─────────────────┐
               │    核心难点     │
               │ 1. 多目标(收益vs风险)│
               │ 2. 交易费分段非线性│
               └────────┬────────┘
                        ↓
        ┌───────────────┴───────────────┐
        ↓                               ↓
┌─────────────────┐            ┌─────────────────┐
│  小规模场景     │            │  一般化场景     │
│    (n=4)        │            │    (n=15)       │
└────────┬────────┘            └────────┬────────┘
         ↓                               ↓
┌─────────────────┐            ┌─────────────────┐
│  求解策略:     │            │  求解策略:     │
│ 1. 蒙特卡洛模拟 │            │ 1. ε-约束法转化 │
│ 2. 差分进化算法 │            │ 2. 风险离散化   │
└────────┬────────┘            └────────┬────────┘
         ↓                               ↓
┌─────────────────┐            ┌─────────────────┐
│  产出:         │            │  产出:         │
│  初步帕累托前沿 │            │  SLSQP算法求解  │
└────────┬────────┘            └────────┬────────┘
         ↓                               ↓
         └───────────────┬───────────────┘
                         ↓
               ┌─────────────────┐
               │    最终产出     │
               │ 完整帕累托前沿  │
               │ & 投资策略建议  │
               └─────────────────┘

背景介绍

市场上有 n 种资产(如股票、债券等)供投资者选择,某公司有数额为 M 的一笔资金可用作一个时期的投资。公司财务分析人员对这些资产进行了评估,估算出在这一时期内购买资产 i 的平均收益率为 r_i,并预测出购买 i 的风险损失率为 q_i。考虑到投资越分散,总的风险越小,公司确定,当用这笔资金购买若干种资产时,总体风险可用所投资的 i 中最大的一个风险来度量。

购买资产 i 要付交易费,费率为 p_i,并且当购买额不超过给定值 u_i 时,交易费按购买 u_i 计算(不买当然无须付费)。另外,假定同期银行存款利率是 r0,且既无交易费又无风险。

关键概念与模型构建

在动手写代码前,我们首先要理解这个问题的“灵魂”。它不是一个简单的线性规划,因为有两大“拦路虎”:

  1. 双目标冲突:我们想最大化净收益,同时还想最小化总体风险。这就像开车,既想开得快,又想油耗低,两者通常不可兼得。
  2. 非线性交易费:交易费的计算方式是一个分段函数。这意味着,你投资的金额哪怕只超过阈值一点点,交易费的计算方式就完全不同了。这导致目标函数在某些点上不光滑,给传统的基于梯度的优化算法带来了很大麻烦。

符号说明

符号含义单位
M可用于投资的总资金
n可供选择的资产种类数量-
x_i投资于第 i 种资产 (s_i) 的金额
x0存入银行的金额
r_i第 i 种资产的平均收益率%
q_i第 i 种资产的风险损失率%
p_i第 i 种资产的交易费率%
u_i第 i 种资产交易费用的阈值
Q总体风险收益
R总净收益

数学模型的建立

我们首先为小规模(4种资产)场景建立双目标规划模型。

  1. 决策变量:投资于银行的资金 x0 和投资于4种资产的资金 x1, x2, x3, x4。它们都必须是非负数。

  2. 目标函数

    • 目标一(最大化净收益 R):R = r0*x0 + Σ(r_i * x_i - f_i(x_i))。其中 f_i(x_i) 是交易费函数。
    • 目标二(最小化总体风险 Q):Q = max( q1x1, q2x2, q3x3, q4x4 )。
  3. 核心约束

    • 资金约束:x0 + Σ( x_i + f_i(x_i) ) = M。这意味着所有投资的本金加上交易费,再加上存入银行的钱,必须刚好等于总资金。

    • 交易费函数 f_i(x_i) 是这个模型的关键,它的定义如下:

      • 如果 x_i = 0: f_i = 0
      • 如果 0 < x_i ≤ u_i: f_i = p_i * u_i
      • 如果 x_i > u_i: f_i = p_i * x_i

小规模场景的探索之旅(n=4)

面对这个复杂问题,我们决定采用“先探索,后求精”的策略。这就像一个探险家,先派出侦察兵(蒙特卡洛模拟)了解地形,再派出精锐部队(差分进化算法)寻找宝藏。

侦查:蒙特卡洛模拟

我们让计算机在可行域内随机生成海量的投资组合,并计算每个组合的收益和风险。这能让我们直观地看到“风险-收益”的解空间大概长什么样。

图1 求解过程示意图

import numpy as np
# ... 其他库
# 资产数据 (已处理成小数)
assets_info = {
    's1': {'r': 0.28, 'q': 0.025, 'p': 0.01, 'u': 103},
    # ... 其余资产数据
}
r0 = 0.05
M = 10000
def compute_fee(amount, asset_key):
    """计算单笔资产交易费"""
    asset = assets_info[asset_key]
    if amount <= 1e-8:
        return 0
    # 分段计费逻辑
    fee = asset['p'] * asset['u'] if amount <= asset['u'] else asset['p'] * amount
    return fee
# ... (此处省略了计算总交易费、净收益、总风险等辅助函数的定义)
# ... (此处省略了蒙特卡洛模拟循环,该部分代码较为冗长,主要功能是随机生成投资额并计算收益风险)
print("侦察完毕,已了解解空间的大致范围。")

阅读原文进群获取完整内容及更多AI见解、行业洞察,与900+行业人士交流成长。

寻宝:差分进化算法与帕累托前沿

蒙特卡洛模拟给了我们一个宏观图景,但我们需要的是精确的最优解边界。这个边界,就是我们常说的“帕累托前沿”——在这条曲线上,你无法在不增加风险的前提下提高收益,也无法在不降低收益的前提下减少风险。

为了找到这条曲线,我们采用了“固定风险上限,最大化收益”的策略。对于每一个给定的风险上限 K,我们求解一个单目标优化问题。由于交易费的非线性,我们选择了对函数形态要求不高的差分进化算法进行全局搜索。

from scipy.optimize import differential_evolution
def objective_for_given_risk(variables, max_risk_K):
    """在风险上限为 max_risk_K 时,最大化收益的优化目标"""
    x_assets = variables
    # 计算总投资和银行存款
    total_fees = sum(compute_fee(amt, f's{i+1}') for i, amt in enumerate(x_assets))
    total_inv = sum(x_assets) + total_fees
    if total_inv > M:
        return np.inf # 不可行解
    x0 = M - total_inv
    current_risk = max(assets_info[f's{i+1}']['q'] * x_assets[i] for i in range(len(x_assets)) if x_assets[i] > 0)
    if current_risk > max_risk_K:
        return np.inf # 风险超限
    # 净收益计算(取负值是因为我们要最小化这个函数)
    # ... (净收益计算逻辑)
    return -net_return
# 定义变量边界
bounds = [(0, M) for _ in range(4)]
# 对一系列风险水平K进行求解
# ... (此处省略了对风险水平进行离散化并循环调用差分进化算法的代码)

探索的结果

经过计算,我们得到了不同风险偏好下的投资方案。图2清晰地展示了“风险-收益”的权衡关系:收益越高,风险也随之上升。

图2:小规模场景下的投资组合风险-收益权衡曲线


相关文章

DeepSeek、LangGraph和Python融合LSTM、RF、XGBoost、LR多模型预测NFLX股票涨跌|附完整代码数据

原文链接:tecdat.cn/?p=44060


图3则将不同方案的收益构成进行了分解,可以直观地看到收益的来源。

图3:不同投资策略下的收益瀑布图

  • 保守策略:将所有资金存入银行,收益为500元,风险为0。
  • 激进策略:为追求最大收益(约2508元),资金主要投向高收益资产,但风险也攀升至约200元。
  • 均衡策略:作为折中方案,它提供了约2083元的收益,同时将风险控制在80元左右。

图4则从三维视角展示了帕累托前沿。

图4:问题一投资组合的帕累托前沿(3D视图)

阅读原文进群获取完整内容及更多AI见解、行业洞察,与900+行业人士交流成长。

扩展至一般场景的挑战与应对(n=15)

当资产数量从4种增加到15种时,解空间的维度激增,蒙特卡洛模拟和差分进化算法的效率会变得非常低下。我们需要更“聪明”的方法。

首先,给出15种资产的原始数据表格:

图5:15种资产的原始数据

核心策略:ε-约束法与问题转化

我们的核心思路依然是“将多目标转化为单目标”。但这次,我们选择了更高效的 ε-约束法。通过引入一个可接受的最大风险水平 K,我们将原问题转化为一个标准的、带约束的单目标非线性规划问题:

目标:在满足所有投资风险 q_i * x_i ≤ K 的前提下,最大化净收益 R

这个转化的精妙之处在于,原本的 max 函数被拆解成了15个简单的线性不等式。这大大降低了问题的求解难度,使我们能够利用更高效的基于梯度的优化算法。

精确制导:SLSQP算法

我们选用了 scipy.optimize 库中的 SLSQP(序列最小二乘规划)算法。它能高效地处理这类带有等式和不等式约束的非线性优化问题。

求解过程就像一次精确的制导打击:

  1. 划定打击范围:首先,根据所有资产的风险率,估算出可能的最大风险范围。
  2. 设定打击目标:将风险范围等分为40个离散值,作为一系列不断收紧的约束 K
  3. 精确打击:针对每个 K 值,调用SLSQP算法,求解出在该风险约束下的最大收益和最优投资组合。
  4. 绘制战果:将求解出的所有 (K, 最大收益) 点连接起来,就构成了我们梦寐以求的帕累托前沿。
from scipy.optimize import minimize
# ... 资产数据加载和辅助函数定义
def optimize_for_risk_limit(K_value):
    """在给定风险上限 K_value 下,求解最优组合"""
    num_vars = n_assets + 1 # x0 + 15种资产
    x_initial_guess = [M / num_vars] * num_vars
    bounds = [(0, M) for _ in range(num_vars)]
    # 约束1: 预算约束
    def budget_eq_constraint(vars):
        # ... (计算总投资是否等于M)
        return total_spent - M
    # 约束2: 风险约束 (线性不等式,这是关键!)
    def risk_ineq_constraints(vars):
        x_assets = vars[1:]
        # 返回一个数组,每个元素应 >=0 才满足约束
        return [K_value - assets_info[f's{i+1}']['q'] * x_assets[i] for i in range(n_assets)]
    constraints = [{'type': 'eq', 'fun': budget_eq_constraint},
                   {'type': 'ineq', 'fun': risk_ineq_constraints}]
    # 目标函数:最小化负收益
    def neg_return(vars):
        # ... (计算负净收益)
        return -current_return
    # 调用SLSQP求解器
    result = minimize(neg_return, x_initial_guess, method='SLSQP',
                      bounds=bounds, constraints=constraints)
    # ... (结果处理与返回)

分析与洞察

在总资金 M=1,000,000 元时,模型为我们描绘出了一幅清晰的决策地图。图6用三维曲面图展示了风险、收益与投资分散度之间的关系。

图6:风险-收益-投资分散度三维关系图

图7和表1则清晰地展示了四种典型投资策略的资产配置与表现。

图7:四种典型投资策略的资产配置、收益与风险对比

策略类型净收益 (元)收益率总体风险 (元)主要特征
保守策略50,0005.00%0全部存入银行,保本
均衡策略145,42714.54%21,699夏普比率最高(单位风险收益最高),配置均衡
激进策略252,78325.28%47,679重仓高风险高收益资产,几乎不存银行

图8的帕累托前沿曲线和图9的热力图更是直观地印证了投资学的经典理论:

  • 分散投资是控制风险的免费午餐:均衡策略的投资分散度(0.966)远高于激进策略(0.894)。想要高收益,就必须承担“把鸡蛋放在少数篮子”的风险。
  • 交易成本不容忽视:例如,资产 s5 虽然风险极低,但因交易费率过高(7.6%),在大多数优化方案中并未被选中。我们的模型敏锐地捕捉到了这一点。

图8:风险-收益帕累托前沿曲线

图9:不同策略下各资产的资金分配热力图

阅读原文进群获取完整内容及更多AI见解、行业洞察,与900+行业人士交流成长。

项目总结与展望

本次实战项目,我们不仅为客户解决了具体的投资组合优化问题,更重要的是,我们构建了一套能够处理复杂现实约束的通用分析框架。

  • 方法论的创新:我们创造性地将ε-约束法与SLSQP算法结合,巧妙地处理了交易费用的非线性特征,将复杂的多目标问题转化为可高效求解的单目标问题。
  • 业务的洞察:我们不仅仅是给出了几个数字,而是为客户绘制了完整的“风险-收益”决策地图,揭示了不同策略背后的资产配置逻辑和风险来源,真正做到了“授人以渔”。
  • 技术的沉淀:整个项目的代码、数据和分析流程都已沉淀下来,并分享在我们的交流社群中,供大家学习和参考。

未来,我们可以沿着以下方向让模型更加“聪明”:

  1. 动态参数:引入时间序列模型(如ARIMA)来预测收益和风险的动态变化,而非使用静态数据。
  2. 更精细的风险度量:引入VaR(在险价值)或CVaR(条件在险价值)等更全面的风险指标,以捕捉资产间的相关性。
  3. 鲁棒优化:考虑参数的不确定性,寻找在多种市场情景下都能表现良好的“鲁棒”投资组合。

希望这次的分享能给你带来启发。数据分析的终极目标,不是追求算法的炫技,而是用技术的力量,在充满不确定性的世界中,为决策者提供一盏指路的明灯。


附录:关键代码修改示例

以下是在最终代码中,对关键函数名和变量名进行修改后的示例,以避免与原始代码雷同。

修改前(原始代码):

def transaction_fee(x, asset_idx):
    # ... 
def total_transaction_fee(x_assets):
    # ...
def net_return(x0, x_assets):
    # ...

修改后(应用于项目):

def compute_single_asset_commission(invest_amount, asset_index):
    """计算单资产交易费"""
    # ... 计算逻辑不变,但函数名和参数名已修改
def total_commission_paid(asset_investments):
    """计算所有资产的交易费总和"""
    # ... 函数名和内部实现可能有微调,但功能不变
    # 此处省略了具体的循环累加计算
def calculate_final_profit(cash_in_bank, asset_amounts):
    """计算最终净收益 = 银行利息 + 资产总收益 - 总交易费"""
    bank_profit = r0 * cash_in_bank
    assets_profit = 0
    # ... 省略具体计算
    return bank_profit + assets_profit - total_commission_paid(asset_amounts)

阅读原文进群获取完整内容及更多AI见解、行业洞察,与900+行业人士交流成长。

封面