本次复现的量化策略来自国元证券的《如何用ETF构造绝对收益组合——基于风险预算的资产配置策略》
量化策略思想
基本思想:战略(风险预算)+战术(股债择时、黄金择时)
战略
- 使用风险预算模型计算股债金的仓位
- 风险分配比例:股票/债券/黄金:97/2/1
- 风险资产(股票)持仓不超过30%
战术
股债择时
ERP指标
- 买入信号:加权ERP分位数>70%,则下一持仓周期股票市场发出看多信号,债券市场发出看空信号;
- 卖出信号:加权ERP分位数<30%,则下一持仓周期股票市场发出看空信号,债券市场发出看多信号。
量价信号
- 买入信号:成交量破近M日成交量均值以及收盘价在N日均线上
- 卖出信号:价格下穿N日均线。
合成指标
- 买入信号:两个指标都发出看多信号;
- 卖出信号:两个指标都发出看空信号。
黄金择时
美债实际收益率
- 买入信号:美债实际收益率≤MA(20);
- 卖出信号:美债实际收益率>MA(20)。
VIX指数
- 买入信号:VIX15天最大值>20。
价格动量
- 买入信号:黄金价格250动量>0。
合成指标
-
买入信号:三个指标的的合计净信号方向≥2;
-
卖出信号:三个指标的的合计净信号方向<0。
量化代码复现
考虑到数据量较大,故将回测区间缩短为2019-2023年
每月进行调仓
暂不考虑交易费用问题
import pandas as pd
import os
import matplotlib.pyplot as plt
from pylab import mpl
import numpy as np
import akshare as ak
from scipy.optimize import minimize
from datetime import timedelta
from pandas.tseries.offsets import BDay
from matplotlib.ticker import PercentFormatter
from matplotlib.ticker import FuncFormatter
## 绘制三类资产的历史净值曲线
# 1、处理中证800指数
zz800 = pd.DataFrame()
for root, dirs, files in os.walk(r"D:\学习\python学习\课程\期末数据\指数"):
for file in files:
if file.endswith('.xlsx'): # 检查文件是否为Excel文件
excel_path = os.path.join(root, file)
print(excel_path)
try:
df = pd.read_excel(excel_path, engine='openpyxl')
df = df.iloc[2:]
new_colname = ["index_code", "date", "open_index", "high_index", "low_index", "close_index", "volume", "amount", "return"]
df.columns = new_colname
df = df[df["index_code"] == "000906"]
zz800 = pd.concat([zz800, df], axis=0, ignore_index=True)
print(zz800)
except Exception as e:
print(f"读取文件{file}时出错: {e}")
# 2、处理上海黄金现货
gold = pd.read_excel(r"D:\学习\python学习\课程\期末数据\上海黄金现货\CGM_Shgtrd.xlsx")
gold = gold[2:]
gold = gold[(gold["Goldtype"] == "Au9999") | (gold["Goldtype"] == "AU9999")]
gold.columns = ["Goldtype", "date", "open_price", "close_price", "high_price", "low_price", "gap_price", "gap_rate", "volumn", "amount", "average_price", "holding"]
gold["close_value"] = gold["close_price"]*gold["holding"]
# print(gold)
# 3、处理中债综合指数
bond_index = pd.read_excel(r"D:\学习\python学习\课程\期末数据\中债综合指数.xlsx")
bond_index.columns = ["date", "close_index", "gap_price", "gap_rate", "cur_gap_price", "cur_gap_rate"]
# print(bond_index)
# 将'date'列转换为datetime类型
zz800['date'] = pd.to_datetime(zz800['date'])
gold['date'] = pd.to_datetime(gold['date'])
bond_index['date'] = pd.to_datetime(bond_index['date'])
# 统一时间区间
min_date = max(zz800['date'].min(), gold['date'].min(), bond_index['date'].min())
max_date = min(zz800['date'].max(), gold['date'].max(), bond_index['date'].max())
full_range = pd.date_range(start=min_date, end=max_date, freq='D')
# 对三个df应用新日期范围
zz800 = zz800.set_index('date').reindex(full_range).rename_axis('date').reset_index()
gold = gold.set_index('date').reindex(full_range).rename_axis('date').reset_index()
bond_index = bond_index.set_index('date').reindex(full_range).rename_axis('date').reset_index()
# 获取等权df
aver_index = zz800["close_index"]/zz800["close_index"].iloc[0] + gold["close_price"]/gold["close_price"].iloc[0] + bond_index["close_index"]/bond_index["close_index"].iloc[0]
data = {
'date':zz800['date'],
'aver_index':aver_index
}
aver_df = pd.DataFrame(data)
# 计算四个df的历史净值
zz800['sin_value'] = zz800['close_index']/zz800['close_index'].iloc[0]
gold['sin_value'] = gold['close_price']/gold['close_price'].iloc[0]
bond_index['sin_value'] = bond_index['close_index']/bond_index['close_index'].iloc[0]
aver_df['sin_value'] = aver_df['aver_index']/aver_df['aver_index'].iloc[0]
# print(gold)
# 删除空值
zz800 = zz800.dropna(subset = ['sin_value'])
gold = gold.dropna(subset = ['sin_value'])
bond_index = bond_index.dropna(subset = ['sin_value'])
aver_df = aver_df.dropna(subset = ['sin_value'])
# 绘制图表
plt.figure(figsize=(10, 5))
plt.rcParams['font.family'] = ['SimHei']
# 对于每一个DataFrame,绘制相应的close值
plt.plot(zz800['date'], zz800['sin_value'], label='中证800', linestyle = '-', color = (0, 0, 1))
plt.plot(gold['date'], gold['sin_value'], label='上海黄金现货', linestyle = '-', color = (0, 0.5, 1))
plt.plot(bond_index['date'], bond_index['sin_value'], label='中证综合债指数', color = (0, 0.4, 0))
plt.plot(aver_df['date'], aver_df['sin_value'], label='等权', linestyle = '-', color = (1, 0, 0))
# 添加标题和标签
plt.title('三类资产的历史净值曲线')
plt.xlabel('日期')
plt.ylabel('历史净值')
plt.legend() # 显示图例
# 展示图形
plt.show()
# 绘制三类资产历史表现
# 1、计算年化收益率
years = (zz800['date'].iloc[-1] - zz800['date'].iloc[0]).days / 365.25
annual_return = []
for df in zz800, gold, bond_index, aver_df:
annual_return.append((1 + df['sin_value'].iloc[-1]) ** (1 / years) - 1)
# 2、计算波动率
def year_volatility(df):
df['close_index'] = df['close_index'].astype('float')
df['log_return'] = np.log(df['close_index'] / df['close_index'].shift(1))
df.dropna(subset = ['log_return'], inplace=True)
daily_volatility = df['log_return'].std()
trading_days = 252
annual_volatility = daily_volatility * np.sqrt(trading_days)
return annual_volatility
gold['close_price'] = gold['close_price'].astype('float')
gold['log_return'] = np.log(gold['close_price'].iloc[1:] / gold['close_price'].shift(1).iloc[1:])
gold.dropna(subset = ['log_return'], inplace=True)
daily_volatility_gold = gold['log_return'].std()
trading_days = 252
annual_volatility_gold = daily_volatility_gold * np.sqrt(trading_days)
aver_df['aver_index'] = aver_df['aver_index'].astype('float')
aver_df['log_return'] = np.log(aver_df['aver_index'] / aver_df['aver_index'].shift(1))
aver_df.dropna(subset = ['log_return'], inplace=True)
daily_volatility_aver = aver_df['log_return'].std()
annual_volatility_aver = daily_volatility_aver * np.sqrt(trading_days)
annual_volatility = [year_volatility(zz800), annual_volatility_gold, year_volatility(bond_index), annual_volatility_aver]
# print(annual_volatility)
# 3、计算夏普比率
sharp = []
for i in range(4):
sharp.append((annual_return[i]-0.02)/annual_volatility[i])
# print(sharp)
# 4、计算最大回撤
def MDD(df):
df['CumMax'] = df['close_index'].cummax()
df['Drawdown'] = (df['close_index'] - df['CumMax']) / df['CumMax']
max_drawdown = abs(df['Drawdown'].min())
return max_drawdown
gold['CumMax'] = gold['close_price'].cummax()
gold['Drawdown'] = (gold['close_price'] - gold['CumMax']) / gold['CumMax']
max_drawdown_gold = abs(gold['Drawdown'].min())
aver_df['CumMax'] = aver_df['aver_index'].cummax()
aver_df['Drawdown'] = (aver_df['aver_index'] - aver_df['CumMax']) / aver_df['CumMax']
max_drawdown_aver = abs(aver_df['Drawdown'].min())
max_drawdown = [MDD(zz800), max_drawdown_gold, MDD(bond_index), max_drawdown_aver]
# print(max_drawdown)
# 5、计算卡玛比率
kama = []
for i in range(4):
kama.append(annual_return[i]/max_drawdown[i])
# print(kama)
return_data = {
"资产名称":["中证800", "上海黄金现货", "中债综合指数", "等权"],
"年化收益率":annual_return,
"波动率":annual_volatility,
"夏普比率":sharp,
"最大回撤":max_drawdown,
"卡玛比率":kama
}
return_performance = pd.DataFrame(return_data)
# return_performance
# 百分号显示
def percentage(df, column):
df[column] = df[column].apply(lambda x: '{:.2%}'.format(x))
percentage(return_performance, "年化收益率")
percentage(return_performance, "波动率")
percentage(return_performance, "最大回撤")
print("三类资产历史表现")
return_performance
# 绘制【风险预算 vs 固定股债比净值】 和 【风险预算 vs 固定股债比仓位】
# 将中证800和中债综合指数的时间区间设定为 2019-01-01 到 2024-12-31,并将其日收益率数据放置在一个表中
start_date = pd.to_datetime('2018-10-01')
end_date = pd.to_datetime('2024-12-31')
zz800.set_index('date', inplace=True)
bond_index.set_index('date', inplace=True)
zz800 = zz800[(zz800.index >= start_date) & (zz800.index <= end_date)]
bond_index = bond_index[(bond_index.index >= start_date) & (bond_index.index <= end_date)]
current_date = pd.to_datetime('2019-01-01')
monthly_results = []
target_risk_contributions = np.array([0.98, 0.02])
def risk_budget_objective(weights, cov, target_risk_contribs):
weights = np.array(weights)
sigma = np.sqrt(np.dot(weights, np.dot(cov, weights)))
MRC = np.dot(cov, weights)/sigma
TRC = weights * MRC
delta_TRC = [sum((i - TRC)**2) for i in TRC]
risk_contrib_diff = TRC - target_risk_contribs
return np.sum(risk_contrib_diff**2)
def total_weight_constraint(x):
return np.sum(x)-1.0
fixed_value = [] # 存储固定股债比策略的月末价值
while current_date <= end_date:
# 计算当前月的最后一天
next_month_first_day = (current_date.replace(day=1) + pd.offsets.MonthBegin(1))
month_end_date = next_month_first_day - pd.Timedelta(days=1)
# 获取过去60天的交易日的数据
look_back_period = current_date - BDay(60)
try:
zz800_window = zz800.loc[look_back_period:current_date]
bond_index_window = bond_index.loc[look_back_period:current_date]
# 创建包含股票和债券回报率的数据集
stock_bond_data = {
'return_stock': zz800_window['log_return'],
'return_bond': bond_index_window['log_return']
}
stock_bond_return = pd.DataFrame(stock_bond_data).dropna()
if not stock_bond_return.empty:
# 计算协方差矩阵
R_stock_bond_cov = stock_bond_return.cov()
stock_bond_cov = np.array(R_stock_bond_cov)
# 定义随机的初始化权重
bounds = [
(0.0, 0.3),
(0.7, 1.0)
]
weight_x0 = np.array([
np.random.uniform(low=bounds[0][0], high=bounds[0][1]),
np.random.uniform(low=bounds[1][0], high=bounds[1][1])
])
weight_x0 /= np.sum(weight_x0)
constraints = ({'type': 'eq', 'fun': total_weight_constraint})
options={'disp': False, 'maxiter': 1000, 'ftol': 1e-20}
solution = minimize(risk_budget_objective, weight_x0, args=(stock_bond_cov, target_risk_contributions), bounds=bounds, constraints=constraints, method='SLSQP', options=options)
final_weights = solution.x
value = zz800_window['close_index'].iloc[-1]*final_weights[0]/zz800['close_index'].iloc[0] + bond_index_window['close_index'].iloc[-1]*final_weights[1]/bond_index['close_index'].iloc[0]
fixed_value.append(zz800_window['close_index'].iloc[-1]*0.2/zz800['close_index'].iloc[0] + bond_index_window['close_index'].iloc[-1]*0.8/bond_index['close_index'].iloc[0])
# 将结果存储起来
monthly_results.append({
'month': current_date.strftime('%Y-%m'),
'cov_matrix': stock_bond_cov,
'weights': final_weights,
'value': value
})
else:
print(f"Warning: No valid data found for {current_date.strftime('%Y-%m')}")
except Exception as e:
print(f"Error processing {current_date.strftime('%Y-%m')}: {str(e)}")
# 移动到下一个月
current_date = next_month_first_day
# 打印或进一步处理 monthly_results 中的结果
risk_net_value = []
for result in monthly_results:
print(f"Month: {result['month']}")
print("协方差矩阵:")
print(result['cov_matrix'])
print("最终权重:")
print(f"{result['weights'][0]:.2%} 投资于 中证800")
print(f"{result['weights'][1]:.2%} 投资于 中债综合指数")
risk_net_value.append(result['value']/monthly_results[0]['value'])
# 计算固定股债比净值
fixed_net_value = [value/fixed_value[0] for value in fixed_value]
date = [result['month'] for result in monthly_results]
# 计算差额
gap = [risk_net_value[num] - fixed_net_value[num] for num in range(len(risk_net_value))]
# 绘制风险预算 VS 固定股债比净值
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
axes[0].set_xlabel('日期')
axes[0].plot(date, fixed_net_value, label='固定比例股债', linestyle = '-', color = (0, 0, 1))
axes[0].plot(date, risk_net_value, label='风险预算股债', linestyle = '-', color = (0.5, 0, 0.5))
# 创建次轴(右轴)用于面积图
ax2 = axes[0].twinx()
ax2.fill_between(date, gap, color = (0, 0.5, 0.5), alpha=0.3, label='超额(右)')
# 设置 x 轴刻度(每隔一年显示一次)
axes[0].set_xticks(date[::6]) # 隔半年显示一次
axes[0].set_xticklabels(date[::6], rotation=45)
# 添加标题和标签
axes[0].set_title('风险预算 vs 固定股债比净值')
axes[0].legend() # 显示图例
# 绘制风险预算 VS 固定股债比仓位
stock_weight = [result['weights'][0] for result in monthly_results]
axes[1].fill_between(date, stock_weight, color="navy", alpha=0.3, label = '中证800')
axes[1].fill_between(date, 1, stock_weight, color="darkgoldenrod", alpha=0.3, label = '中债综合指数')
axes[1].set_xticks(date[::6]) # 隔半年显示一次
axes[1].set_xticklabels(date[::6], rotation=45)
# 纵坐标显示百分比
axes[1].yaxis.set_major_formatter(PercentFormatter(xmax=1))
axes[1].set_title('风险预算 vs 固定股债比仓位')
axes[1].legend() # 显示图例
# 展示图形
plt.show()
# 绘制ERP信号与指数的情况,观察ERP信号对指数顶部的预测效果
# 1、计算ERP
zz800_PE = pd.read_excel(r"D:\学习\python学习\课程\期末数据\中证800指数滚动5年市盈率.xlsx")
zz800_PE.columns = ["date", "close_index", "PE"]
treasury10 = pd.read_excel(r"D:\学习\python学习\课程\期末数据\中国_国债到期收益率_10年.xlsx")
treasury10.columns = ["date", "return"]
# 统一时间区间
zz800_PE['date'] = pd.to_datetime(zz800_PE['date'])
treasury10['date'] = pd.to_datetime(treasury10['date'])
zz800_PE = zz800_PE.set_index('date').reindex(full_range).rename_axis('date').reset_index()
treasury10 = treasury10.set_index('date').reindex(full_range).rename_axis('date').reset_index()
zz800_PE = zz800_PE.dropna()
treasury10 = treasury10.dropna()
ERP = 1/zz800_PE['PE'] - treasury10['return']/100
ERP_data = {
'date': zz800_PE['date'],
'ERP': ERP
}
ERP_df = pd.DataFrame(ERP_data)
ERP_df = ERP_df.reset_index(drop=True)
# 2、计算ERP滚动五年分位数
percentiles = []
for i in range(len(ERP_df)):
current_date = ERP_df.loc[i, 'date']
current_erp = ERP_df.loc[i, 'ERP']
# 确定过去五年的起始日期
start_date = current_date - pd.DateOffset(years=5)
# 筛选出过去五年的ERP数据
rolling_data = ERP_df[(ERP_df['date'] >= start_date) & (ERP_df['date'] <= current_date)]['ERP']
# 如果过去五年的数据不足,则跳过(或可以根据需求填充为NaN)
if len(rolling_data) < 3:
percentiles.append(np.nan)
continue
# 对筛选出的数据进行排序
sorted_data = np.sort(rolling_data)
# 找到当前ERP值在排序后的数据中的位置
rank = np.searchsorted(sorted_data, current_erp, side='left')
# 计算分位数
percentile = rank / len(sorted_data)
percentiles.append(percentile)
# 将分位数添加到DataFrame中
ERP_df['percentile'] = percentiles
# 统一时间区间
ERP_df.set_index('date', inplace=True)
ERP_df = ERP_df[(ERP_df.index >= '2018-10-01') & (ERP_df.index <= '2024-12-31')]
# 3、绘制图7
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(zz800.index, zz800['close_index'], label='中证800', linestyle = '-', color = (0, 0, 1))
# 创建次轴(右轴)用于面积图
ax2 = ax1.twinx()
ax2.fill_between(ERP_df.index, ERP_df['percentile']*100, color = "goldenrod", alpha=0.3, label='ERP五年分位数(%,右)')
# 设置 x 轴刻度
ax1.set_xticks(date[::6]) # 隔半年显示一次
ax1.set_xticklabels(date[::6], rotation=45)
# 添加标题和标签
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
lines = lines1 + lines2
labels = labels1 + labels2
ax1.legend(lines, labels, loc='upper right')
ax1.set_title('ERP信号对于指数顶部的预测效果明显')
# 展示图形
plt.show()
# 定义多头策略持仓
def calculate_position_long(df, initial_capital):
situation = []
for i in range(len(df['signal'])):
if i == 0:
if df.iloc[i]['signal'] == 1:
sit = 1
else:
sit = 0
else:
if situation[i-1] + df.iloc[i]['signal'] > 1:
sit = 1
elif situation[i-1] + df.iloc[i]['signal'] <= -1:
sit = 0
else:
sit = situation[i-1] + df.iloc[i]['signal']
situation.append(sit)
df['situation'] = situation
df['position'] = initial_capital
# 如果第一行signal为1,则买入
if df['signal'].iloc[0] == 1:
df.loc[df.index[0], 'position'] = initial_capital / df['close_index'].iloc[0] * df['close_index'].iloc[0]
# 遍历后续行
for i in range(1, len(df)):
prev_situation = df['situation'].iloc[i-1]
curr_signal = df['signal'].iloc[i]
# 计算position
if df['situation'].iloc[i] == 1:
if curr_signal == 1 and prev_situation == 0:
# 买入
shares = (df['position'].iloc[i-1]) / df['close_index'].iloc[i]
df.loc[df.index[i], 'position'] = shares * df['close_index'].iloc[i]
else:
# 持有
shares = df['position'].iloc[i-1] / df['close_index'].iloc[i-1]
df.loc[df.index[i], 'position'] = shares * df['close_index'].iloc[i]
# 卖出
elif df['situation'].iloc[i] == 0 and curr_signal == -1 and prev_situation == 1:
shares = df['position'].iloc[i-1] / df['close_index'].iloc[i-1]
df.loc[df.index[i], 'position'] = shares * df['close_index'].iloc[i]
else:
# 空仓,保持现金不变
df.loc[df.index[i], 'position'] = df['position'].iloc[i-1]
return df
# 定义多空策略持仓
def calculate_position_ls(df, initial_capital):
situation = []
for i in range(len(df['signal'])):
if i == 0:
sit = df.iloc[i]['signal']
else:
if situation[i-1] + df.iloc[i]['signal'] > 1:
sit = 1
elif situation[i-1] + df.iloc[i]['signal'] < -1:
sit = -1
else:
sit = situation[i-1] + df.iloc[i]['signal']
situation.append(sit)
df['situation'] = situation
df['position'] = 0.0
# 处理第一行
if df['signal'].iloc[0] == 1:
# 买入
shares = initial_capital / df['close_index'].iloc[0]
df.loc[df.index[0], 'position'] = shares * df['close_index'].iloc[0]
elif df['signal'].iloc[0] == 0:
# 持有现金
df.loc[df.index[0], 'position'] = initial_capital
else:
# 卖空
shares = initial_capital / df['close_index'].iloc[0]
df.loc[df.index[0], 'position'] = initial_capital + shares * df['close_index'].iloc[0] - shares * df['close_index'].iloc[0]
# 遍历后续行
for i in range(1, len(df)):
prev_situation = df['situation'].iloc[i-1]
curr_situation = df['situation'].iloc[i]
curr_signal = df['signal'].iloc[i]
if curr_situation == 1:
if prev_situation == 0:
# 从空仓转为多头
prev_value = df['position'].iloc[i-1]
shares = prev_value / df['close_index'].iloc[i]
df.loc[df.index[i], 'position'] = shares * df['close_index'].iloc[i]
else:
# 继续持有多头
shares = df['position'].iloc[i-1] / df['close_index'].iloc[i-1]
df.loc[df.index[i], 'position'] = shares * df['close_index'].iloc[i]
elif curr_situation == 0:
if prev_situation == 1:
# 从多头转为空仓
shares = df['position'].iloc[i-1] / df['close_index'].iloc[i-1]
df.loc[df.index[i], 'position'] = shares * df['close_index'].iloc[i]
elif prev_situation == -1:
# 从空头转为空仓
for j in range(0, i, -1):
if df['situation'].iloc[j] != -1:
short_close_index = df['close_index'].iloc[j+1]
shares = df['position'].iloc[j]/short_close_index
continue
cash = df['position'].iloc[i-1] + shares*df['close_index'].iloc[i-1]
df.loc[df.index[i], 'position'] = cash - shares * df['close_index'].iloc[i]
else:
# 继续空仓
df.loc[df.index[i], 'position'] = df['position'].iloc[i-1]
else: # curr_situation == -1
if prev_situation == 0:
# 从空仓转为空头
cash = df['position'].iloc[i-1]
shares = cash / df['close_index'].iloc[i]
df.loc[df.index[i], 'position'] = cash + shares * df['close_index'].iloc[i] - shares * df['close_index'].iloc[i]
else:
# 继续持有空头
for j in range(0, i, -1):
if df['situation'].iloc[j] != -1:
short_close_index = df['close_index'].iloc[j+1]
shares = df['position'].iloc[j]/short_close_index
continue
cash = df['position'].iloc[i-1] + shares*df['close_index'].iloc[i-1]
df.loc[df.index[i], 'position'] = cash - shares * df['close_index'].iloc[i]
return df
# 定义空头策略持仓
def calculate_position_short(df, initial_capital):
situation = []
for i in range(len(df['signal'])):
if i == 0:
if df.iloc[i]['signal'] == -1:
sit = -1
else:
sit = 0
else:
if situation[i-1] + df.iloc[i]['signal'] >= 1:
sit = 0
elif situation[i-1] + df.iloc[i]['signal'] < -1:
sit = -1
else:
sit = situation[i-1] + df.iloc[i]['signal']
situation.append(sit)
df['situation'] = situation
df['position'] = initial_capital
# 处理第一行
if df['signal'].iloc[0] == 0:
# 持有现金
df.loc[df.index[0], 'position'] = initial_capital
else:
# 卖空
shares = initial_capital / df['close_index'].iloc[0]
df.loc[df.index[0], 'position'] = initial_capital + shares * df['close_index'].iloc[0] - shares * df['close_index'].iloc[0]
# 遍历后续行
for i in range(1, len(df)):
prev_situation = df['situation'].iloc[i-1]
curr_situation = df['situation'].iloc[i]
curr_signal = df['signal'].iloc[i]
if curr_situation == 0:
if prev_situation == -1:
# 从空头转为空仓
for j in range(0, i, -1):
if df['situation'].iloc[j] != -1:
short_close_index = df['close_index'].iloc[j+1]
shares = df['position'].iloc[j]/short_close_index
continue
cash = df['position'].iloc[i-1] + shares*df['close_index'].iloc[i-1]
df.loc[df.index[i], 'position'] = cash - shares * df['close_index'].iloc[i]
else:
# 继续空仓
df.loc[df.index[i], 'position'] = df['position'].iloc[i-1]
else: # curr_situation == -1
if prev_situation == 0:
# 从空仓转为空头
cash = df['position'].iloc[i-1]
shares = cash / df['close_index'].iloc[i]
df.loc[df.index[i], 'position'] = cash + shares * df['close_index'].iloc[i] - shares * df['close_index'].iloc[i]
else:
# 继续持有空头
for j in range(0, i, -1):
if df['situation'].iloc[j] != -1:
short_close_index = df['close_index'].iloc[j+1]
shares = df['position'].iloc[j]/short_close_index
continue
cash = df['position'].iloc[i-1] + shares*df['close_index'].iloc[i-1]
df.loc[df.index[i], 'position'] = cash - shares * df['close_index'].iloc[i]
return df
# 绘制ERP信号回测净值
# 计算加权ERP
zz800_ERP = zz800.join(ERP_df, how='inner')
zz800_ERP['ERP_mean'] = zz800_ERP['percentile'].rolling(window=60, min_periods=1).mean()
zz800_ERP = zz800_ERP[(zz800_ERP.index >= '2018-10-01') & (zz800_ERP.index <= '2024-12-31')]
# 生成买入卖出信号
threshold_high = 0.7 # 买入阈值
threshold_low = 0.3 # 卖出阈值
zz800_ERP_month = zz800_ERP.groupby(zz800_ERP.index.to_period('M')).nth(0)
zz800_ERP_month['signal'] = 0 # 初始化信号
zz800_ERP_month['signal'] = np.where(zz800_ERP_month['ERP_mean'].shift(1) > 0.7, 1,
np.where(zz800_ERP_month['ERP_mean'].shift(1) < 0.3, -1, 0))
# 计算每月持仓
zz800_ERP_month = zz800_ERP_month[(zz800_ERP_month.index >= '2019-01-01') & (zz800_ERP_month.index <= '2024-12-31')]
zz800_ERP_month = calculate_position_long(zz800_ERP_month, 10000)
# 计算净值
zz800_month = zz800.groupby(zz800.index.to_period('M')).nth(0)
zz800_month = zz800_month[(zz800_month.index >= '2019-01-01') & (zz800_month.index <= '2024-12-31')]
zz800_month['net_value'] = zz800_month['close_index']/zz800_month['close_index'].iloc[0]
zz800_ERP_month['net_value'] = zz800_ERP_month['position']/10000
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(zz800_ERP_month.index, zz800_month['net_value'], label='指数净值', linestyle = '-', color = (0, 0, 1))
ax1.plot(zz800_ERP_month.index, zz800_ERP_month['net_value'], label='加权ERP净值', linestyle = '-', color = (0, 0.5, 0.5))
# 创建次轴(右轴)用于面积图
ax2 = ax1.twinx()
ax2.fill_between(ERP_df.index, ERP_df['percentile']*100, color = 'goldenrod', alpha=0.3, label='ERP五年分位数(%,右)')
# 设置 x 轴刻度
ax1.set_xticks(date[::6]) # 隔半年显示一次
ax1.set_xticklabels(date[::6], rotation=45)
# 添加标题和标签
ax1.set_title('ERP信号回测净值')
# 获取所有图例句柄和标签
lines1, labels1 = ax1.get_legend_handles_labels() # 获取折线图的图例
lines2, labels2 = ax2.get_legend_handles_labels() # 获取面积图的图例
# 合并所有图例
lines = lines1 + lines2
labels = labels1 + labels2
# 创建图例并设置位置在中心
fig.legend(lines, labels, loc = 'upper center', bbox_to_anchor = (0.5, 0.89), ncol = 3)
# 展示图形
plt.show()
# 绘制量价信号结合表现
# 1、计算收盘价M日均线
M = 5
zz800['MA5'] = zz800['close_index'].rolling(window=M).mean()
# 2、计算成交量N日均值
N = 5
zz800['volume5'] = zz800['volume'].rolling(window=N).mean()
# 3、计算持仓
# (1)原始——zz800_month
# (2)仅根据成交量调仓
# 买卖信号生成
zz800v_month = zz800.groupby(zz800.index.to_period('M')).nth(0)
zz800v_month = zz800v_month[(zz800v_month.index >= '2019-01-01') & (zz800v_month.index <= '2024-12-31')]
zz800v_month['signal'] = 0 # 初始化信号为0
# 买入条件:成交量突破N日均值
buy_condition = zz800v_month['volume'] > zz800v_month['volume5']
zz800v_month.loc[buy_condition, 'signal'] = 1
# 卖出条件:成交量小于N日均值
sell_condition = zz800v_month['volume'] < zz800v_month['volume5']
zz800v_month.loc[sell_condition, 'signal'] = -1
# 计算持仓
zz800v_month = calculate_position_long(zz800v_month, 10000)
# print(zz800v_month)
# (3)仅根据均线调仓
# 买卖信号生成
zz800p_month = zz800.groupby(zz800.index.to_period('M')).nth(0)
zz800p_month = zz800p_month[(zz800p_month.index >= '2019-01-01') & (zz800p_month.index <= '2024-12-31')]
zz800p_month['signal'] = 0 # 初始化信号为0
# 买入条件:价格突破MA(5)
buy_condition = zz800p_month['close_index'] > zz800p_month['MA5']
zz800p_month.loc[buy_condition, 'signal'] = 1
# 卖出条件:价格小于MA(5)
sell_condition = zz800p_month['close_index'] < zz800p_month['MA5']
zz800p_month.loc[sell_condition, 'signal'] = -1
# 计算持仓
zz800p_month = calculate_position_long(zz800p_month, 10000)
# print(zz800p_month)
# (4)量价信号——多头策略
# 买卖信号生成
zz800vp_month_long = zz800.groupby(zz800.index.to_period('M')).nth(0)
zz800vp_month_long = zz800vp_month_long[(zz800vp_month_long.index >= '2019-01-01') & (zz800vp_month_long.index <= '2024-12-31')]
zz800vp_month_long['signal'] = 0 # 初始化信号为0
# 买入条件:成交量突破N日均值且收盘价在M日均线上
buy_condition = (zz800vp_month_long['volume'] > zz800vp_month_long['volume5']) & (zz800vp_month_long['close_index'] > zz800vp_month_long['MA5'])
zz800vp_month_long.loc[buy_condition, 'signal'] = 1
# 卖出条件:收盘价下穿M日均线
sell_condition = (zz800vp_month_long['close_index'] < zz800vp_month_long['MA5']) & (zz800vp_month_long['close_index'].shift(1) > zz800vp_month_long['MA5'].shift(1))
zz800vp_month_long.loc[sell_condition, 'signal'] = -1
# 计算持仓
zz800vp_month_long = calculate_position_long(zz800vp_month_long, 10000)
# print(zz800vp_month_long)
# (5)量价信号——多空策略
zz800vp_month_ls = zz800.groupby(zz800.index.to_period('M')).nth(0)
zz800vp_month_ls = zz800vp_month_ls[(zz800vp_month_ls.index >= '2019-01-01') & (zz800vp_month_ls.index <= '2024-12-31')]
zz800vp_month_ls['signal'] = 0 # 初始化信号为0
# 买入条件:成交量突破N日均值且收盘价在M日均线上
buy_condition = (zz800vp_month_ls['volume'] > zz800vp_month_ls['volume5']) & (zz800vp_month_ls['close_index'] > zz800vp_month_ls['MA5'])
zz800vp_month_ls.loc[buy_condition, 'signal'] = 1
# 卖出条件:收盘价下穿M日均线
sell_condition = (zz800vp_month_ls['close_index'] < zz800vp_month_ls['MA5']) & (zz800vp_month_ls['close_index'].shift(1) > zz800vp_month_ls['MA5'].shift(1))
zz800vp_month_ls.loc[sell_condition, 'signal'] = -1
# 计算持仓
zz800vp_month_ls = calculate_position_ls(zz800vp_month_ls, 10000)
# print(zz800vp_month_ls)
# 计算胜率
def calculate_win_rate(df):
# 创建收益率列
df['monthly_return'] = np.log(df['net_value'] / df['net_value'].shift(1))
# 找出交易点(situation发生变化的点)
trade_points = df['situation'].diff() != 0
# 获取交易点的索引
trade_dates = df.index[trade_points]
if len(trade_dates) < 2:
print("交易次数不足,无法计算胜率")
return
# 计算每次交易的收益
trades = []
for i in range(len(trade_dates)-1):
start_date = trade_dates[i]
end_date = trade_dates[i+1]
# 计算这次交易的收益率
start_position = df.loc[start_date, 'position']
end_position = df.loc[end_date, 'position']
trade_return = (end_position - start_position) / start_position
trades.append(trade_return)
# 计算胜率
winning_trades = sum(1 for x in trades if x > 0)
total_trades = len(trades)
win_rate = winning_trades / total_trades if total_trades > 0 else 0
# print(f"总交易次数: {total_trades}")
# print(f"盈利次数: {winning_trades}")
# print(f"胜率: {win_rate:.2%}")
return win_rate
# 回测结果汇总表生成
def backet_result(df):
result = []
# 调仓频率
result.append('M')
# 累计净值
df['net_value'] = df['position']/10000
result.append(df.iloc[-1]['net_value'])
# 合计周期
result.append(len(df))
# 信号次数
sum_signal = 0
for sig in df['situation']:
if sig != 0:
sum_signal = sum_signal + 1
result.append(sum_signal)
# 多头
long_signal = 0
for sit_l in df['situation']:
if sit_l == 1:
long_signal = long_signal + 1
result.append(long_signal)
# 空头
short_signal = 0
for sit_s in df['situation']:
if sit_s == -1:
short_signal = short_signal + 1
result.append(short_signal)
# 胜率
result.append(calculate_win_rate(df))
# 盈亏比
df['monthly_return'] = np.log(df['net_value'] / df['net_value'].shift(1))
positive_returns = df['monthly_return'][df['monthly_return'] > 0]
negative_returns = df['monthly_return'][df['monthly_return'] < 0]
avg_gain = positive_returns.mean()
avg_loss = abs(negative_returns.mean())
profit_loss_ratio = avg_gain / avg_loss if avg_loss != 0 else float('inf')
result.append(profit_loss_ratio)
# 期望
expected_return = df['monthly_return'].mean()*12
result.append(expected_return)
# 年化收益
years = (df.index[-1] - df.index[0]).days / 365.25
annual_return = []
total_return = (df['net_value'].iloc[-1] - df['net_value'].iloc[0]) / df['net_value'].iloc[0]
annual_return = (1 + total_return) ** (1 / years) - 1
result.append(annual_return)
# 波动
monthly_volatility = df['monthly_return'].std()
trading_months = 12
annual_volatility = monthly_volatility * np.sqrt(trading_months)
result.append(annual_volatility)
# 夏普
sharp = (annual_return-0.02) / annual_volatility
result.append(sharp)
# 最大回撤
df['CumMax'] = df['position'].cummax()
df['Drawdown'] = (df['position'] - df['CumMax']) / df['CumMax']
result.append(abs(df['Drawdown'].min()))
return result
# 计算原始情况的胜率
zz800_month['position'] = zz800_month['close_index']
zz800_month['monthly_return'] = zz800_month['position'].pct_change()
zz800_positive_returns = len(zz800_month[zz800_month['monthly_return'] > 0])
zz800_negative_returns = len(zz800_month[zz800_month['monthly_return'] < 0])
zz800_win_rate = zz800_positive_returns / (zz800_positive_returns + zz800_negative_returns)
# 计算原始情况的年化收益率
years = (zz800_month.index[-1] - zz800_month.index[0]).days / 365.25
zz800_annual_return = []
zz800_total_return = (zz800_month['net_value'].iloc[-1] - zz800_month['net_value'].iloc[0]) / zz800_month['net_value'].iloc[0]
zz800_annual_return = (1 + zz800_total_return) ** (1 / years) - 1
# 计算原始情况的波动率
zz800_monthly_volatility = zz800_month['monthly_return'].std()
trading_months = 12
zz800_annual_volatility = zz800_monthly_volatility * np.sqrt(trading_months)
zz800_result = ['M', zz800_month.iloc[-1]['net_value'], len(zz800_month), len(zz800_month), len(zz800_month), 0, zz800_win_rate, zz800_positive_returns/zz800_negative_returns, zz800_month['monthly_return'].mean()*12, zz800_annual_return, zz800_annual_volatility, (zz800_annual_return-0.02)/zz800_annual_volatility, MDD(zz800_month)]
index = ['调仓频率', '累计净值', '合计周期', '信号次数', '多头', '空头', '胜率', '盈亏比', '期望', '年化收益', '波动', '夏普', '最大回撤']
vp_performance_data = {
'原始': zz800_result,
'成交量': backet_result(zz800v_month),
'均线': backet_result(zz800p_month),
'量价信号_多头': backet_result(zz800vp_month_long),
'量价信号——多空': backet_result(zz800vp_month_ls)
}
vp_performance = pd.DataFrame(vp_performance_data)
# 定义双层表头
header_tuples = [
(' ', '原始'),
('成交量', '5'),
('均线', '5'),
('量价信号', '多头'),
('量价信号', '多空')
]
# 设置 MultiIndex 作为列索引
vp_performance.columns = pd.MultiIndex.from_tuples(header_tuples)
pd.set_option('display.multi_sparse', False)
vp_performance['参数'] = index
vp_performance.set_index('参数', inplace = True)
# 调整数据显示格式
def performance_format(df):
df.loc['累计净值'] = df.loc['累计净值'].apply(lambda x: '{:.2f}'.format(x))
df.loc['胜率'] = df.loc['胜率'].apply(lambda x: '{:.0%}'.format(x))
df.loc['盈亏比'] = df.loc['盈亏比'].apply(lambda x: '{:.2f}'.format(x))
df.loc['期望'] = df.loc['期望'].apply(lambda x: '{:.2f}'.format(x))
df.loc['年化收益'] = df.loc['年化收益'].apply(lambda x: '{:.0%}'.format(x))
df.loc['波动'] = df.loc['波动'].apply(lambda x: '{:.0%}'.format(x))
df.loc['夏普'] = df.loc['夏普'].apply(lambda x: '{:.2f}'.format(x))
df.loc['最大回撤'] = df.loc['最大回撤'].apply(lambda x: '{:.0%}'.format(x))
return df
vp_performance = performance_format(vp_performance)
print("量价信号结合表现")
vp_performance
# 计算净值和回撤
ERP_vp_month_data = {
'date': zz800_ERP_month.index,
'close_index': zz800_ERP_month['close_index'],
'signal_ERP': zz800_ERP_month['signal'],
'signal_vp': zz800vp_month_long['signal']
}
ERP_vp_month = pd.DataFrame(ERP_vp_month_data)
conditions = [
(ERP_vp_month['signal_ERP'] == 1) & (ERP_vp_month['signal_vp'] == 1),
(ERP_vp_month['signal_ERP'] == -1) & (ERP_vp_month['signal_vp'] == -1)
]
choices = [1, -1]
ERP_vp_month['signal'] = np.select(conditions, choices, default=0)
# 计算合成指标净值
ERP_vp_month_long = ERP_vp_month.copy()
ERP_vp_month_long = calculate_position_long(ERP_vp_month_long, 10000)
ERP_vp_month_long['net_value'] = ERP_vp_month_long['position']/10000
# 计算合成指标回撤
def drawdown(df):
df['CumMax'] = df['position'].cummax()
df['Drawdown'] = (df['position'] - df['CumMax']) / df['CumMax']
return df
ERP_vp_month_long = drawdown(ERP_vp_month_long)
# 绘制合成指标净值
plt.figure(figsize=(10, 5))
plt.rcParams['font.family'] = ['SimHei']
# 对于每一个DataFrame,绘制相应的close值
plt.plot(ERP_vp_month_long.index, ERP_vp_month_long['net_value'], label='信号结合', linestyle = '-', color = (1, 0, 0))
plt.plot(ERP_vp_month_long.index, zz800_ERP_month['net_value'], label='加权ERP信号', linestyle = '-', color = (0, 0.6, 1))
plt.plot(ERP_vp_month_long.index, zz800vp_month_long['net_value'], label='量价信号', color = (0, 0, 1))
# 添加标题和标签
plt.title('合成指标净值')
plt.xlabel('日期')
plt.ylabel('净值')
plt.legend() # 显示图例
# 展示图形
plt.show()
# 绘制合成指标回测
plt.figure(figsize=(10, 5))
plt.rcParams['font.family'] = ['SimHei']
# 对于每一个DataFrame,绘制相应的close值
plt.plot(ERP_vp_month_long.index, ERP_vp_month_long['Drawdown'], label='合成信号', linestyle = '-', color = (1, 0, 0))
plt.plot(zz800_month.index, zz800_month['Drawdown'], label='原始信号', linestyle = '-', color = (0, 0, 1))
# 添加标题和标签
plt.title('合成指标回撤')
plt.xlabel('日期')
plt.ylabel('回撤')
plt.legend() # 显示图例
# 展示图形
plt.show()
# 绘制ERP+量价指标信号
ERP_vp_month_ls = ERP_vp_month.copy()
ERP_vp_month_ls = calculate_position_ls(ERP_vp_month_ls, 10000)
ERP_vp_performance_data = {
'原始': zz800_result,
'策略多头': backet_result(ERP_vp_month_long),
'策略多空': backet_result(ERP_vp_month_ls)
}
ERP_vp_performance = pd.DataFrame(ERP_vp_performance_data)
# 定义双层表头
header_tuples = [
(' ', '原始'),
('信号结合表现', '策略多头'),
('信号结合表现', '策略多空')
]
# 设置 MultiIndex 作为列索引
ERP_vp_performance.columns = pd.MultiIndex.from_tuples(header_tuples)
pd.set_option('display.multi_sparse', True)
ERP_vp_performance['参数'] = index
ERP_vp_performance.set_index('参数', inplace = True)
# 调整数据显示格式
ERP_vp_performance = performance_format(ERP_vp_performance)
print("ERP+量价指标信号")
ERP_vp_performance
# 1、生成买卖信号
US_bond10 = pd.read_excel(r"D:\学习\python学习\课程\期末数据\美国_国债实际收益率_10年.xlsx")
US_bond10.columns = ['date', 'return']
US_bond10 = US_bond10.dropna()
# 计算各均线
US_bond10['MA15'] = US_bond10['return'].rolling(window=15).mean()
US_bond10['MA20'] = US_bond10['return'].rolling(window=20).mean()
US_bond10['MA25'] = US_bond10['return'].rolling(window=25).mean()
# 合并黄金和美债
# 统一时间区间
gold['date'] = pd.to_datetime(gold['date'])
min_date = max(US_bond10['date'].min(), gold['date'].min())
max_date = min(US_bond10['date'].max(), gold['date'].max())
full_range = pd.date_range(start=min_date, end=max_date, freq='D')
US_bond10 = US_bond10.set_index('date').reindex(full_range).rename_axis('date').reset_index()
gold = gold.set_index('date').reindex(full_range).rename_axis('date').reset_index()
US_bond10 = US_bond10.dropna()
gold = gold.dropna(subset = ['close_price'])
gold_US = pd.merge(gold, US_bond10, how = 'inner')
gold_US.set_index('date', inplace = True)
gold_US = gold_US.drop(columns = ["Goldtype", "open_price", "high_price", "low_price", "gap_price", "gap_rate", "volumn", "amount", "average_price", "holding", "close_value", "sin_value", "log_return", "CumMax", "Drawdown"])
gold_US.columns = ['close_index', 'return', 'MA15', 'MA20', 'MA25']
gold_US_month = gold_US.groupby(gold_US.index.to_period('M')).nth(0)
gold_US_month = gold_US_month[(gold_US_month.index >= '2019-01-01') & (gold_US_month.index <= '2024-12-31')]
# MA(20):
gold_US_20l = gold_US_month.copy()
gold_US_20l['signal'] = 0
# 买入信号:十年期美债实际收益率<MA(20)
gold_US_20l.loc[gold_US_20l['return'] > gold_US_20l['MA20'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率>MA(20)
gold_US_20l.loc[gold_US_20l['return'] < gold_US_20l['MA20'], 'signal'] = -1
# 计算持仓
gold_US_20l = calculate_position_long(gold_US_20l, 10000)
gold_US_20l['net_value'] = gold_US_20l['position']/10000
gold_US_20l
# 绘制美债实际收益率均线信号择时效果
gold_US_20l['net_value_gold'] = gold_US_20l['close_index']/gold_US_20l['close_index'].iloc[0]
gold_US_20l['gap'] = gold_US_20l['net_value'] - gold_US_20l['net_value_gold']
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(gold_US_20l.index, gold_US_20l['net_value'], label='美债均线择时净值', linestyle = '-', color = (0, 0, 1))
ax1.plot(gold_US_20l.index, gold_US_20l['net_value_gold'], label='原始净值', linestyle = '-', color = (0, 0.5, 1))
# 创建次轴(右轴)用于面积图
ax2 = ax1.twinx()
ax2.fill_between(gold_US_20l.index, gold_US_20l['gap'], color = 'goldenrod', alpha=0.3, label='超额(右)')
# 设置 x 轴刻度
ax1.set_xticks(date[::6]) # 隔半年显示一次
ax1.set_xticklabels(date[::6], rotation=45)
# 添加标题和标签
ax1.set_title('美债实际收益率均线信号择时效果')
# 获取所有图例句柄和标签
lines1, labels1 = ax1.get_legend_handles_labels() # 获取折线图的图例
lines2, labels2 = ax2.get_legend_handles_labels() # 获取面积图的图例
# 合并所有图例
lines = lines1 + lines2
labels = labels1 + labels2
# 创建图例并设置位置在中心
fig.legend(lines, labels, loc = 'upper center', bbox_to_anchor = (0.5, 0.89), ncol = 3)
# 展示图形
plt.show()
# 计算各策略情况
# 1、纯多头——原始——gold_US_result
# 计算原始情况的胜率
gold_US_month['position'] = gold_US_month['close_index']
gold_US_month['net_value'] = gold_US_month['position']/gold_US_month['position'].iloc[0]
gold_US_month['monthly_return'] = gold_US_month['position'].pct_change()
gold_US_positive_returns = len(gold_US_month[gold_US_month['monthly_return'] > 0])
gold_US_negative_returns = len(gold_US_month[gold_US_month['monthly_return'] < 0])
gold_US_win_rate = gold_US_positive_returns / (gold_US_positive_returns + gold_US_negative_returns)
# 计算原始情况的年化收益率
years = (gold_US_month.index[-1] - gold_US_month.index[0]).days / 365.25
gold_US_annual_return = []
gold_US_total_return = (gold_US_month['net_value'].iloc[-1] - gold_US_month['net_value'].iloc[0]) / gold_US_month['net_value'].iloc[0]
gold_US_annual_return = (1 + gold_US_total_return) ** (1 / years) - 1
# 计算原始情况的波动率
gold_US_monthly_volatility = gold_US_month['monthly_return'].std()
trading_months = 12
gold_US_annual_volatility = gold_US_monthly_volatility * np.sqrt(trading_months)
gold_US_result = ['M',
gold_US_month.iloc[-1]['net_value'],
len(gold_US_month), len(gold_US_month),
len(gold_US_month),
0,
gold_US_win_rate,
gold_US_positive_returns/gold_US_negative_returns,
gold_US_month['monthly_return'].mean()*12,
gold_US_annual_return, gold_US_annual_volatility,
(gold_US_annual_return-0.02)/gold_US_annual_volatility,
MDD(gold_US_month)]
# 2、纯多头——15——gold_US_15l
# MA(15):
gold_US_15l = gold_US_month.copy()
gold_US_15l['signal'] = 0
# 买入信号:十年期美债实际收益率<MA(15)
gold_US_15l.loc[gold_US_15l['return'] > gold_US_15l['MA15'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率>MA(15)
gold_US_15l.loc[gold_US_15l['return'] < gold_US_15l['MA15'], 'signal'] = -1
# 计算持仓
gold_US_15l = calculate_position_long(gold_US_15l, 10000)
gold_US_15l['net_value'] = gold_US_15l['position']/10000
# 3、纯多头——20——gold_US_20l
# 4、纯多头——25——gold_US_25l
# MA(25):
gold_US_25l = gold_US_month.copy()
gold_US_25l['signal'] = 0
# 买入信号:十年期美债实际收益率<MA(25)
gold_US_25l.loc[gold_US_25l['return'] > gold_US_25l['MA25'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率>MA(25)
gold_US_25l.loc[gold_US_25l['return'] < gold_US_25l['MA25'], 'signal'] = -1
# 计算持仓
gold_US_25l = calculate_position_long(gold_US_25l, 10000)
gold_US_25l['net_value'] = gold_US_25l['position']/10000
# 5、纯空头——原始——gold_US_short_result
# 计算原始情况的胜率
gold_US_month_short = gold_US_month.copy()
gold_US_month_short['position'] = 20000 - (10000/gold_US_month_short['close_index'].iloc[0])*gold_US_month_short['close_index']
gold_US_month_short['net_value'] = gold_US_month_short['position']/10000
gold_US_month_short['monthly_return'] = gold_US_month_short['position'].pct_change()
#print(gold_US_month_short['monthly_return'])
gold_US_short_positive_returns = len(gold_US_month_short[gold_US_month_short['monthly_return'] > 0])
gold_US_short_negative_returns = len(gold_US_month_short[gold_US_month_short['monthly_return'] < 0])
gold_US_short_win_rate = gold_US_short_positive_returns / (gold_US_short_positive_returns + gold_US_short_negative_returns)
# 计算原始情况的年化收益率
years_short = (gold_US_month_short.index[-1] - gold_US_month_short.index[0]).days / 365.25
# print(years_short)
gold_US_short_total_return = (gold_US_month_short['net_value'].iloc[-1] - gold_US_month_short['net_value'].iloc[0]) / gold_US_month_short['net_value'].iloc[0]
# print(gold_US_short_total_return)
gold_US_short_annual_return = (1 + gold_US_short_total_return) ** (1 / years_short) - 1
# print(gold_US_short_annual_return)
# 计算原始情况的波动率
gold_US_short_monthly_volatility = gold_US_month_short['monthly_return'].std()
gold_US_short_annual_volatility = gold_US_short_monthly_volatility * np.sqrt(trading_months)
# 计算夏普比率
sharp = (gold_US_short_annual_return-0.02)/gold_US_short_annual_volatility
gold_US_short_result = ['M',
gold_US_month_short.iloc[-1]['net_value'],
len(gold_US_month_short), len(gold_US_month_short),
0,
len(gold_US_month_short),
gold_US_short_win_rate,
gold_US_short_positive_returns/gold_US_short_negative_returns,
gold_US_month_short['monthly_return'].mean()*12,
gold_US_short_annual_return,
gold_US_short_annual_volatility,
sharp,
MDD(gold_US_month_short)]
# 6、纯空头——15——gold_US_15s
# MA(15):
gold_US_15s = gold_US_month.copy()
gold_US_15s['signal'] = 0
# 买入信号:十年期美债实际收益率<MA(15)
gold_US_15s.loc[gold_US_15s['return'] > gold_US_15s['MA15'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率>MA(15)
gold_US_15s.loc[gold_US_15s['return'] < gold_US_15s['MA15'], 'signal'] = -1
# 计算持仓
gold_US_15s = calculate_position_short(gold_US_15s, 10000)
gold_US_15s['net_value'] = gold_US_15s['position']/10000
# 7、纯空头——20——gold_US_20s
# MA(20):
gold_US_20s = gold_US_month.copy()
gold_US_20s['signal'] = 0
# 买入信号:十年期美债实际收益率>MA(20)
gold_US_20s.loc[gold_US_20s['return'] > gold_US_20s['MA20'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率<MA(15)
gold_US_20s.loc[gold_US_20s['return'] < gold_US_20s['MA20'], 'signal'] = -1
# 计算持仓
gold_US_20s = calculate_position_short(gold_US_20s, 10000)
gold_US_20s['net_value'] = gold_US_20s['position']/10000
# 8、纯空头——25——gold_US_25s
# MA(25):
gold_US_25s = gold_US_month.copy()
gold_US_25s['signal'] = 0
# 买入信号:十年期美债实际收益率>MA(20)
gold_US_25s.loc[gold_US_25s['return'] > gold_US_25s['MA25'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率<MA(20)
gold_US_25s.loc[gold_US_25s['return'] < gold_US_25s['MA25'], 'signal'] = -1
# 计算持仓
gold_US_25s = calculate_position_short(gold_US_25s, 10000)
gold_US_25s['net_value'] = gold_US_25s['position']/10000
# 9、多空——15——gold_US_15ls
# MA(15):
gold_US_15ls = gold_US_month.copy()
gold_US_15ls['signal'] = 0
# 买入信号:十年期美债实际收益率<MA(15)
gold_US_15ls.loc[gold_US_15ls['return'] > gold_US_15ls['MA15'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率>MA(15)
gold_US_15ls.loc[gold_US_15ls['return'] < gold_US_15ls['MA15'], 'signal'] = -1
# 计算持仓
gold_US_15ls = calculate_position_ls(gold_US_15ls, 10000)
gold_US_15ls['net_value'] = gold_US_15ls['position']/10000
# 10、多空——20——gold_US_20ls
# MA(20):
gold_US_20ls = gold_US_month.copy()
gold_US_20ls['signal'] = 0
# 买入信号:十年期美债实际收益率>MA(20)
gold_US_20ls.loc[gold_US_20ls['return'] > gold_US_20ls['MA20'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率<MA(15)
gold_US_20ls.loc[gold_US_20ls['return'] < gold_US_20ls['MA20'], 'signal'] = -1
# 计算持仓
gold_US_20ls = calculate_position_ls(gold_US_20ls, 10000)
gold_US_20ls['net_value'] = gold_US_20ls['position']/10000
# 11、多空——25——gold_US_25ls
# MA(25):
gold_US_25ls = gold_US_month.copy()
gold_US_25ls['signal'] = 0
# 买入信号:十年期美债实际收益率>MA(20)
gold_US_25ls.loc[gold_US_25ls['return'] > gold_US_25ls['MA25'], 'signal'] = 1
# 卖出信号:十年期美债实际收益率<MA(20)
gold_US_25ls.loc[gold_US_25ls['return'] < gold_US_25ls['MA25'], 'signal'] = -1
# 计算持仓
gold_US_25ls = calculate_position_ls(gold_US_25ls, 10000)
gold_US_25ls['net_value'] = gold_US_25ls['position']/10000
# 绘制美债均线指标不同参数多空效果
gold_US_performance_data = {
'纯多头——原始': gold_US_result,
'纯多头——15': backet_result(gold_US_15l),
'纯多头——20': backet_result(gold_US_20l),
'纯多头——25': backet_result(gold_US_25l),
'纯空头——原始': gold_US_short_result,
'纯空头——15': backet_result(gold_US_15s),
'纯空头——20': backet_result(gold_US_20s),
'纯空头——25': backet_result(gold_US_25s),
'多空——15': backet_result(gold_US_15ls),
'多空——20': backet_result(gold_US_20ls),
'多空——25': backet_result(gold_US_25ls)
}
gold_US_performance = pd.DataFrame(gold_US_performance_data)
# 定义双层表头
header_tuples = [
('纯多头', '原始'),
('纯多头', '15'),
('纯多头', '20'),
('纯多头', '25'),
('纯空头', '原始'),
('纯空头', '15'),
('纯空头', '20'),
('纯空头', '25'),
('多空', '15'),
('多空', '20'),
('多空', '25')
]
# 设置 MultiIndex 作为列索引
gold_US_performance.columns = pd.MultiIndex.from_tuples(header_tuples)
pd.set_option('display.multi_sparse', True)
gold_US_performance['参数'] = index
gold_US_performance.set_index('参数', inplace = True)
# 调整数据显示格式
gold_US_performance = performance_format(gold_US_performance)
print("美债均线指标不同参数多空效果")
gold_US_performance
# 绘制VIX指标择时净值表现
VIX = pd.read_excel(r"D:\学习\python学习\课程\期末数据\标准普尔500波动率指数(VIX).xlsx")
VIX.columns = ['date', 'VIX']
VIX = VIX.dropna()
#zz800_ERP['ERP_mean'] = zz800_ERP['percentile'].rolling(window=60, min_periods=1).mean()
# 滚动计算M日最大值
VIX['Max10'] = VIX['VIX'].rolling(window=10).max()
VIX['Max15'] = VIX['VIX'].rolling(window=15).max()
VIX['Max20'] = VIX['VIX'].rolling(window=20).max()
VIX['MA20'] = VIX['VIX'].rolling(window=20).mean()
# 合并黄金和VIX
# 统一时间区间
min_date = max(VIX['date'].min(), gold['date'].min())
max_date = min(VIX['date'].max(), gold['date'].max())
full_range = pd.date_range(start=min_date, end=max_date, freq='D')
VIX = VIX.set_index('date').reindex(full_range).rename_axis('date').reset_index()
gold = gold.set_index('date').reindex(full_range).rename_axis('date').reset_index()
VIX = VIX.dropna()
gold = gold.dropna(subset = ['close_price'])
gold_VIX = pd.merge(gold, VIX, how = 'inner')
gold_VIX.set_index('date', inplace = True)
gold_VIX = gold_VIX.drop(columns = ["Goldtype", "open_price", "high_price", "low_price", "gap_price", "gap_rate", "volumn", "amount", "average_price", "holding", "close_value", "sin_value", "log_return", "CumMax", "Drawdown"])
gold_VIX.columns = ['close_index', 'VIX', 'Max10', 'Max15', 'Max20', 'MA20']
gold_VIX_month = gold_VIX.groupby(gold_VIX.index.to_period('M')).nth(0)
gold_VIX_month = gold_VIX_month[(gold_VIX_month.index >= '2019-01-01') & (gold_VIX_month.index <= '2024-12-31')]
# MAX(15):
gold_VIX_15 = gold_VIX_month.copy()
gold_VIX_15['signal'] = 0
# 买入信号:VIX15天>20
gold_VIX_15.loc[gold_VIX_15['Max15'] > 20, 'signal'] = 1
gold_VIX_15.loc[gold_VIX_15['Max15'] < 20, 'signal'] = -1
# 计算持仓
gold_VIX_15 = calculate_position_long(gold_VIX_15, 10000)
gold_VIX_15['net_value'] = gold_VIX_15['position']/10000
gold_VIX_15
# 画图
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(gold_VIX_15.index, gold_VIX_15['net_value'], label='VIX指标择时净值', linestyle = '-', color = (0, 0, 1))
ax1.plot(gold_VIX_15.index, gold_US_20l['net_value_gold'], label='原始净值', linestyle = '-', color = 'goldenrod')
ax1.set_title("VIX指标择时净值表现")
ax1.legend()
# 展示图形
plt.show()
# 绘制VIX指标不同参数效果
# 1、原始——gold_US_result
# 2、10天最大——gold_VIX_10
# MAX(10):
gold_VIX_10 = gold_VIX_month.copy()
gold_VIX_10['signal'] = 0
# 买入信号:VIX10天>20
gold_VIX_10.loc[gold_VIX_10['Max10'] > 20, 'signal'] = 1
gold_VIX_10.loc[gold_VIX_10['Max10'] < 20, 'signal'] = -1
# 计算持仓
gold_VIX_10 = calculate_position_long(gold_VIX_10, 10000)
gold_VIX_10['net_value'] = gold_VIX_10['position']/10000
gold_VIX_10
# 3、15天最大——gold_VIX_15
# 4、20天最大——gold_VIX_20
# MAX(20):
gold_VIX_20 = gold_VIX_month.copy()
gold_VIX_20['signal'] = 0
# 买入信号:VIX20天>20
gold_VIX_20.loc[gold_VIX_20['Max20'] > 20, 'signal'] = 1
gold_VIX_20.loc[gold_VIX_20['Max20'] < 20, 'signal'] = -1
# 计算持仓
gold_VIX_20 = calculate_position_long(gold_VIX_20, 10000)
gold_VIX_20['net_value'] = gold_VIX_20['position']/10000
gold_VIX_20
# 4、20天均值——gold_VIX_20mean
# MAX(20):
gold_VIX_20mean = gold_VIX_month.copy()
gold_VIX_20mean['signal'] = 0
# 买入信号:VIX20天>20
gold_VIX_20mean.loc[gold_VIX_20mean['MA20'] > 20, 'signal'] = 1
gold_VIX_20mean.loc[gold_VIX_20mean['MA20'] < 20, 'signal'] = -1
# 计算持仓
gold_VIX_20mean = calculate_position_long(gold_VIX_20mean, 10000)
gold_VIX_20mean['net_value'] = gold_VIX_20mean['position']/10000
gold_VIX_20mean
# 画表
gold_VIX_performance_data = {
'原始': gold_US_result,
'10天最大': backet_result(gold_VIX_10),
'15天最大': backet_result(gold_VIX_15),
'20天最大': backet_result(gold_VIX_20),
'20天均值': backet_result(gold_VIX_20mean)
}
gold_VIX_performance = pd.DataFrame(gold_VIX_performance_data)
gold_VIX_performance['参数'] = index
gold_VIX_performance.set_index('参数', inplace = True)
# 调整数据显示格式
gold_VIX_performance = performance_format(gold_VIX_performance)
print("VIX指标不同参数效果")
gold_VIX_performance
# 绘制黄金价格250日动量指标净值曲线
gold_MT = gold.copy()
gold_MT = gold_MT.drop(columns = ["Goldtype", "open_price", "high_price", "low_price", "gap_price", "gap_rate", "volumn", "amount", "average_price", "holding", "close_value", "sin_value", "log_return", "CumMax", "Drawdown"])
gold_MT.set_index('date', inplace = True)
gold_MT.columns = ['close_index']
# 20日动量
gold_MT['MT20'] = gold_MT['close_index']/gold_MT['close_index'].shift(20)-1
# 60日动量
gold_MT['MT60'] = gold_MT['close_index']/gold_MT['close_index'].shift(60)-1
# 250日动量
gold_MT['MT250'] = gold_MT['close_index']/gold_MT['close_index'].shift(250)-1
gold_MT_month = gold_MT.groupby(gold_MT.index.to_period('M')).nth(0)
gold_MT_month = gold_MT_month[(gold_MT_month.index >= '2019-01-01') & (gold_MT_month.index <= '2024-12-31')]
# 250日动量买卖
gold_MT_250 = gold_MT_month.copy()
gold_MT_250['signal'] = 0
# 买入信号:VIX20天>20
gold_MT_250.loc[gold_MT_250['MT250'] > 0, 'signal'] = 1
gold_MT_250.loc[gold_MT_250['MT250'] < 0, 'signal'] = -1
# 计算持仓
gold_MT_250 = calculate_position_long(gold_MT_250, 10000)
gold_MT_250['net_value'] = gold_MT_250['position']/10000
gold_MT_250
# 画图
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(gold_MT_250.index, gold_MT_250['net_value'], label='黄金动量择时净值', linestyle = '-', color = (0, 0, 1))
ax1.plot(gold_MT_250.index, gold_US_20l['net_value_gold'], label='原始净值', linestyle = '-', color = 'goldenrod')
ax1.set_title("黄金价格250日动量指标净值曲线")
ax1.legend()
# 展示图形
plt.show()
# 绘制黄金价格动量不同参数回测情况
# 1、多头——原始——gold_US_result
# 2、多头——20——gold_MT_20
gold_MT_20 = gold_MT_month.copy()
gold_MT_20['signal'] = 0
# 买入信号:VIX20天>20
gold_MT_20.loc[gold_MT_20['MT20'] > 0, 'signal'] = 1
gold_MT_20.loc[gold_MT_20['MT20'] < 0, 'signal'] = -1
# 计算持仓
gold_MT_20 = calculate_position_long(gold_MT_20, 10000)
gold_MT_20['net_value'] = gold_MT_20['position']/10000
gold_MT_20
# 3、多头——60——gold_MT_60
gold_MT_60 = gold_MT_month.copy()
gold_MT_60['signal'] = 0
# 买入信号:VIX20天>20
gold_MT_60.loc[gold_MT_60['MT60'] > 0, 'signal'] = 1
gold_MT_60.loc[gold_MT_60['MT60'] < 0, 'signal'] = -1
# 计算持仓
gold_MT_60 = calculate_position_long(gold_MT_60, 10000)
gold_MT_60['net_value'] = gold_MT_60['position']/10000
gold_MT_60
# 4、多头——250——gold_MT_250
# 5、空头——原始——gold_US_month_short
# 6、空头——20——gold_MT_20s
gold_MT_20s = gold_MT_month.copy()
gold_MT_20s['signal'] = 0
# 买入信号:VIX20天>20
gold_MT_20s.loc[gold_MT_20s['MT20'] > 0, 'signal'] = 1
gold_MT_20s.loc[gold_MT_20s['MT20'] < 0, 'signal'] = -1
# 计算持仓
gold_MT_20s = calculate_position_short(gold_MT_20s, 10000)
gold_MT_20s['net_value'] = gold_MT_20s['position']/10000
gold_MT_20s
# 7、空头——60——gold_MT_60s
gold_MT_60s = gold_MT_month.copy()
gold_MT_60s['signal'] = 0
# 买入信号:VIX20天>20
gold_MT_60s.loc[gold_MT_60s['MT60'] > 0, 'signal'] = 1
gold_MT_60s.loc[gold_MT_60s['MT60'] < 0, 'signal'] = -1
# 计算持仓
gold_MT_60s = calculate_position_short(gold_MT_60s, 10000)
gold_MT_60s['net_value'] = gold_MT_60s['position']/10000
gold_MT_60s
# 8、空头——250——gold_MT_250s
gold_MT_250s = gold_MT_month.copy()
gold_MT_250s['signal'] = 0
# 买入信号:VIX20天>20
gold_MT_250s.loc[gold_MT_250s['MT250'] > 0, 'signal'] = 1
gold_MT_250s.loc[gold_MT_250s['MT250'] < 0, 'signal'] = -1
# 计算持仓
gold_MT_250s = calculate_position_short(gold_MT_250s, 10000)
gold_MT_250s['net_value'] = gold_MT_250s['position']/10000
gold_MT_250s
# 画表
gold_MT_performance_data = {
'多头——原始': gold_US_result,
'多头——20': backet_result(gold_MT_20),
'多头——60': backet_result(gold_MT_60),
'多头——250': backet_result(gold_MT_250),
'空头——原始': gold_US_short_result,
'空头——20': backet_result(gold_MT_20s),
'空头——60': backet_result(gold_MT_60s),
'空头——250': backet_result(gold_MT_250s),
}
gold_MT_performance = pd.DataFrame(gold_MT_performance_data)
# 定义双层表头
header_tuples = [
('多头', '原始'),
('多头', '20'),
('多头', '60'),
('多头', '250'),
('空头', '原始'),
('空头', '20'),
('空头', '60'),
('空头', '250')
]
# 设置 MultiIndex 作为列索引
gold_MT_performance.columns = pd.MultiIndex.from_tuples(header_tuples)
pd.set_option('display.multi_sparse', True)
gold_MT_performance['参数'] = index
gold_MT_performance.set_index('参数', inplace = True)
# 调整数据显示格式
gold_MT_performance = performance_format(gold_MT_performance)
print("黄金价格动量不同参数回测情况")
gold_MT_performance
# 绘制黄金合成指标择时净值
gold_US_VIX_MT_data = {
'date': gold_US_20l.index,
'close_index': gold_US_20l['close_index'].reindex(gold_US_20l.index),
'signal_US': gold_US_20l['signal'].reindex(gold_US_20l.index),
'signal_VIX': gold_VIX_15['signal'].reindex(gold_US_20l.index),
'signal_MT': gold_MT_250['signal'].reindex(gold_US_20l.index)
}
gold_US_VIX_MT = pd.DataFrame(gold_US_VIX_MT_data)
gold_US_VIX_MT.set_index('date', inplace = True)
gold_US_VIX_MT.dropna()
# 生成买卖信号
gold_US_VIX_MT['signal'] = 0
# 买入信号:三个指标的合计净信号方向>=2
gold_US_VIX_MT.loc[gold_US_VIX_MT['signal_US'] + gold_US_VIX_MT['signal_VIX'] + gold_US_VIX_MT['signal_MT'] >= 2, 'signal'] = 1
# 卖出信号:合计信号<0
gold_US_VIX_MT.loc[gold_US_VIX_MT['signal_US'] + gold_US_VIX_MT['signal_VIX'] + gold_US_VIX_MT['signal_MT'] < 0, 'signal'] = -1
# 计算持仓
gold_US_VIX_MT_long = gold_US_VIX_MT.copy()
gold_US_VIX_MT_long = calculate_position_long(gold_US_VIX_MT_long, 10000)
gold_US_VIX_MT_long['net_value'] = gold_US_VIX_MT_long['position']/10000
gold_US_VIX_MT_long['gap'] = gold_US_VIX_MT_long['net_value'] - gold_US_20l['net_value_gold']
# 画图
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(gold_US_VIX_MT_long.index, gold_US_VIX_MT_long['net_value'], label='策略净值(合成指标)', linestyle = '-', color = (0, 0, 1))
ax1.plot(gold_US_VIX_MT_long.index, gold_US_20l['net_value_gold'], label='原始净值', linestyle = '-', color = (0, 0.5, 1))
# 创建次轴(右轴)用于面积图
ax2 = ax1.twinx()
ax2.fill_between(gold_US_VIX_MT_long.index, gold_US_VIX_MT_long['gap'], color = 'goldenrod', alpha=0.3, label='超额(右)')
# 设置 x 轴刻度
ax1.set_xticks(date[::6]) # 隔半年显示一次
ax1.set_xticklabels(date[::6], rotation=45)
# 添加标题和标签
ax1.set_title('黄金合成指标择时净值')
# 获取所有图例句柄和标签
lines1, labels1 = ax1.get_legend_handles_labels() # 获取折线图的图例
lines2, labels2 = ax2.get_legend_handles_labels() # 获取面积图的图例
# 合并所有图例
lines = lines1 + lines2
labels = labels1 + labels2
# 创建图例并设置位置在中心
fig.legend(lines, labels, loc = 'upper center', bbox_to_anchor = (0.5, 0.89), ncol = 3)
# 展示图形
plt.show()
# 绘制黄金合成指标择时表现
# 1、原始——gold_US_result
# 2、策略多头——gold_US_VIX_MT_long
# 3、策略空头——gold_US_VIX_MT_short
# 计算持仓
gold_US_VIX_MT_short = gold_US_VIX_MT.copy()
gold_US_VIX_MT_short = calculate_position_short(gold_US_VIX_MT_short, 10000)
gold_US_VIX_MT_short['net_value'] = gold_US_VIX_MT_short['position']/10000
# 画表
gold_US_VIX_MT_performance_data = {
'原始': gold_US_result,
'策略多头': backet_result(gold_US_VIX_MT_long),
'策略空头': backet_result(gold_US_VIX_MT_short)
}
gold_US_VIX_MT_performance = pd.DataFrame(gold_US_VIX_MT_performance_data)
gold_US_VIX_MT_performance['参数'] = index
gold_US_VIX_MT_performance.set_index('参数', inplace = True)
# 调整数据显示格式
gold_US_VIX_MT_performance = performance_format(gold_US_VIX_MT_performance)
print("黄金合成指标择时表现")
gold_US_VIX_MT_performance
# 定义战略+战术业绩表现表的呈现
def perform_annual(df):
# 计算收益率
returns = df['net_value'].pct_change()
annual_return = (df['net_value'].iloc[-1] / df['net_value'].iloc[0]) - 1
# 计算波动率
volatility = returns.std() * np.sqrt(252)
# 计算夏普比率
risk_free_rate = 0.02
sharpe_ratio = (annual_return - risk_free_rate) / volatility
# 计算最大回撤率
cummax = df['net_value'].cummax()
drawdown = (df['net_value'] - cummax) / cummax
max_drawdown = abs(drawdown.min())
# 计算卡玛比率
calmar_ratio = annual_return / max_drawdown if max_drawdown != 0 else np.nan
return pd.Series({
'收益率': annual_return,
'波动率': volatility,
'夏普比率': sharpe_ratio,
'最大回撤率': max_drawdown,
'卡玛比率': calmar_ratio
})
# 合并并格式化显示
def perform_annual_format(df):
# 按年份分组计算指标
metrics_df = df.groupby('年份').apply(perform_annual).reset_index()
# 格式化显示
metrics_df['收益率'] = metrics_df['收益率'].map('{:.2%}'.format)
metrics_df['波动率'] = metrics_df['波动率'].map('{:.2%}'.format)
metrics_df['夏普比率'] = metrics_df['夏普比率'].map('{:.2f}'.format)
metrics_df['最大回撤率'] = metrics_df['最大回撤率'].map('{:.2%}'.format)
metrics_df['卡玛比率'] = metrics_df['卡玛比率'].map('{:.2f}'.format)
return metrics_df
# 定义风险预算+择时持仓计算
def risk_budget_timing(risk_weights_dict, timing_asset_df):
# 创建一个空的DataFrame来存储结果
result_df = pd.DataFrame()
# 设置日期索引
dates = timing_asset_df[0].index
result_df.index = dates
# 将权重字典转换为DataFrame以便于操作
weights_df = pd.DataFrame(risk_weights_dict)
weights_df['month'] = pd.to_datetime(weights_df['month'].astype(str), format='%Y-%m')
weights_df = weights_df.set_index('month')
# 为每个资产计算加权持仓
total_net_value = 0
for i, df in enumerate(timing_asset_df):
# 获取当前资产的列名
asset_name = f'asset_{i+1}_net_value'
# 计算加权持仓
weighted_net_value = []
for date in dates:
# 找到对应月份的权重
month = pd.to_datetime(date).replace(day=1)
weight = weights_df.loc[month, 'weights'][i]
net_value = df.loc[date, 'net_value'] * weight
weighted_net_value.append(net_value)
# 将加权持仓添加到结果DataFrame中
result_df[asset_name] = weighted_net_value
total_net_value += result_df[asset_name]
# 添加总持仓列
result_df['net_value'] = total_net_value
return result_df
# 将股票、债券、黄金三类资产的格式调整成(1)日期保留到月份(2)根据策略生成买卖信号,并计算持仓
# 1、股票
zz800_ERP_vp_month = ERP_vp_month_long[['close_index', 'signal']]
zz800_ERP_vp_month.index = zz800_ERP_vp_month.index.strftime('%Y-%m')
# 计算持仓
zz800_ERP_vp_month = calculate_position_long(zz800_ERP_vp_month, 10000)
# 计算净值
zz800_ERP_vp_month['net_value'] = zz800_ERP_vp_month['position']/10000
# 2、债券
# (1)合并ERP数据
bond_ERP = bond_index.join(ERP_df, how='inner')
bond_ERP['ERP_mean'] = bond_ERP['percentile'].rolling(window=60, min_periods=1).mean()
bond_ERP = bond_ERP[(bond_ERP.index >= '2018-10-01') & (bond_ERP.index <= '2024-12-31')]
# 生成买入卖出信号
threshold_high = 0.7 # 买入阈值
threshold_low = 0.3 # 卖出阈值
bond_ERP_month = bond_ERP.groupby(bond_ERP.index.to_period('M')).nth(0)
bond_ERP_month['signal'] = 0 # 初始化信号
bond_ERP_month['signal'] = np.where(bond_ERP_month['ERP_mean'].shift(1) > 0.7, -1,
np.where(bond_ERP_month['ERP_mean'].shift(1) < 0.3, 1, 0))
# (2)日期格式转换
bond_ERP_month = bond_ERP_month[['close_index', 'signal']]
# 计算持仓
bond_ERP_month = bond_ERP_month[(bond_ERP_month.index >= '2019-01-01') & (bond_ERP_month.index <= '2024-12-31')]
bond_ERP_month.index = bond_ERP_month.index.strftime('%Y-%m')
bond_ERP_month = calculate_position_long(bond_ERP_month, 10000)
# 计算净值
bond_ERP_month['net_value'] = bond_ERP_month['position']/10000
# 3、黄金
gold_US_VIX_MT_month = gold_US_VIX_MT_long[['close_index', 'signal']]
gold_US_VIX_MT_month.index = gold_US_VIX_MT_month.index.strftime('%Y-%m')
# 计算持仓
gold_US_VIX_MT_month = calculate_position_long(gold_US_VIX_MT_month, 10000)
# 计算净值
gold_US_VIX_MT_month['net_value'] = gold_US_VIX_MT_month['position']/10000
# 绘制风险预算+股债择时指标业绩表现
# 风险预算+择时仓位
risk_stock_bond_timing = risk_budget_timing(monthly_results, [zz800_ERP_vp_month, bond_ERP_month])
risk_stock_bond_timing.index = pd.to_datetime(risk_stock_bond_timing.index)
risk_stock_bond_timing['年份'] = risk_stock_bond_timing.index.strftime('%Y')
risk_stock_bond_timing_result = perform_annual_format(risk_stock_bond_timing)
risk_stock_bond_timing_result.set_index('年份', inplace = True)
# 风险预算股债98/2
risk_stock_bond_data = {
'date': risk_stock_bond_timing.index,
'net_value': risk_net_value
}
risk_stock_bond = pd.DataFrame(risk_stock_bond_data)
# print(risk_stock_bond)
risk_stock_bond['date'] = pd.to_datetime(risk_stock_bond['date'])
risk_stock_bond.set_index('date', inplace = True)
# print(risk_stock_bond)
risk_stock_bond['年份'] = risk_stock_bond.index.strftime('%Y')
risk_stock_bond_result = perform_annual_format(risk_stock_bond)
risk_stock_bond_result.set_index('年份', inplace = True)
# 合并两个表
merged_stock_bond = pd.concat([risk_stock_bond_result, risk_stock_bond_timing_result], axis=1, keys=['风险预算股债98/2', '择时加仓30%'])
merged_stock_bond.reset_index(inplace=True)
merged_stock_bond['相对超额收益率'] = [
float(risk_stock_bond_timing_result['收益率'].iloc[i].strip('%')) / 100 -
float(risk_stock_bond_result['收益率'].iloc[i].strip('%')) / 100
for i in range(len(risk_stock_bond_result['收益率']))
]
print("风险预算+股债择时指标业绩表现")
merged_stock_bond
# 绘制加入股债择时时的净值
risk_stock_bond_timing['gap'] = risk_stock_bond_timing['net_value'] - risk_stock_bond['net_value']
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(risk_stock_bond_timing.index, risk_stock_bond_timing['net_value'], label='风险预算+股债择时', linestyle = '-', color = (0, 0, 1))
ax1.plot(risk_stock_bond_timing.index, risk_stock_bond['net_value'], label='风险预算', linestyle = '-', color = (0, 0.5, 1))
# 创建次轴(右轴)用于面积图
ax2 = ax1.twinx()
ax2.fill_between(risk_stock_bond_timing.index, risk_stock_bond_timing['gap'], color = 'goldenrod', alpha=0.3, label='超额(右)')
# 设置 x 轴刻度
ax1.set_xticks(date[::6]) # 隔半年显示一次
ax1.set_xticklabels(date[::6], rotation=45)
# 添加标题和标签
ax1.set_title('加入股债择时时的净值')
# 获取所有图例句柄和标签
lines1, labels1 = ax1.get_legend_handles_labels() # 获取折线图的图例
lines2, labels2 = ax2.get_legend_handles_labels() # 获取面积图的图例
# 合并所有图例
lines = lines1 + lines2
labels = labels1 + labels2
# 创建图例并设置位置在中心
fig.legend(lines, labels, loc = 'upper center', bbox_to_anchor = (0.5, 0.89), ncol = 3)
# 展示图形
plt.show()
# 绘制表——1%风险预算分配给黄金,组合波动率下降明显
gold['date'] = pd.to_datetime(gold['date'])
gold.set_index('date', inplace = True)
# 1、计算风险预算股债金的投资权重
start_date = pd.to_datetime('2018-10-01')
end_date = pd.to_datetime('2024-12-31')
current_date = pd.to_datetime('2019-01-01')
risk_sbg_monthly_results = []
target_risk_contributions = np.array([0.97, 0.02, 0.01])
while current_date <= end_date:
# 计算当前月的最后一天
next_month_first_day = (current_date.replace(day=1) + pd.offsets.MonthBegin(1))
month_end_date = next_month_first_day - pd.Timedelta(days=1)
# print(type(next_month_first_day))
# print(type(month_end_date))
# 获取过去60天的交易日的数据
look_back_period = current_date - BDay(60)
# print(type(look_back_period))
try:
zz800_window = zz800.loc[look_back_period:current_date]
# print(zz800_window)
bond_index_window = bond_index.loc[look_back_period:current_date]
# print(bond_index_window)
gold_window = gold.loc[look_back_period:current_date]
# print(gold_window)
# print(type(current_date))
# 创建包含股票和债券回报率的数据集
stock_bond_gold_data = {
'return_stock': zz800_window['log_return'],
'return_bond': bond_index_window['log_return'],
'return_gold': gold_window['log_return']
}
stock_bond_gold_return = pd.DataFrame(stock_bond_gold_data).dropna()
# print(stock_bond_gold_return)
if not stock_bond_gold_return.empty:
# 计算协方差矩阵
R_stock_bond_gold_cov = stock_bond_gold_return.cov()
stock_bond_gold_cov = np.array(R_stock_bond_gold_cov)
# 定义随机的初始化权重
bounds_sbg = [
(0.0, 0.3),
(0.7, 1.0),
(0.0, 1.0)
]
weight_x0_sbg = np.array([
np.random.uniform(low=bounds_sbg[0][0], high=bounds_sbg[0][1]),
np.random.uniform(low=bounds_sbg[1][0], high=bounds_sbg[1][1]),
np.random.uniform(low=bounds_sbg[2][0], high=bounds_sbg[2][1])
])
weight_x0_sbg /= np.sum(weight_x0_sbg)
constraints_sbg = ({'type': 'eq', 'fun': total_weight_constraint})
options_sbg={'disp': False, 'maxiter': 1000, 'ftol': 1e-20}
solution_sbg = minimize(risk_budget_objective, weight_x0_sbg, args=(stock_bond_gold_cov, target_risk_contributions), bounds=bounds_sbg, constraints=constraints_sbg, method='SLSQP', options=options_sbg)
final_weights_sbg = solution_sbg.x
value_sbg = zz800_window['close_index'].iloc[-1]*final_weights_sbg[0]/zz800['close_index'].iloc[0] + bond_index_window['close_index'].iloc[-1]*final_weights_sbg[1]/bond_index['close_index'].iloc[0] + gold_window['close_price'].iloc[-1]*final_weights_sbg[2]/gold['close_price'].iloc[0]
# fixed_value.append(zz800_window['close_index'].iloc[-1]*0.2 + bond_index_window['close_index'].iloc[-1]*0.8)
# 将结果存储起来
risk_sbg_monthly_results.append({
'month': current_date.strftime('%Y-%m'),
'cov_matrix': stock_bond_gold_cov,
'weights': final_weights_sbg,
'value': value_sbg
})
else:
print(f"Warning: No valid data found for {current_date.strftime('%Y-%m')}")
except Exception as e:
print(f"Error processing {current_date.strftime('%Y-%m')}: {str(e)}")
# 移动到下一个月
current_date = next_month_first_day
# print(type(current_date))
# 打印或进一步处理 monthly_results 中的结果
risk_sbg_net_value = []
for result in risk_sbg_monthly_results:
print(f"Month: {result['month']}")
print("协方差矩阵:")
print(result['cov_matrix'])
print("最终权重:")
print(f"{result['weights'][0]:.2%} 投资于 中证800")
print(f"{result['weights'][1]:.2%} 投资于 中债综合指数")
print(f"{result['weights'][2]:.2%} 投资于 黄金")
risk_sbg_net_value.append(result['value']/risk_sbg_monthly_results[0]['value'])
# 将结果生成Dataframe方便处理
risk_sbg_data = {
'date': risk_stock_bond_timing.index,
'net_value': risk_sbg_net_value
}
risk_sbg = pd.DataFrame(risk_sbg_data)
risk_sbg['date'] = pd.to_datetime(risk_sbg['date'])
risk_sbg.set_index('date', inplace = True)
risk_sbg['年份'] = risk_sbg.index.strftime('%Y')
risk_sbg_result = perform_annual_format(risk_sbg)
risk_sbg_result.set_index('年份', inplace = True)
# 合并两个表
merged_risk_sbg = pd.concat([risk_stock_bond_result, risk_sbg_result], axis=1, keys=['风险预算股债98/2', '风险预算股债金97/2/1'])
merged_risk_sbg.reset_index(inplace=True)
merged_risk_sbg['相对超额波动率'] = [
float(risk_sbg_result['波动率'].iloc[i].strip('%')) / 100 -
float(risk_stock_bond_result['波动率'].iloc[i].strip('%')) / 100
for i in range(len(risk_stock_bond_result['波动率']))
]
print()
print()
print("1%风险预算分配给黄金,组合波动率下降明显")
merged_risk_sbg
# 绘制表——加入黄金择时,组合波动进一步下降,收益有所提升
# 风险预算+黄金择时
zz800_risk_gold_timing = zz800_ERP_vp_month[['close_index']]
zz800_risk_gold_timing['net_value'] = zz800_risk_gold_timing['close_index']/zz800_risk_gold_timing['close_index'].iloc[0]
bond_risk_gold_timing = bond_ERP_month[['close_index']]
bond_risk_gold_timing['net_value'] = bond_risk_gold_timing['close_index']/bond_risk_gold_timing['close_index'].iloc[0]
risk_gold_timing = risk_budget_timing(risk_sbg_monthly_results, [zz800_risk_gold_timing, bond_risk_gold_timing, gold_US_VIX_MT_month])
risk_gold_timing.index = pd.to_datetime(risk_gold_timing.index)
risk_gold_timing['年份'] = risk_gold_timing.index.strftime('%Y')
risk_gold_timing_result = perform_annual_format(risk_gold_timing)
risk_gold_timing_result.set_index('年份', inplace = True)
risk_gold_timing_result
# 合并两个表
merged_gold = pd.concat([risk_sbg_result, risk_gold_timing_result], axis=1, keys=['风险预算股债金97/2/1', '风险预算股债金97/2/1+黄金择时'])
merged_gold.reset_index(inplace=True)
merged_gold['相对超额波动率'] = [
float(risk_gold_timing_result['波动率'].iloc[i].strip('%')) / 100 -
float(risk_sbg_result['波动率'].iloc[i].strip('%')) / 100
for i in range(len(risk_stock_bond_result['波动率']))
]
print("加入黄金择时,组合波动进一步下降,收益有所提升")
merged_gold
# 绘制图——黄金择时降低波动平滑收益
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(risk_gold_timing.index, risk_gold_timing['net_value'], label='风险预算+黄金择时', linestyle = '-', color = (0, 0, 1))
ax1.plot(risk_sbg.index, risk_sbg['net_value'], label='风险预算', linestyle = '-', color = 'goldenrod')
ax1.set_title("黄金择时降低波动平滑收益")
ax1.legend()
# 展示图形
plt.show()
# 绘制图——组合净值
risk_sbg_timing = risk_budget_timing(risk_sbg_monthly_results, [zz800_ERP_vp_month, bond_ERP_month, gold_US_VIX_MT_month])
risk_sbg_timing.index = pd.to_datetime(risk_sbg_timing.index)
risk_sbg_timing['年份'] = risk_sbg_timing.index.strftime('%Y')
risk_sbg_timing_result = perform_annual_format(risk_sbg_timing)
risk_sbg_timing_result.set_index('年份', inplace = True)
risk_sbg_timing_result
# print(risk_sbg_timing)
# 固定比例
fix_sbg_data = {
'date': zz800_ERP_vp_month.index,
'zz800_price': zz800_ERP_vp_month['close_index'],
'bond_price': bond_ERP_month['close_index'],
'gold_price': gold_US_VIX_MT_month['close_index']
}
fix_sbg = pd.DataFrame(fix_sbg_data)
fix_sbg['date'] = pd.to_datetime(fix_sbg['date'])
fix_sbg.set_index(fix_sbg['date'])
fix_sbg['net_value'] = 0.15*fix_sbg['zz800_price']/fix_sbg['zz800_price'].iloc[0] + 0.8*fix_sbg['bond_price']/fix_sbg['bond_price'].iloc[0] + 0.05*fix_sbg['gold_price']/fix_sbg['gold_price'].iloc[0]
# 画图
fig, ax1 = plt.subplots(figsize=(15, 5))
plt.rcParams['font.family'] = ['SimHei'] # 解决中文字体无法显示的问题
plt.rcParams['axes.unicode_minus']=False # 解决负号无法显示的问题
ax1.set_xlabel('日期')
ax1.plot(risk_sbg_timing.index, fix_sbg['net_value'], label='固定比例', linestyle = '-', color = (0, 0, 1))
ax1.plot(risk_sbg_timing.index, risk_sbg['net_value'], label='风险预算', linestyle = '-', color = (0, 0.5, 0.9))
ax1.plot(risk_sbg_timing.index, risk_sbg_timing['net_value'], label='风险预算(加入战术择时)', linestyle = '-', color = 'goldenrod')
ax1.set_title("组合净值")
ax1.legend()
# 展示图形
plt.show()
# 绘制表——固定比例 -> 风险预算 -> 加入择时的风险预算组合年度业绩
# 计算固定比例收益情况
fix_sbg.index = pd.to_datetime(fix_sbg.index)
fix_sbg['年份'] = fix_sbg.index.strftime('%Y')
fix_sbg_result = perform_annual_format(fix_sbg)
fix_sbg_result.set_index('年份', inplace = True)
# 合并三个表
merged_fix_risk_timing = pd.concat([fix_sbg_result, risk_sbg_result, risk_sbg_timing_result], axis=1, keys=['固定比例(股债金15/80/5)', '风险预算(股债金97/2/1)', '风险预算(股债金97/2/1+战略择时)'])
print("固定比例 -> 风险预算 -> 加入择时的风险预算组合年度业绩")
merged_fix_risk_timing
结果
不知道是因为我理解问题还是代码策略问题抑或是这个策略不太适合这个时间窗,回测出来的结果都不是很好