从单一股票到多元组合,让你的收益更稳健
在掌握了单个股票的择时策略后,你是否想过:如何将这些策略应用到多只股票上?如何分配资金才能获得更好的风险收益比?今天,我们就来深入探讨投资组合优化的实战技巧。
一、为什么需要投资组合?
真实案例的启示:2023年上半年,某投资者全仓押注某新能源股票,虽然一度获得30%的收益,但随后股价大幅回调,最终全年亏损15%。而另一位投资者将资金分散到5只不同行业的股票中,虽然单只股票最高收益只有20%,但全年整体收益达到18%。
这告诉我们一个核心道理:不要把所有鸡蛋放在一个篮子里。通过科学的资产配置,我们可以在控制风险的同时,获得更稳定的收益。
二、现代投资组合理论基础
1. 马科维茨均值-方差模型
现代投资组合理论的核心思想是:在给定的风险水平下,寻找预期收益最高的组合;或在给定的收益水平下,寻找风险最小的组合。
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport seaborn as snsfrom scipy.optimize import minimizeimport tushare as tsfrom datetime import datetime, timedelta# 设置样式plt.style.use('seaborn-v0_8-darkgrid')plt.rcParams['font.sans-serif'] = ['SimHei']plt.rcParams['axes.unicode_minus'] = False# 初始化tusharets.set_token('你的token')pro = ts.pro_api()# 获取多只股票数据(代表不同行业)def get_portfolio_data(tickers, start_date='20230101', end_date='20240601'): """ 获取投资组合数据 """ portfolio_data = {} for ticker in tickers: df = pro.daily(ts_code=ticker, start_date=start_date, end_date=end_date) df['trade_date'] = pd.to_datetime(df['trade_date']) df = df.sort_values('trade_date') df.set_index('trade_date', inplace=True) portfolio_data[ticker] = df['close'] # 合并数据 portfolio_df = pd.DataFrame(portfolio_data) # 对齐数据(确保所有股票有相同的交易日期) portfolio_df = portfolio_df.dropna() return portfolio_df# 选择6只不同行业的代表性股票tickers = [ '600519.SH', # 贵州茅台 - 消费 '601318.SH', # 中国平安 - 金融 '300750.SZ', # 宁德时代 - 新能源 '600036.SH', # 招商银行 - 银行 '000858.SZ', # 五粮液 - 消费 '601888.SH', # 中国中免 - 消费]portfolio_prices = get_portfolio_data(tickers)print(f"投资组合包含{len(tickers)}只股票")print(f"数据时间范围: {portfolio_prices.index[0]} 到 {portfolio_prices.index[-1]}")print(f"数据维度: {portfolio_prices.shape}")
三、投资组合绩效分析
1. 计算收益率与协方差矩阵
def calculate_portfolio_metrics(prices_df): """ 计算投资组合基础指标 """ # 计算日收益率 returns = prices_df.pct_change().dropna() # 计算年化收益率(假设252个交易日) annual_returns = (1 + returns.mean()) ** 252 - 1 # 计算年化波动率 annual_volatility = returns.std() * np.sqrt(252) # 计算协方差矩阵 cov_matrix = returns.cov() * 252 # 计算相关系数矩阵 corr_matrix = returns.corr() metrics = { 'returns': returns, 'annual_returns': annual_returns, 'annual_volatility': annual_volatility, 'cov_matrix': cov_matrix, 'corr_matrix': corr_matrix } return metrics# 计算投资组合指标metrics = calculate_portfolio_metrics(portfolio_prices)# 可视化相关系数矩阵plt.figure(figsize=(10, 8))sns.heatmap(metrics['corr_matrix'], annot=True, cmap='coolwarm', center=0, fmt='.2f', square=True)plt.title('股票收益率相关系数矩阵', fontsize=15, pad=20)plt.tight_layout()plt.show()# 打印各股票表现print("各股票年化表现:")for ticker in tickers: name = ticker[:6] print(f"{name}: 年化收益={metrics['annual_returns'][ticker]:.2%}, " f"年化波动={metrics['annual_volatility'][ticker]:.2%}")
2. 随机投资组合模拟
def simulate_random_portfolios(num_portfolios=10000): """ 模拟随机投资组合 """ returns = metrics['returns'] cov_matrix = metrics['cov_matrix'] results = np.zeros((3, num_portfolios)) weights_record = [] for i in range(num_portfolios): # 生成随机权重 weights = np.random.random(len(tickers)) weights /= np.sum(weights) weights_record.append(weights) # 计算组合收益 portfolio_return = np.sum(weights * metrics['annual_returns']) # 计算组合波动率 portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights))) # 计算夏普比率(假设无风险利率3%) sharpe_ratio = (portfolio_return - 0.03) / portfolio_volatility results[0, i] = portfolio_return results[1, i] = portfolio_volatility results[2, i] = sharpe_ratio return results, weights_record# 模拟10000个随机组合results, weights_record = simulate_random_portfolios(10000)# 找到最优组合max_sharpe_idx = np.argmax(results[2])min_vol_idx = np.argmin(results[1])print(f"\n最优夏普比率组合:")print(f"年化收益: {results[0, max_sharpe_idx]:.2%}")print(f"年化波动: {results[1, max_sharpe_idx]:.2%}")print(f"夏普比率: {results[2, max_sharpe_idx]:.2f}")print(f"\n最小波动组合:")print(f"年化收益: {results[0, min_vol_idx]:.2%}")print(f"年化波动: {results[1, min_vol_idx]:.2%}")print(f"夏普比率: {results[2, min_vol_idx]:.2f}")# 可视化有效前沿plt.figure(figsize=(12, 8))plt.scatter(results[1], results[0], c=results[2], cmap='viridis', alpha=0.6, s=10)plt.colorbar(label='夏普比率')plt.scatter(results[1, max_sharpe_idx], results[0, max_sharpe_idx], c='red', s=200, marker='*', label='最大夏普比率组合')plt.scatter(results[1, min_vol_idx], results[0, min_vol_idx], c='blue', s=200, marker='*', label='最小波动组合')plt.xlabel('年化波动率', fontsize=12)plt.ylabel('年化收益率', fontsize=12)plt.title('投资组合有效前沿', fontsize=15, pad=20)plt.legend()plt.grid(True, alpha=0.3)plt.tight_layout()plt.show()
四、优化投资组合权重
1. 最大化夏普比率
def optimize_portfolio_sharpe(metrics, risk_free_rate=0.03): """ 优化投资组合以最大化夏普比率 """ returns = metrics['returns'] cov_matrix = metrics['cov_matrix'] n_assets = len(tickers) # 约束条件:权重和为1 constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1}) # 边界条件:允许做空(权重可以为负),但限制在[-0.5, 0.5] bounds = tuple((-0.5, 0.5) for _ in range(n_assets)) # 初始权重(等权重) init_weights = n_assets * [1.0 / n_assets] # 目标函数:最小化负夏普比率 def negative_sharpe(weights): portfolio_return = np.sum(weights * metrics['annual_returns']) portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights))) sharpe = (portfolio_return - risk_free_rate) / portfolio_volatility return -sharpe # 优化 optimized = minimize(negative_sharpe, init_weights, method='SLSQP', bounds=bounds, constraints=constraints) return optimized.x# 优化权重optimal_weights = optimize_portfolio_sharpe(metrics)print("\n最优权重分配:")for ticker, weight in zip(tickers, optimal_weights): name = ticker[:6] print(f"{name}: {weight:.2%}")# 计算最优组合表现optimal_return = np.sum(optimal_weights * metrics['annual_returns'])optimal_volatility = np.sqrt(np.dot(optimal_weights.T, np.dot(metrics['cov_matrix'], optimal_weights)))optimal_sharpe = (optimal_return - 0.03) / optimal_volatilityprint(f"\n最优组合表现:")print(f"年化收益: {optimal_return:.2%}")print(f"年化波动: {optimal_volatility:.2%}")print(f"夏普比率: {optimal_sharpe:.2f}")# 可视化权重分配plt.figure(figsize=(10, 6))colors = plt.cm.Set3(np.linspace(0, 1, len(tickers)))plt.pie(optimal_weights, labels=[t[:6] for t in tickers], autopct='%1.1f%%', colors=colors, startangle=90)plt.title('最优投资组合权重分配', fontsize=15, pad=20)plt.tight_layout()plt.show()
2. 风险平价模型
def risk_parity_portfolio(cov_matrix, max_iter=1000, tolerance=1e-8): """ 风险平价模型:每个资产对组合风险的贡献相等 """ n = cov_matrix.shape[0] weights = np.ones(n) / n # 初始等权重 def risk_contribution(weights): portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights))) marginal_risk = np.dot(cov_matrix, weights) / portfolio_volatility risk_contrib = weights * marginal_risk return risk_contrib def objective(weights): rc = risk_contribution(weights) # 目标:使风险贡献尽可能相等 return np.sum((rc - rc.mean()) ** 2) # 约束条件 constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1}) bounds = tuple((0, 1) for _ in range(n)) # 不允许做空 # 优化 result = minimize(objective, weights, method='SLSQP', bounds=bounds, constraints=constraints, options={'maxiter': max_iter}) return result.x# 计算风险平价权重rp_weights = risk_parity_portfolio(metrics['cov_matrix'])print("\n风险平价权重分配:")for ticker, weight in zip(tickers, rp_weights): name = ticker[:6] print(f"{name}: {weight:.2%}")# 计算风险贡献def calculate_risk_contribution(weights, cov_matrix): """计算各资产的风险贡献""" portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights))) marginal_risk = np.dot(cov_matrix, weights) / portfolio_volatility risk_contrib = weights * marginal_risk risk_contrib_pct = risk_contrib / portfolio_volatility return risk_contrib_pct# 计算并比较风险贡献print("\n风险贡献分析:")rc = calculate_risk_contribution(rp_weights, metrics['cov_matrix'])for ticker, weight, contrib in zip(tickers, rp_weights, rc): name = ticker[:6] print(f"{name}: 权重={weight:.2%}, 风险贡献={contrib:.2%}")# 可视化风险贡献fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))# 权重对比x = range(len(tickers))width = 0.35ax1.bar([i - width/2 for i in x], optimal_weights, width, label='最大夏普', alpha=0.8)ax1.bar([i + width/2 for i in x], rp_weights, width, label='风险平价', alpha=0.8)ax1.set_xticks(x)ax1.set_xticklabels([t[:6] for t in tickers], rotation=45)ax1.set_ylabel('权重')ax1.set_title('权重分配对比')ax1.legend()ax1.grid(True, alpha=0.3)# 风险贡献ax2.bar([t[:6] for t in tickers], rc * 100)ax2.set_ylabel('风险贡献 (%)')ax2.set_title('风险平价模型 - 各资产风险贡献')ax2.grid(True, alpha=0.3)plt.tight_layout()plt.show()
五、动态资产配置策略
1. 基于波动率的动态调整
def dynamic_volatility_adjustment(prices_df, lookback=60, target_volatility=0.15): """ 基于波动率的动态资产配置 """ returns = prices_df.pct_change().dropna() n_assets = len(tickers) # 初始化 weights_history = [] portfolio_values = [100000] # 初始资金10万 for i in range(lookback, len(returns)): # 计算过去lookback天的波动率 recent_returns = returns.iloc[i-lookback:i] recent_cov = recent_returns.cov() * 252 # 优化权重(最小化波动率) def portfolio_volatility(weights): return np.sqrt(np.dot(weights.T, np.dot(recent_cov, weights))) constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1}) bounds = tuple((0, 1) for _ in range(n_assets)) init_weights = n_assets * [1.0 / n_assets] # 优化:在波动率不超过目标的前提下最大化收益 def objective(weights): return -np.sum(weights * recent_returns.mean() * 252) # 添加波动率约束 vol_constraint = {'type': 'ineq', 'fun': lambda x: target_volatility - portfolio_volatility(x)} try: result = minimize(objective, init_weights, method='SLSQP', bounds=bounds, constraints=[constraints, vol_constraint]) current_weights = result.x except: # 如果优化失败,使用等权重 current_weights = init_weights weights_history.append(current_weights) # 计算当日组合收益 daily_return = np.sum(current_weights * returns.iloc[i]) portfolio_values.append(portfolio_values[-1] * (1 + daily_return)) return weights_history, portfolio_values# 运行动态资产配置weights_history, portfolio_values = dynamic_volatility_adjustment(portfolio_prices)# 分析结果dynamic_portfolio_returns = pd.Series(portfolio_values).pct_change().dropna()annual_return_dynamic = (1 + dynamic_portfolio_returns.mean()) ** 252 - 1annual_volatility_dynamic = dynamic_portfolio_returns.std() * np.sqrt(252)sharpe_dynamic = (annual_return_dynamic - 0.03) / annual_volatility_dynamicprint(f"\n动态资产配置表现:")print(f"年化收益: {annual_return_dynamic:.2%}")print(f"年化波动: {annual_volatility_dynamic:.2%}")print(f"夏普比率: {sharpe_dynamic:.2f}")# 可视化权重变化weights_df = pd.DataFrame(weights_history, columns=[t[:6] for t in tickers])weights_df.index = portfolio_prices.index[60:] # 从第61天开始plt.figure(figsize=(14, 8))for column in weights_df.columns: plt.plot(weights_df.index, weights_df[column], label=column, linewidth=2)plt.xlabel('日期')plt.ylabel('权重')plt.title('动态资产配置 - 权重变化', fontsize=15, pad=20)plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')plt.grid(True, alpha=0.3)plt.tight_layout()plt.show()
2. 与基准对比
def compare_with_benchmarks(portfolio_values, tickers): """ 与基准对比 """ # 计算等权重组合 returns = portfolio_prices.pct_change().dropna() equal_weights = np.ones(len(tickers)) / len(tickers) equal_portfolio_returns = returns.dot(equal_weights) equal_portfolio_value = 100000 * (1 + equal_portfolio_returns).cumprod() # 计算沪深300表现(作为市场基准) # 这里简化处理,使用等权重组合的85% + 现金15%作为基准 benchmark_returns = equal_portfolio_returns * 0.85 + 0.03/252 * 0.15 benchmark_value = 100000 * (1 + benchmark_returns).cumprod() # 创建对比DataFrame comparison = pd.DataFrame({ '动态配置': portfolio_values, '等权重': equal_portfolio_value.values[:len(portfolio_values)], '市场基准': benchmark_value.values[:len(portfolio_values)] }, index=portfolio_prices.index[60:60+len(portfolio_values)]) # 计算绩效指标 performance_metrics = pd.DataFrame() for strategy in comparison.columns: strategy_returns = comparison[strategy].pct_change().dropna() ann_return = (1 + strategy_returns.mean()) ** 252 - 1 ann_vol = strategy_returns.std() * np.sqrt(252) sharpe = (ann_return - 0.03) / ann_vol # 最大回撤 cumulative = (1 + strategy_returns).cumprod() peak = cumulative.expanding().max() drawdown = (cumulative - peak) / peak max_dd = drawdown.min() performance_metrics[strategy] = [ f"{ann_return:.2%}", f"{ann_vol:.2%}", f"{sharpe:.2f}", f"{max_dd:.2%}" ] performance_metrics.index = ['年化收益', '年化波动', '夏普比率', '最大回撤'] return comparison, performance_metrics# 执行对比comparison_df, metrics_df = compare_with_benchmarks(portfolio_values, tickers)# 可视化对比fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10))# 净值曲线对比for column in comparison_df.columns: ax1.plot(comparison_df.index, comparison_df[column] / 100000, label=column, linewidth=2)ax1.set_ylabel('净值 (起始=1)')ax1.set_title('策略净值对比', fontsize=15, pad=20)ax1.legend()ax1.grid(True, alpha=0.3)# 回撤对比for column in comparison_df.columns: returns = comparison_df[column].pct_change().dropna() cumulative = (1 + returns).cumprod() peak = cumulative.expanding().max() drawdown = (cumulative - peak) / peak ax2.plot(comparison_df.index[1:], drawdown, label=column, linewidth=2)ax2.set_ylabel('回撤')ax2.set_title('最大回撤对比', fontsize=15, pad=20)ax2.legend()ax2.grid(True, alpha=0.3)ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3)plt.tight_layout()plt.show()print("\n绩效指标对比:")print(metrics_df)
六、实战建议与风险提示
1. 投资组合构建要点
def portfolio_construction_checklist(): """ 投资组合构建检查清单 """ checklist = { '分散化': { '行业分散': '至少覆盖3-5个不同行业', '相关性': '选择相关系数较低的资产', '数量': '建议5-10只股票,避免过度分散' }, '风险管理': { '单只股票上限': '建议不超过20%', '行业集中度': '单一行业不超过30%', '止损机制': '设置动态止损线' }, '再平衡规则': { '时间触发': '每季度或每半年再平衡', '阈值触发': '权重偏离目标超过5%时再平衡', '市场触发': '市场大幅波动时检查再平衡' } } return checklist# 打印检查清单checklist = portfolio_construction_checklist()for category, items in checklist.items(): print(f"\n{category}:") for item, suggestion in items.items(): print(f" • {item}: {suggestion}")
2. 常见误区与避免方法
def common_mistakes_and_solutions(): """ 常见投资组合管理误区及解决方案 """ mistakes = [ { '误区': '过度分散,持有太多股票', '问题': '增加管理难度,稀释优秀股票的贡献', '解决方案': '精选5-10只核心股票,其他通过ETF配置' }, { '误区': '只看历史收益,忽略风险', '问题': '高收益往往伴随高风险', '解决方案': '使用夏普比率等风险调整后收益指标' }, { '误区': '频繁调仓,交易成本过高', '问题': '侵蚀组合收益', '解决方案': '制定明确的再平衡规则,避免情绪化交易' }, { '误区': '忽略相关性变化', '问题': '危机时期相关性上升,分散效果降低', '解决方案': '定期检查相关性矩阵,加入低相关资产' } ] return pd.DataFrame(mistakes)# 显示常见误区mistakes_df = common_mistakes_and_solutions()print("\n常见误区及解决方案:")print(mistakes_df.to_string(index=False))