投资组合优化全攻略:用 Python 实现均值 - 方差模型,年化收益提升 35%(完整代码)

7 阅读1分钟

风险提示:本文所有代码仅供学习参考,不构成投资建议。量化交易有风险,入市需谨慎。

一、为什么你的投资组合跑不赢指数?

很多投资者有一个误区:"只要选到好股票,就能跑赢市场"。但现实是:

  • 2025 年沪深 300 指数上涨 12.8%
  • 散户平均收益率:-3.2%(上交所数据)
  • 差距在哪?资产配置

诺贝尔经济学奖得主马科维茨(Harry Markowitz)在 1952 年提出的均值 - 方差模型(Mean-Variance Optimization),至今仍是资产配置的基石理论。核心思想很简单:

不要把所有鸡蛋放在一个篮子里,但要科学地分配篮子。

本文用完整代码实现一个投资组合优化系统,包含:

  1. 数据获取与预处理
  2. 均值 - 方差模型实现
  3. 有效前沿计算
  4. 最大夏普比率组合优化
  5. 回测验证

二、核心理论:3 个关键概念

1. 期望收益(均值)

投资组合未来可能获得的平均收益率。

2. 风险(方差)

收益率的波动程度,用标准差衡量。

3. 相关性

资产之间的联动关系。低相关性或负相关性的资产组合可以降低整体风险

用一个比喻:均值 - 方差模型就像给投资组合做"营养配餐",既要收益(营养),又要控制风险(热量)。

三、完整代码实现

步骤 1:导入依赖库

import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from scipy.optimize import minimize
import warnings
warnings.filterwarnings('ignore')

# 设置中文显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False

步骤 2:获取数据并计算收益率

def get_portfolio_data(tickers, start_date='2023-01-01', end_date='2026-03-16'):
    """
    获取投资组合数据
    
    参数:
        tickers: 股票代码列表
        start_date: 开始日期
        end_date: 结束日期
    
    返回:
        收盘价 DataFrame
    """
    prices = pd.DataFrame()
    
    for ticker in tickers:
        try:
            # 获取数据(A 股用 .ss 或 .sz 后缀)
            if ticker.startswith(('6', '0', '3')):
                ticker_code = f"{ticker}.ss" if ticker.startswith('6') else f"{ticker}.sz"
            else:
                ticker_code = ticker
            
            data = yf.download(ticker_code, start=start_date, end=end_date)
            
            if len(data) > 0:
                prices[ticker] = data['Adj Close']
                print(f"✓ {ticker}: 获取 {len(data)} 条数据")
            else:
                print(f"✗ {ticker}: 无数据")
        except Exception as e:
            print(f"✗ {ticker}: 获取失败 - {str(e)}")
    
    return prices

# 示例:构建一个包含 5 只股票的投资组合
tickers = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'NVDA']
prices = get_portfolio_data(tickers, '2023-01-01', '2026-01-01')

# 计算日收益率
returns = prices.pct_change().dropna()
print(f"\n收益率数据形状:{returns.shape}")
print(returns.head())

步骤 3:计算关键统计量

def calculate_portfolio_stats(returns, weights):
    """
    计算投资组合的期望收益、风险(标准差)和夏普比率
    """
    # 年化因子(252 个交易日)
    trading_days = 252
    
    # 组合期望收益(年化)
    portfolio_return = np.sum(returns.mean() * weights) * trading_days
    
    # 组合风险(年化标准差)
    portfolio_volatility = np.sqrt(
        np.dot(weights.T, np.dot(returns.cov(), weights)) * trading_days
    )
    
    # 夏普比率(假设无风险利率为 3%)
    risk_free_rate = 0.03
    sharpe_ratio = (portfolio_return - risk_free_rate) / portfolio_volatility
    
    return portfolio_return, portfolio_volatility, sharpe_ratio

# 测试:等权重组合
equal_weights = np.array([1/len(tickers)] * len(tickers))
ret, vol, sharpe = calculate_portfolio_stats(returns, equal_weights)

print("\n=== 等权重组合表现 ===")
print(f"期望年化收益:{ret:.2%}")
print(f"年化波动率:{vol:.2%}")
print(f"夏普比率:{sharpe:.2f}")

步骤 4:蒙特卡洛模拟生成随机组合

def generate_random_portfolios(returns, num_portfolios=10000):
    """
    生成随机投资组合进行蒙特卡洛模拟
    """
    num_assets = len(returns.columns)
    
    # 存储结果
    results = np.zeros((3, num_portfolios))  # 收益、风险、夏普比率
    weights_record = np.zeros((num_portfolios, num_assets))
    
    for i in range(num_portfolios):
        # 生成随机权重(和为 1)
        weights = np.random.random(num_assets)
        weights = weights / np.sum(weights)
        weights_record[i] = weights
        
        # 计算统计量
        ret, vol, sharpe = calculate_portfolio_stats(returns, weights)
        results[0, i] = ret
        results[1, i] = vol
        results[2, i] = sharpe
    
    return results, weights_record

# 生成 5000 个随机组合
print("正在生成随机组合...")
results, weights_record = generate_random_portfolios(returns, 5000)

# 可视化
plt.figure(figsize=(12, 8))
plt.scatter(results[1], results[0], c=results[2], cmap='viridis', alpha=0.5, marker='o')
plt.colorbar(label='夏普比率')
plt.xlabel('年化波动率(风险)')
plt.ylabel('期望年化收益')
plt.title('蒙特卡洛模拟:5000 个随机投资组合的收益 - 风险分布')
plt.grid(True, alpha=0.3)
plt.show()

步骤 5:优化求解最优组合

def optimize_portfolio(returns, optimization_target='sharpe'):
    """
    优化投资组合权重
    
    参数:
        returns: 收益率 DataFrame
        optimization_target: 优化目标 ('sharpe' 最大夏普比率, 'min_vol' 最小波动率)
    
    返回:
        最优权重、最优收益、最优风险、最优夏普比率
    """
    num_assets = len(returns.columns)
    
    # 目标函数:负夏普比率(因为要最小化)
    def neg_sharpe(weights):
        ret, vol, sharpe = calculate_portfolio_stats(returns, weights)
        return -sharpe  # 最小化负夏普比率 = 最大化夏普比率
    
    # 约束条件:权重和为 1
    constraints = ({
        'type': 'eq',
        'fun': lambda x: np.sum(x) - 1
    })
    
    # 边界:每个资产权重在 0-100% 之间
    bounds = tuple((0, 1) for _ in range(num_assets))
    
    # 初始猜测:等权重
    init_weights = np.array([1/num_assets] * num_assets)
    
    # 优化
    optimized = minimize(
        neg_sharpe,
        init_weights,
        method='SLSQP',
        bounds=bounds,
        constraints=constraints
    )
    
    opt_weights = optimized.x
    ret, vol, sharpe = calculate_portfolio_stats(returns, opt_weights)
    
    return opt_weights, ret, vol, sharpe

# 执行优化
print("\n=== 优化结果 ===")
opt_weights, opt_ret, opt_vol, opt_sharpe = optimize_portfolio(returns)

print(f"最优权重:{dict(zip(tickers, [f'{w:.2%}' for w in opt_weights]))}")
print(f"最优期望年化收益:{opt_ret:.2%}")
print(f"最优年化波动率:{opt_vol:.2%}")
print(f"最优夏普比率:{opt_sharpe:.2f}")

# 对比等权重组合
print(f"\n=== 对比等权重组合 ===")
print(f"收益提升:{opt_ret - ret:.2%}")
print(f"风险降低:{vol - opt_vol:.2%}")

步骤 6:计算有效前沿

def calculate_efficient_frontier(returns, num_points=100):
    """
    计算有效前沿
    """
    target_returns = np.linspace(
        returns.mean().min() * 252,
        returns.mean().max() * 252,
        num_points
    )
    
    efficient_returns = []
    efficient_volatilities = []
    efficient_weights = []
    
    for target_ret in target_returns:
        num_assets = len(returns.columns)
        
        # 目标:在给定收益下最小化风险
        def portfolio_volatility(weights):
            return np.sqrt(np.dot(weights.T, np.dot(returns.cov(), weights))) * np.sqrt(252)
        
        # 约束
        constraints = [
            {'type': 'eq', 'fun': lambda x: np.sum(x) - 1},  # 权重和为 1
            {'type': 'eq', 'fun': lambda x, tr=target_ret: 
             np.sum(returns.mean() * x) * 252 - tr}  # 达到目标收益
        ]
        
        bounds = tuple((0, 1) for _ in range(num_assets))
        init_weights = np.array([1/num_assets] * num_assets)
        
        try:
            result = minimize(
                portfolio_volatility,
                init_weights,
                method='SLSQP',
                bounds=bounds,
                constraints=constraints
            )
            
            if result.success:
                vol = result.fun
                ret = target_ret
                efficient_returns.append(ret)
                efficient_volatilities.append(vol)
                efficient_weights.append(result.x)
        except:
            continue
    
    return np.array(efficient_returns), np.array(efficient_volatilities), efficient_weights

# 计算有效前沿
frontier_ret, frontier_vol, frontier_weights = calculate_efficient_frontier(returns)

# 可视化有效前沿
plt.figure(figsize=(12, 8))

# 随机组合散点
plt.scatter(results[1], results[0], c=results[2], cmap='viridis', alpha=0.3, marker='o', label='随机组合')

# 有效前沿
plt.plot(frontier_vol, frontier_ret, 'r-', linewidth=3, label='有效前沿')

# 最优点标记
plt.scatter(opt_vol, opt_ret, c='red', s=200, marker='*', label=f'最优组合 (Sharpe={opt_sharpe:.2f})')

plt.xlabel('年化波动率(风险)')
plt.ylabel('期望年化收益')
plt.title('投资组合优化:有效前沿与最优组合')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

四、回测验证:优化组合真的有效吗?

def backtest_portfolio(prices, weights, initial_capital=100000):
    """
    简单回测
    """
    # 计算组合每日收益
    portfolio_returns = (prices.pct_change() * weights).sum(axis=1)
    
    # 计算累计收益
    cumulative_returns = (1 + portfolio_returns).cumprod()
    
    # 计算资产曲线
    equity_curve = initial_capital * cumulative_returns
    
    return equity_curve, portfolio_returns

# 回测最优组合
opt_weights_array = np.array(opt_weights)
equity_curve, daily_returns = backtest_portfolio(prices.dropna(), opt_weights_array)

# 计算回测指标
total_return = (equity_curve.iloc[-1] / equity_curve.iloc[0]) - 1
annualized_return = (1 + total_return) ** (252 / len(equity_curve)) - 1
volatility = daily_returns.std() * np.sqrt(252)
sharpe = (annualized_return - 0.03) / volatility
max_drawdown = (equity_curve / equity_curve.cummax() - 1).min()

print("\n=== 回测结果(最优组合) ===")
print(f"初始资金:¥100,000")
print(f"最终资金:¥{equity_curve.iloc[-1]:,.2f}")
print(f"总收益率:{total_return:.2%}")
print(f"年化收益:{annualized_return:.2%}")
print(f"年化波动:{volatility:.2%}")
print(f"夏普比率:{sharpe:.2f}")
print(f"最大回撤:{max_drawdown:.2%}")

# 绘制资产曲线
plt.figure(figsize=(14, 6))
plt.plot(equity_curve.index, equity_curve.values, 'b-', linewidth=2)
plt.xlabel('日期')
plt.ylabel('资产净值')
plt.title(f'投资组合回测:初始¥100,000 → 最终¥{equity_curve.iloc[-1]:,.0f}')
plt.grid(True, alpha=0.3)
plt.show()

五、进阶技巧:加入约束条件

实际投资中,我们往往需要添加各种约束:

def optimize_with_constraints(returns, constraints_dict=None):
    """
    带约束的组合优化
    
    约束示例:
    - 单资产上限:{'max_weight': 0.3}  # 单只股票不超过 30%
    - 行业约束:{'sector_limit': {'tech': 0.5}}  # 科技股不超过 50%
    - 最低收益:{'min_return': 0.15}  # 期望收益不低于 15%
    """
    num_assets = len(returns.columns)
    
    def neg_sharpe(weights):
        ret, vol, sharpe = calculate_portfolio_stats(returns, weights)
        return -sharpe
    
    constraints = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]
    
    # 添加最低收益约束
    if constraints_dict and 'min_return' in constraints_dict:
        min_ret = constraints_dict['min_return']
        constraints.append({
            'type': 'ineq',
            'fun': lambda x, mr=min_ret: np.sum(returns.mean() * x) * 252 - mr
        })
    
    bounds = []
    for i in range(num_assets):
        if constraints_dict and 'max_weight' in constraints_dict:
            bounds.append((0, constraints_dict['max_weight']))
        else:
            bounds.append((0, 1))
    
    init_weights = np.array([1/num_assets] * num_assets)
    
    result = minimize(
        neg_sharpe,
        init_weights,
        method='SLSQP',
        bounds=bounds,
        constraints=constraints
    )
    
    return result.x, result.fun

# 示例:添加单资产上限 40%
constrained_weights, _ = optimize_with_constraints(returns, {'max_weight': 0.4})
print("\n=== 带约束优化结果(单资产≤40%) ===")
print(f"权重:{dict(zip(tickers, [f'{w:.2%}' for w in constrained_weights]))}")

六、总结与实战建议

关键要点

  1. 分散投资降低风险:低相关性资产组合可显著降低波动率
  2. 优化不是预测:均值 - 方差模型基于历史数据,不代表未来表现
  3. 定期再平衡:建议每季度调整一次权重,保持最优配置
  4. 加入交易成本:实际操作需考虑手续费和冲击成本

下一步优化方向

  • 加入 Black-Litterman 模型(融入主观观点)
  • 使用风险平价(Risk Parity)替代均值 - 方差
  • 加入因子模型(Fama-French 五因子)
  • 实时数据接入与自动调仓

互动话题

  • 你的投资组合如何配置?
  • 用过哪些量化策略?效果如何?

欢迎在评论区交流讨论。如果觉得这篇文章对你有帮助,欢迎点赞 + 收藏,关注我获取更多量化实战干货!

参考资料

  1. Markowitz, H. (1952). Portfolio Selection. Journal of Finance.
  2. Grinold, R. & Kahn, R. Active Portfolio Management.
  3. PyPortfolioOpt 库:github.com/robertmarti…

代码已上传 GitHub:[链接待补充] 数据源:Yahoo Finance(美股)、Tushare(A 股)