
大家好,我是量化老王。作为《量化交易开发100篇》的第8篇,咱们在上一篇已经完成了“全行情适配+风险控制”的单股票组合策略搭建,解决了“单一策略风险高”“止损止盈不精细”的问题。但量化交易的另一大核心原则是“不要把鸡蛋放在一个篮子里”——单只股票哪怕策略再好,也可能因个股黑天鹅(如业绩暴雷、突发利空)导致大幅亏损。
今天第8篇,咱们就实现“多股票组合回测”:构建一个包含5-10只股票的量化组合,通过“分散投资+合理权重分配”Python量化交易:多股票组合策略搭建与风险控制实战指南(pdf),进一步降低单一股票的非系统性风险,同时保留策略的盈利能力。全程代码落地,整合前文的趋势/震荡识别、动态仓位、精细化止损止盈,形成完整的多股票量化交易体系。
一、核心逻辑:多股票组合的优势与搭建原则1. 为什么要做多股票组合?
单股票交易的风险主要来自两方面:一是系统性风险(大盘下跌带动个股下跌,所有策略都难规避);二是非系统性风险(个股自身问题,如财务造假、行业政策利空)。多股票组合的核心作用是“分散非系统性风险”——不同行业、不同风格的股票走势不完全同步,一只股票亏损,可能被其他股票的盈利抵消,让组合收益更平稳。
2. 多股票组合的搭建原则(新手直接套用)
为了让组合既分散风险又有盈利能力,咱们制定3个可落地的原则:
二、第一步:股票筛选与数据获取(实现)
咱们筛选5只不同行业的龙头股(兼顾流动性和稳定性),具体标的及行业如下:
股票代码
股票名称
所属行业
筛选理由
.SH
贵州茅台
消费(白酒)
行业龙头,业绩稳定,抗跌性强
.SZ
比亚迪
新能源
赛道龙头,成长属性强,波动适中
.SZ
迈瑞医疗
医疗健康
医疗器械龙头,刚需属性,稳定性好
.SZ
五 粮 液
消费(白酒)
次高端龙头,与茅台形成消费双配
.SH
中国平安
金融(保险)
金融龙头,估值低,分红稳定
代码:批量获取多股票数据
# 导入必备库(复用前文基础库)
import tushare as ts
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore') # 忽略警告,让代码更整洁
# 解决中文乱码
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 基础配置:Tushare Token+股票列表
ts.set_token('你的Tushare Token')
pro = ts.pro_api()
# 筛选的5只股票(代码+名称,便于后续标注)
stock_list = [
('600519.SH', '贵州茅台'),
('002594.SZ', '比亚迪'),
('300760.SZ', '迈瑞医疗'),
('000858.SZ', '五粮液'),
('601318.SH', '中国平安')
]
start_date = '20230101'
end_date = '20240630' # 回测周期与前文一致,便于对比
# 2. 批量获取多股票日线数据(封装成函数)
def get_multi_stock_data(stock_list, start_date, end_date):
"""
批量获取多股票日线数据
参数:
- stock_list: 股票代码+名称列表
- start_date/end_date: 起止日期
返回:
- stock_data_dict: 字典,key=股票名称,value=对应DataFrame
"""
stock_data_dict = {}
for ts_code, name in stock_list:
# 获取单股票数据(复用前文的预处理逻辑)
df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
# 数据预处理
df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')
df = df.sort_values('trade_date').set_index('trade_date').dropna()
# 保存到字典
stock_data_dict[name] = df
print(f"已获取{name}数据,形状:{df.shape}")
return stock_data_dict
# 执行函数,获取数据
stock_data = get_multi_stock_data(stock_list, start_date, end_date)
# 查看数据是否获取成功(以贵州茅台为例)
print("\n贵州茅台数据前3行:")
print(stock_data['贵州茅台'][['open', 'high', 'low', 'close', 'vol']].head(3))
第二步:整合前文核心策略(行情识别+风险控制)
将第6篇的“行情识别”、第7篇的“动态仓位”“精细化止损止盈”封装成统一函数,用于后续批量处理每只股票。
# 3. 整合核心策略函数(复用前文逻辑,统一封装)
def apply_strategy_with_risk_control(df):
"""
给单只股票的DataFrame应用完整策略(行情识别+动态仓位+精细化止损止盈)
参数:df 单只股票的日线DataFrame
返回:df 含策略信号、仓位、收益率的DataFrame
"""
# 3.1 行情识别(ATR+标准差)
def calculate_atr(data, period=14):
data['tr1'] = data['high'] - data['low']
data['tr2'] = abs(data['high'] - data['close'].shift(1))
data['tr3'] = abs(data['low'] - data['close'].shift(1))
data['tr'] = data[['tr1', 'tr2', 'tr3']].max(axis=1)
return data['tr'].rolling(window=period).mean()
df['ATR14'] = calculate_atr(df)
df['ATR_std20'] = df['ATR14'].rolling(window=20).std()
df['market_type'] = 'unknown'
df.loc[df['ATR_std20'] < 0.5, 'market_type'] = 'shock' # 震荡行情
df.loc[df['ATR_std20'] >= 0.5, 'market_type'] = 'trend' # 趋势行情
# 3.2 多均线+RSI策略信号
# 多均线策略(趋势用)
df['MA_short'] = df['close'].rolling(window=5).mean()
df['MA_mid'] = df['close'].rolling(window=10).mean()
df['MA_long'] = df['close'].rolling(window=20).mean()
df['ma_buy'] = ((df['MA_short'] > df['MA_mid']) & (df['MA_mid'] > df['MA_long'])) & (
~((df['MA_short'].shift(1) > df['MA_mid'].shift(1)) & (df['MA_mid'].shift(1) > df['MA_long'].shift(1))))
df['ma_sell'] = ((df['MA_short'] < df['MA_mid']) & (df['MA_mid'] < df['MA_long'])) & (
~((df['MA_short'].shift(1) < df['MA_mid'].shift(1)) & (df['MA_mid'].shift(1) < df['MA_long'].shift(1))))
# RSI策略(震荡用)
delta = df['close'].diff()
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.rolling(window=14).mean()
avg_loss = loss.rolling(window=14).mean()
rs = avg_gain / avg_loss
df['RSI'] = 100 - (100 / (1 + rs)).fillna(100)
df['rsi_buy'] = (df['RSI'] < 30) & (df['RSI'].shift(1) >= 30)
df['rsi_sell'] = (df['RSI'] > 70) & (df['RSI'].shift(1) <= 70)
# 3.3 动态仓位管理
def calculate_position(data, risk_tolerance=0.02, max_loss_pct=0.05):
data['base_position'] = 0.7 # 趋势基础仓位70%
data.loc[data['market_type'] == 'shock', 'base_position'] = 0.3 # 震荡30%
data['position_ratio'] = (risk_tolerance / max_loss_pct) * data['base_position']
return data['position_ratio'].clip(lower=0.1, upper=1.0) # 仓位限制
df['position_ratio'] = calculate_position(df)
# 3.4 精细化止损止盈
df['stop_loss_price'] = 0.0
df['take_profit_price'] = 0.0
df['entry_price'] = 0.0
df['final_buy'] = False
df['final_sell'] = False
entry_price = 0.0
position = 0 # 0空仓,1持仓
for date in df.index:

market_type = df.loc[date, 'market_type']
close = df.loc[date, 'close']
atr = df.loc[date, 'ATR14']
# 买入信号(空仓时)
if (df.loc[date, 'ma_buy'] and market_type == 'trend') or (df.loc[date, 'rsi_buy'] and market_type == 'shock'):
if position == 0:
entry_price = close
df.loc[date, 'entry_price'] = entry_price
df.loc[date, 'final_buy'] = True
position = 1
# 持仓时计算止损止盈
if position == 1:
profit_pct = (close - entry_price) / entry_price
atr_multiplier = 2.0 if market_type == 'trend' else 1.5
stop_loss = close - atr_multiplier * atr
df.loc[date, 'stop_loss_price'] = stop_loss
# 移动止盈逻辑
if profit_pct < 0.1:
take_profit = entry_price * 1.2
elif 0.1 <= profit_pct < 0.2:
take_profit = close - 1.0 * atr
else:
take_profit = close - 0.5 * atr
df.loc[date, 'take_profit_price'] = take_profit
# 卖出信号
if close < stop_loss or close > take_profit:
df.loc[date, 'final_sell'] = True
position = 0
entry_price = 0.0
# 3.5 计算单只股票的策略收益率
df['daily_return'] = 0.0
position = 0
pos_ratio = 0.0
for date in df.index:
if df.loc[date, 'final_buy']:
position = 1
pos_ratio = df.loc[date, 'position_ratio']
elif df.loc[date, 'final_sell']:
position = 0
pos_ratio = 0.0
if position == 1:
df.loc[date, 'daily_return'] = pos_ratio * (df.loc[date, 'close'].shift(-1) / df.loc[date, 'close'] - 1)
# 计算单只股票累计收益率
df['cum_return'] = (1 + df['daily_return']).cumprod()
return df
# 4. 批量给每只股票应用策略
stock_strategy_data = {}
for name, df in stock_data.items():
stock_strategy_data[name] = apply_strategy_with_risk_control(df.copy())
print(f"\n{name}策略应用完成,最终累计收益率:{(stock_strategy_data[name]['cum_return'].iloc[-2]-1)*100:.2f}%")
第三步:多股票组合构建与回测(核心步骤)
核心是“等权分配权重”+“组合收益加权计算”,同时对比“单股票策略”和“组合策略”的风险收益,验证组合的优势。
# 5. 多股票组合回测(等权分配)
# 5.1 统一所有股票的索引(确保日期对齐)
all_dates = pd.date_range(start=start_date, end=end_date, freq='B') # 交易日索引
weight = 1 / len(stock_list) # 等权权重(5只股票,每只20%)
# 5.2 计算组合每日收益率和累计收益率
combo_daily_return = pd.Series(0.0, index=all_dates) # 组合每日收益率
stock_cum_returns = {} # 存储每只股票的累计收益率(用于后续对比)
for name, df in stock_strategy_data.items():
# 对齐日期,缺失值填充为0
daily_return_aligned = df['daily_return'].reindex(all_dates, fill_value=0.0)
# 组合收益率 = 各股票收益率 × 权重 求和
combo_daily_return += daily_return_aligned * weight
# 保存单股票累计收益率(对齐日期)
stock_cum_returns[name] = (1 + daily_return_aligned).cumprod()
# 计算组合累计收益率
combo_cum_return = (1 + combo_daily_return).cumprod()
# 5.3 计算组合的核心评估指标(对比单股票)
def calculate_metrics(cum_return, daily_return):
"""计算策略核心指标:累计收益率、最大回撤、夏普比率"""
final_return = (cum_return.iloc[-1] - 1) * 100
# 最大回撤
cum_max = cum_return.cummax()
drawdown = (cum_return - cum_max) / cum_max
max_drawdown = drawdown.min() * 100
# 夏普比率(年化,无风险利率3%)
annual_return = final_return / 1.5 # 回测1.5年
daily_vol = daily_return.std() * (240 ** 0.5) # 年化波动率
sharpe = (annual_return - 3) / daily_vol if daily_vol != 0 else 0
return {
'累计收益率(%)': round(final_return, 2),
'最大回撤(%)': round(max_drawdown, 2),
'夏普比率': round(sharpe, 2)
}
# 计算组合指标
combo_metrics = calculate_metrics(combo_cum_return, combo_daily_return)
# 计算每只股票的指标
stock_metrics = {}
for name, cum_ret in stock_cum_returns.items():
daily_ret = stock_strategy_data[name]['daily_return'].reindex(all_dates, fill_value=0.0)
stock_metrics[name] = calculate_metrics(cum_ret, daily_ret)
# 5.4 输出对比结果
print("\n=== 多股票组合 vs 单股票策略核心指标对比 ===")
print(f"组合策略(等权5只股票):{combo_metrics}")
for name, metrics in stock_metrics.items():
print(f"{name}:{metrics}")
第四步:专业可视化(组合 vs 单股票效果对比)
用2张子图直观展示组合优势:① 组合累计收益率 vs 各单股票;② 组合最大回撤 vs 各单股票。
# 6. 可视化组合效果
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), gridspec_kw={'height_ratios': [3, 2]})
# 子图1:组合 vs 单股票累计收益率
ax1.plot(combo_cum_return.index, combo_cum_return, label='等权组合策略', color='red', linewidth=2.0)
for name, cum_ret in stock_cum_returns.items():
ax1.plot(cum_ret.index, cum_ret, label=name, linewidth=1.2, alpha=0.7)
ax1.set_title('多股票组合 vs 单股票累计收益率对比(2023-2024)', fontsize=14, pad=15)
ax1.set_ylabel('累计收益(单位:1)', fontsize=12)
ax1.legend(fontsize=10)
ax1.grid(True, alpha=0.3)
# 子图2:组合 vs 单股票最大回撤对比(柱状图)
names = list(stock_metrics.keys()) + ['组合策略']
max_drawdowns = [stock_metrics[name]['最大回撤(%)'] for name in names[:-1]] + [combo_metrics['最大回撤(%)']]
# 柱状图颜色(组合用红色突出)
colors = ['lightblue'] * len(names[:-1]) + ['red']
ax2.bar(names, max_drawdowns, color=colors, alpha=0.7)
ax2.set_title('多股票组合 vs 单股票最大回撤对比', fontsize=14, pad=15)
ax2.set_ylabel('最大回撤(%)', fontsize=12)
ax2.set_xlabel('策略/股票', fontsize=12)
ax2.tick_params(axis='x', rotation=45) # 旋转x轴标签,避免重叠
# 在柱状图上标注数值
for i, v in enumerate(max_drawdowns):
ax2.text(i, v + 0.2, f'{v:.2f}%', ha='center', fontsize=10)
plt.tight_layout()
plt.show()
可视化结果解读
1. 子图1中,组合策略的累计收益率可能不是最高的股票交易技术+实战快速+pdf,但走势最平稳——避免了单股票(如比亚迪)的大幅波动,哪怕某只股票短期亏损,其他股票的盈利能对冲风险;
2. 子图2中,组合策略的最大回撤明显低于大多数单股票——这是组合分散非系统性风险的核心体现,比如贵州茅台最大回撤可能15%Python量化交易:多股票组合策略搭建与风险控制实战指南(pdf),组合回撤可能只有8%,大幅提升了策略的抗风险能力。
三、新手常见问题解答
1. 为什么选5只股票?选越多越好吗?—— 5-10只是新手最优选择:太少达不到分散效果,太多会增加交易成本和管理难度,且超过10只后,分散风险的边际效益会递减;
2. 等权分配之外,还有其他权重方式吗?—— 有,比如“市值加权”(按股票市值分配权重)、“波动率加权”(波动小的股票权重高),后续会专门讲解动态权重策略;
3. 组合策略的收益率变低了,正常吗?—— 正常!组合的核心目标是“稳定盈利”,而非“追求极致收益”。用部分收益率换取更低的回撤股票交易技术+实战快速+pdf,是长期量化交易的理性选择。









