
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含300篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
你是否想过,为什么不能把所有的钱都押在一只股票上?为什么基金经理总在强调"不要把鸡蛋放在一个篮子里"?
这背后其实有一套严谨的数学理论——现代投资组合理论(Modern Portfolio Theory,简称 MPT)。诺贝尔经济学奖得主 Harry Markowitz 用一个简单的洞见改变了整个投资界:你不应该孤立地看待每一项资产,而应该关注它们组合在一起的表现。
本文将带你用 Python 从零实现投资组合优化,涵盖以下核心内容:
无论你是量化金融的初学者,还是想用 Python 提升投资分析能力的开发者,这篇文章都会对你有所帮助。
核心思想很简单:如果资产 A 涨的时候资产 B 跌(负相关),同时持有两者就能在不降低收益的前提下降低风险。这就是分散投资(Diversification)的魔力。
从此刻起,我们不再问"苹果是不是一只好股票?",而是问"苹果能让我的投资组合变得更好吗?"
投资组合的收益就是各资产收益的加权平均:
其中 w 是权重向量(比如 [0.5, 0.5]),μ 是各资产的期望收益向量。
风险并不是各资产波动率的简单加权平均,它取决于资产之间的协方差:
其中 Σ 是协方差矩阵(N × N),w 是权重向量(N × 1)。
为什么用矩阵运算? 如果你有 50 只股票,手动计算方差需要处理 50² = 2500 个交叉项。而矩阵运算
w @ Cov @ w一行代码就能搞定。
有效前沿是指在给定风险水平下,能获得最高期望收益的所有最优投资组合的集合。我们通过模拟 5000 个随机组合来近似它。
import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
# 1. 获取股票数据
tickers = ['AAPL', 'MSFT', 'GOOG', 'AMZN']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')['Close']
returns = data.pct_change().dropna()
# 2. 计算年化统计量
# 期望收益 = 日均收益 × 252(交易日)
mean_returns = returns.mean() * 252
# 协方差矩阵 = 日协方差 × 252
cov_matrix = returns.cov() * 252
print("--- 年化协方差矩阵 ---")
print(cov_matrix)num_portfolios = 5000
results = np.zeros((3, num_portfolios)) # 三行:收益、波动率、夏普比率
np.random.seed(42)
for i in range(num_portfolios):
# 1. 生成随机权重
weights = np.random.random(4)
weights /= np.sum(weights) # 归一化,使权重之和为 1
# 2. 计算投资组合收益
p_return = np.sum(mean_returns * weights)
# 3. 计算投资组合波动率(标准差)
# 公式:sqrt(w^T * Cov * w)
p_std = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
# 4. 存储结果
results[0, i] = p_return
results[1, i] = p_std
# 夏普比率(假设无风险利率为 0)
results[2, i] = results[0, i] / results[1, i]
# 转为 DataFrame 方便绘图
results_df = pd.DataFrame(results.T, columns=['Return', 'Volatility', 'Sharpe'])plt.figure(figsize=(10, 6))
# 散点图,颜色代表夏普比率
plt.scatter(results_df['Volatility'], results_df['Return'],
c=results_df['Sharpe'], cmap='viridis', marker='o', s=10, alpha=0.7)
plt.colorbar(label='Sharpe Ratio')
plt.xlabel('波动率(风险)')
plt.ylabel('期望收益')
plt.title('有效前沿(蒙特卡洛模拟)')
# 标记模拟中夏普比率最高的组合
max_sharpe_idx = results_df['Sharpe'].idxmax()
max_sharpe_port = results_df.iloc[max_sharpe_idx]
plt.scatter(max_sharpe_port['Volatility'], max_sharpe_port['Return'],
c='red', s=100, marker='*', label='最高夏普比率(模拟)')
plt.legend()
plt.show()如何解读这张图?
蒙特卡洛模拟只是近似值。要找到数学上精确的最优权重,我们需要用优化器。
import scipy.optimize as sco
def get_portfolio_stats(weights, mean_returns, cov_matrix, rf_rate=0):
"""
返回 [投资组合收益, 波动率, 夏普比率]
"""
weights = np.array(weights)
port_return = np.sum(mean_returns * weights)
port_volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
# 夏普比率 =(收益 - 无风险利率)/ 波动率
sharpe = (port_return - rf_rate) / port_volatility
return [port_return, port_volatility, sharpe]
# 目标函数 1:最大化夏普比率 → 最小化负夏普比率
def neg_sharpe(weights, mean_returns, cov_matrix, rf_rate=0):
return -get_portfolio_stats(weights, mean_returns, cov_matrix, rf_rate)[2]
# 目标函数 2:最小化波动率
def minimize_volatility(weights, mean_returns, cov_matrix):
return get_portfolio_stats(weights, mean_returns, cov_matrix)[1]为什么要取负值? 因为 scipy 只有
minimize函数。要最大化 f(x),就最小化 -f(x)。
num_assets = len(tickers)
# 约束:权重之和 = 1(100% 投资)
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
# 边界:每个权重在 0 到 1 之间(不允许卖空)
bounds = tuple((0, 1) for _ in range(num_assets))
# 初始猜测:等权分配
init_guess = num_assets * [1. / num_assets]A. 最大夏普比率组合(切线组合)
# 最小化负夏普比率
opt_sharpe = sco.minimize(neg_sharpe,
init_guess,
args=(mean_returns, cov_matrix),
method='SLSQP',
bounds=bounds,
constraints=constraints)
# 提取结果
max_sharpe_weights = opt_sharpe.x
max_sharpe_stats = get_portfolio_stats(max_sharpe_weights, mean_returns, cov_matrix)
print("--- 最大夏普比率组合 ---")
print(f"收益率:{max_sharpe_stats[0]*100:.2f}%")
print(f"波动率:{max_sharpe_stats[1]*100:.2f}%")
print(f"夏普比率:{max_sharpe_stats[2]:.2f}")
print("\n最优权重:")
for ticker, weight in zip(tickers, max_sharpe_weights):
print(f"{ticker}:{weight*100:.2f}%")运行结果示例:
--- 最大夏普比率组合 ---
收益率:28.91%
波动率:31.30%
夏普比率:0.92
最优权重:
AAPL:59.24%
MSFT:0.00%
GOOG:1.29%
AMZN:39.47%注意优化器可能会将某些资产的权重设为 0%。这意味着 MPT 认为在当前条件下,该资产对组合没有正向贡献。
B. 最小波动率组合
# 最小化波动率
opt_vol = sco.minimize(minimize_volatility,
init_guess,
args=(mean_returns, cov_matrix),
method='SLSQP',
bounds=bounds,
constraints=constraints)
min_vol_weights = opt_vol.x
min_vol_stats = get_portfolio_stats(min_vol_weights, mean_returns, cov_matrix)
print("\n--- 最小波动率组合 ---")
print(f"收益率:{min_vol_stats[0]*100:.2f}%")
print(f"波动率:{min_vol_stats[1]*100:.2f}%")
print(f"夏普比率:{min_vol_stats[2]:.2f}")现代投资组合理论告诉我们,风险分为两类:
CAPM 公式为:
其中 Beta(β) 衡量资产相对于市场的敏感度,Alpha(α) 衡量超出市场预期的超额收益。
from scipy import stats
# 1. 获取数据
tickers = ['NVDA', 'SPY']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')['Close']
returns = data.pct_change().dropna()
# 2. 准备回归数据
X = returns['SPY'].values # 市场收益(自变量)
Y = returns['NVDA'].values # 股票收益(因变量)
# 3. 执行线性回归
slope, intercept, r_value, p_value, std_err = stats.linregress(X, Y)
beta = slope # Beta = 回归斜率
alpha = intercept # Alpha = 回归截距
r_squared = r_value ** 2
print(f"--- CAPM 回归结果(NVDA vs SPY)---")
print(f"Beta(系统风险):{beta:.4f}")
print(f"Alpha(日超额收益):{alpha:.5f}")
print(f"R²(拟合优度):{r_squared:.4f}")运行结果示例:
Beta(系统风险):1.7182
Alpha(日超额收益):0.00175
R²(拟合优度):0.5140解读: Beta 约为 1.72,说明 NVDA 是高 Beta 股票——市场涨 1%,NVDA 平均涨约 1.72%;反之市场跌时,它也会跌得更多。
Beta 并非一成不变。我们可以用滚动窗口来观察它随时间的变化:
# 计算 60 天滚动 Beta
rolling_cov = returns['NVDA'].rolling(window=60).cov(returns['SPY'])
rolling_var = returns['SPY'].rolling(window=60).var()
rolling_beta = rolling_cov / rolling_var
plt.figure(figsize=(10, 5))
rolling_beta.plot(color='purple', label='60 天滚动 Beta')
plt.axhline(beta, color='black', linestyle='--', label='平均 Beta')
plt.title("NVDA 的滚动 Beta 变化")
plt.legend()
plt.show()在实际场景中,用户不会说"给我切线组合",而是说"我偏保守"或"我能承受高风险"。我们可以用不同的目标波动率来匹配不同的风险偏好。
# 资产:股票(SPY)、债券(TLT)、黄金(GLD)
tickers = ['SPY', 'TLT', 'GLD']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31', progress=False)['Close']
returns = data.pct_change().dropna()
mean_returns = returns.mean() * 252
cov_matrix = returns.cov() * 252
def get_port_stats(weights):
"""计算投资组合的收益和波动率"""
weights = np.array(weights)
ret = np.sum(mean_returns * weights)
vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
return ret, vol
def robo_advisor(target_volatility):
"""根据目标波动率优化投资组合"""
num_assets = len(tickers)
# 目标:最大化收益(最小化负收益)
def neg_return(weights):
return -get_port_stats(weights)[0]
# 约束条件
constraints = (
{'type': 'eq', 'fun': lambda x: np.sum(x) - 1}, # 权重之和 = 1
{'type': 'eq', 'fun': lambda x: get_port_stats(x)[1] - target_volatility} # 波动率 = 目标值
)
bounds = tuple((0, 1) for _ in range(num_assets))
init_guess = num_assets * [1. / num_assets]
result = sco.minimize(neg_return, init_guess, method='SLSQP',
bounds=bounds, constraints=constraints)
return result.x
# 运行三种风险偏好
profiles = {
"保守型(波动率 5%)": 0.05,
"均衡型(波动率 10%)": 0.10,
"激进型(波动率 15%)": 0.15
}
print("--- 智能投顾推荐 ---")
for profile, target_vol in profiles.items():
try:
weights = robo_advisor(target_vol)
ret, vol = get_port_stats(weights)
print(f"\n{profile}:")
print(f" 期望收益:{ret*100:.2f}%")
print(f" 配置:SPY {weights[0]*100:.0f}% | TLT {weights[1]*100:.0f}% | GLD {weights[2]*100:.0f}%")
except Exception as e:
print(f"无法求解 {profile},目标可能不可行。")本文带你完整走了一遍现代投资组合理论的核心流程:
scipy.optimize 精确求解了最大夏普比率组合和最小波动率组合。掌握这些知识后,你就具备了用 Python 进行量化投资分析的基础能力。下一步可以探索回测引擎、交易成本建模等更深入的主题。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐