风险提示:本文所有代码仅供学习参考,不构成投资建议。量化交易有风险,入市需谨慎。
一、为什么你的投资组合跑不赢指数?
很多投资者有一个误区:"只要选到好股票,就能跑赢市场"。但现实是:
- 2025 年沪深 300 指数上涨 12.8%
- 散户平均收益率:-3.2%(上交所数据)
- 差距在哪?资产配置
诺贝尔经济学奖得主马科维茨(Harry Markowitz)在 1952 年提出的均值 - 方差模型(Mean-Variance Optimization),至今仍是资产配置的基石理论。核心思想很简单:
不要把所有鸡蛋放在一个篮子里,但要科学地分配篮子。
本文用完整代码实现一个投资组合优化系统,包含:
- 数据获取与预处理
- 均值 - 方差模型实现
- 有效前沿计算
- 最大夏普比率组合优化
- 回测验证
二、核心理论: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]))}")
六、总结与实战建议
关键要点
- 分散投资降低风险:低相关性资产组合可显著降低波动率
- 优化不是预测:均值 - 方差模型基于历史数据,不代表未来表现
- 定期再平衡:建议每季度调整一次权重,保持最优配置
- 加入交易成本:实际操作需考虑手续费和冲击成本
下一步优化方向
- 加入 Black-Litterman 模型(融入主观观点)
- 使用风险平价(Risk Parity)替代均值 - 方差
- 加入因子模型(Fama-French 五因子)
- 实时数据接入与自动调仓
互动话题:
- 你的投资组合如何配置?
- 用过哪些量化策略?效果如何?
欢迎在评论区交流讨论。如果觉得这篇文章对你有帮助,欢迎点赞 + 收藏,关注我获取更多量化实战干货!
参考资料:
- Markowitz, H. (1952). Portfolio Selection. Journal of Finance.
- Grinold, R. & Kahn, R. Active Portfolio Management.
- PyPortfolioOpt 库:github.com/robertmarti…
代码已上传 GitHub:[链接待补充] 数据源:Yahoo Finance(美股)、Tushare(A 股)