29.6 交易回测
回测是一种用于评估策略或模型在过去表现如何的方法。通过回测,可以利用历史数据来评估交易策略的可行性。如果回测结果良好,交易者和分析师可能会对未来使用该策略充满信心。回测允许交易者使用历史数据模拟交易策略,以生成结果并分析风险和盈利能力,从而在实际投入资金之前进行评估。一个经过良好执行的回测,如果结果积极,能够让交易者确信该策略在现实中实施时是有基础的,并且可能会带来利润。相反,如果回测结果不佳,则会促使交易者修改或放弃该策略。
29.6.1 基本回测
在本项目中,回测的目的是评估所开发的交易模型的表现和有效性。通过使用历史股票数据对模型进行回测,我们可以模拟模型在过去的市场环境中的交易结果。这一过程帮助我们了解模型的盈利能力和风险控制效果,以便在实际投资中做出更有信心的决策。回测可以揭示模型在不同市场条件下的表现,从而验证其稳定性和可靠性。
(1)下面这段代码实现了一个回测函数 backtest,用于评估模型在历史数据上的表现。该函数通过将数据集分成训练集和测试集,以滑动窗口的方式逐步训练模型并进行预测。具体步骤包括:从数据集中提取训练集和测试集、训练模型、对测试集进行预测、将预测结果与真实目标值结合,并将所有结果合并返回。这样可以评估模型在不同数据段上的性能。
def backtest(data, model, predictors, start=1000, step=50):
predictions = []
# 遍历数据集,按增量分割
for i in range(start, data.shape[0], step):
# 划分为训练集和测试集
train = data.iloc[0:i].copy()
test = data.iloc[i:(i+step)].copy()
# 训练模型
model.fit(train[predictors], train["Target"])
# 进行预测
preds = model.predict_proba(test[predictors])[:,1]
preds = pd.Series(preds, index=test.index)
preds[preds > .6] = 1
preds[preds <= .6] = 0
# 合并预测结果和测试值
combined = pd.concat({"Target": test["Target"], "Predictions": preds}, axis=1)
# 将合并结果添加到预测列表中
predictions.append(combined)
return pd.concat(predictions)
在上面的回测代码中,我们首先在前1000行数据上训练模型,然后在接下来的50行数据上测试模型,这个过程会对整个数据集进行。这种方法可以确保模型在每次迭代中都从数据中学习,从而能够做出更好的预测。
(2)调用函数predict_proba进行预测,它实际上给出的是预测概率。虽然模型通常使用0.5作为分类阈值,我们这里将阈值提高到0.6,并基于此返回预测结果。可以尝试不同的阈值,看看是否在0.6到1之间的值能提供更好的结果。
start = time.time()
backtestpredictions = backtest(df, model, predictors)
end = time.time()
print(f'Time Elapsed in Backtesting : {round(end-start,2)} seconds')
(3)查看在回测过程中生成的预测结果,包含了模型在每次回测迭代中的预测值和实际目标值。
backtestpredictions
执行后会输出下面的内容,通过比较这些预测值和实际值,可以评估模型在不同数据分割下的性能和稳定性。具体来说,它帮助我们了解模型在历史数据上的表现,以及它在未来数据上的预测能力。
Date Target Predictions
2008-08-12 00:00:00-04:00 1.0 1.0
2008-08-13 00:00:00-04:00 1.0 0.0
2008-08-14 00:00:00-04:00 0.0 0.0
2008-08-15 00:00:00-04:00 1.0 1.0
2008-08-18 00:00:00-04:00 1.0 1.0
... ... ...
2023-01-17 00:00:00-05:00 1.0 0.0
2023-01-18 00:00:00-05:00 0.0 0.0
2023-01-19 00:00:00-05:00 0.0 0.0
2023-01-20 00:00:00-05:00 1.0 1.0
2023-01-23 00:00:00-05:00 1.0 1.0
3637 rows × 2 columns
(4)下面代码用于计算并输出在回测前的预测结果中模型的精确率(Precision Score)。精确率是一个衡量模型预测准确度的指标,计算了预测为正样本中真正正样本的比例。结果乘以100并四舍五入到小数点后两位,用于展示精确率的百分比值。
# 输出在回测前的精确率分数,结果乘以100并四舍五入到小数点后两位
print('在添加预测变量之前的精确率分数',
round(precision_score(backtestpredictions['Target'], backtestpredictions['Predictions']) * 100, 2))
执行后会输出:
Precision Score before adding predictors 67.83
(5)下面代码用于绘制回测预测结果中的错误分类情况。
# 从 "backtestpredictions" 数据框中的数据创建一个计数图,x轴表示目标类别
ax = sns.countplot(backtestpredictions['Target'][backtestpredictions['Target'] != backtestpredictions['Predictions']])
# 移除图表的顶部和右侧边框
plt.gca().spines['top'].set_visible(False)
plt.gca().spines['right'].set_visible(False)
# 设置x轴标签
plt.xlabel(' Target Classes')
# 设置图表标题
plt.title(' Misclassification Plot')
# 遍历图表中的每个条形,并添加一个注释,显示该条形的数量
for bar in ax.patches:
ax.annotate(format(bar.get_height(), '.0f'), (bar.get_x() + bar.get_width() / 2, bar.get_height()),
ha='center', va='center', size=15, xytext=(0, 8), textcoords='offset points')
# 显示图表
plt.show()
执行效果如图29-18所示,生成了一个计数图(countplot),显示了预测错误的目标类别的分布。图中会标记每个条形图的数量,并去除图表的顶部和右侧边框,以便更清晰地展示错误分类的情况。
图29-18 预测错误的目标类别分布图
29.6.2 添加更多预测因子
为了提高模型的精度,我们可以通过特征工程来增强模型性能。具体来说,我们将添加一些新的预测因子,包括周平均值、季度平均值和年度平均值。同时,我们还会分析目标变量的周趋势,即该周的价格走势。此外,我们还引入了多个百分比指标,如开盘价与收盘价的比率、年度与季度平均值等。通过这些附加特征,模型能够获得更多关于数据和趋势的信息,从而提升预测的准确性。
(1)在下面的代码中,通过添加新的特征来增强数据集,目的是提升模型的预测能力。首先,计算了股票的周、季度和年度滚动平均值,并计算了目标变量的周趋势。然后,生成了多个比率特征,包括周、季度、年度平均值与当前收盘价的比率,以及开盘价、最高价、最低价与收盘价的比率。最后,还计算了年度与季度和周平均值的比率。通过这些新增特征,将为模型提供更多关于数据趋势的信息。
# 计算滚动平均值
weekly_mean = df.rolling(7).mean() # 周平均值
quarterly_mean = df.rolling(90).mean() # 季度平均值
annual_mean = df.rolling(365).mean() # 年度平均值
# 计算周趋势
weekly_trend = df.shift(1).rolling(7).mean()["Target"] # 目标变量的周趋势
# 计算滚动平均值的比率和其他比率
df["weekly_mean"] = weekly_mean["Close"] / df["Close"] # 周平均值与收盘价的比率
df["quarterly_mean"] = quarterly_mean["Close"] / df["Close"] # 季度平均值与收盘价的比率
df["annual_mean"] = annual_mean["Close"] / df["Close"] # 年度平均值与收盘价的比率
# 添加年度和周趋势
df["annual_weekly_mean"] = df["annual_mean"] / df["weekly_mean"] # 年度平均值与周平均值的比率
df["annual_quarterly_mean"] = df["annual_mean"] / df["quarterly_mean"] # 年度平均值与季度平均值的比率
df["weekly_trend"] = weekly_trend # 周趋势
# 计算开盘价与收盘价的比率
df["open_close_ratio"] = df["Open"] / df["Close"]
# 计算最高价与收盘价的比率
df["high_close_ratio"] = df["High"] / df["Close"]
# 计算最低价与收盘价的比率
df["low_close_ratio"] = df["Low"] / df["Close"]
df.head() # 显示前几行数据
执行后会输出:
Open High Low Close Volume Target weekly_mean quarterly_mean annual_mean annual_weekly_mean annual_quarterly_mean weekly_trend open_close_ratio high_close_ratio low_close_ratio
Date
2004-08-23 00:00:00-04:00 2.527778 2.729730 2.515015 2.710460 456686856.0 1.0 NaN NaN NaN NaN NaN NaN 0.932601 1.007109 0.927892
2004-08-24 00:00:00-04:00 2.771522 2.839840 2.728979 2.737738 365122512.0 1.0 NaN NaN NaN NaN NaN NaN 1.012340 1.037294 0.996801
2004-08-25 00:00:00-04:00 2.783784 2.792793 2.591842 2.624374 304946748.0 0.0 NaN NaN NaN NaN NaN NaN 1.060742 1.064175 0.987604
2004-08-26 00:00:00-04:00 2.626627 2.702703 2.599600 2.652653 183772044.0 1.0 NaN NaN NaN NaN NaN NaN 0.990189 1.018868 0.980000
2004-08-27 00:00:00-04:00 2.626376 2.701451 2.619119 2.700450 141897960.0 1.0 NaN NaN NaN NaN NaN NaN 0.972570 1.000371 0.969882
(2)打印输出当前数据集在添加更多预测变量(特征)后有多少列,通过 df.shape[1] 获取数据框 df 的列数,并使用 f-string 进行格式化输出。
print(f'We now have {df.shape[1]} columns after adding more predictors')
执行后会输出:
We now have 15 columns after adding more predictors
29.6.3 填充空值
填充空值(Imputing Null Values)是指填补缺失值的过程,虽然 XGBoost 足够强大,可以处理缺失值,但是许多算法并没有提供填充空值功能。因此,通常的做法是对缺失值进行填充。在本项目中,由于之前进行的回测,对于某些行来说,由于时间范围的原因(例如按周、按年计算的均值),没有可用的历史数据,因此出现了一些 NaN(空值)。我们将通过将这些缺失值填充为 0 来处理这些 NaN 值。尽管这可能不是最优的策略,但我们决定使用这种方法。
(1)下面代码用于检查 DataFrame df 中各列的空值情况,遍历了所有的列,如果某列中存在空值,则打印出该列的编号、列名以及该列中空值占总数据的百分比。
# 遍历 df DataFrame 的列数范围
for i in range(len(df.columns)):
# 检查当前列的空值数量是否大于零
if df[df.columns[i]].isnull().sum() > 0:
# 打印有空值的列号、列名以及该列空值所占的百分比
print(f'第 {i} 列 {df.columns[i]} 的空值占比 : {round(df[df.columns[i]].isnull().sum()/df.shape[0]*100, 2)}%')
执行后会遍历 df 数据集的所有列,并识别其中存在空值的列。对于每个包含空值的列,会打印输出该列的编号、名称以及空值在该列中的百分比。
Column 6 weekly_mean null values : 0.13%
Column 7 quarterly_mean null values : 1.92%
Column 8 annual_mean null values : 7.85%
Column 9 annual_weekly_mean null values : 7.85%
Column 10 annual_quarterly_mean null values : 7.85%
Column 11 weekly_trend null values : 0.15%
(2)将 DataFrame df 中的所有空值填充为零,然后检查是否还有未填充的空值。
# 将空值填充为 0
df.fillna(0, inplace=True)
# 检查是否还有未填充的空值
df.isnull().sum()
上述代码的功能是将数据集中所有的空值用 0 进行填充,并通过 isnull().sum() 检查是否成功填充了所有空值。这样可以确保数据在后续处理中不会受到空值的影响。执行后会输出:
Open 0
High 0
Low 0
Close 0
Volume 0
Target 0
weekly_mean 0
quarterly_mean 0
annual_mean 0
annual_weekly_mean 0
annual_quarterly_mean 0
weekly_trend 0
open_close_ratio 0
high_close_ratio 0
low_close_ratio 0
dtype: int64
29.6.4 回测新预测变量
现在已经向数据中添加了更多的列,接下来开始使用这些新特征来进行回测,看看它们能带来多大的性能提升。
(1)下面代码的功能是使用新增的预测变量对模型进行回测,并计算回测所用的时间。
# 定义预测变量列表
predictors = ['Open', 'High','Low','Close','Volume','weekly_mean','quarterly_mean','annual_mean',
'annual_weekly_mean','annual_quarterly_mean','weekly_trend','open_close_ratio','high_close_ratio','low_close_ratio']
# 记录开始时间
start = time.time()
# 使用新增的预测变量进行回测
backtestpredictions = backtest(df, model, predictors)
# 记录结束时间
end = time.time()
# 打印回测所花费的时间
print(f'回测耗时 : {round(end-start,2)} 秒')
在上述代码中,首先定义了多个预测变量,包括股票价格的开盘价、最高价、最低价、收盘价、成交量等时间序列特征。随后,调用 backtest 函数执行回测过程,并测量整个过程的执行时间。执行后会输出:
Time Elapsed in Backtesting : 92.29 seconds
(2)计算在添加了新的预测变量之后的精度得分,并将得分四舍五入保留两位小数后输出。精度得分衡量了模型对正类(如上涨的股票)的预测准确率。
print('Precision Score after adding predictors',
round(precision_score(backtestpredictions['Target'], backtestpredictions['Predictions'])*100,2))
执行后会输出:
Precision Score after adding predictors 85.28
(3)下面代码的功能是输出分类报告,展示模型在回测中对目标数据的预测性能。在分类报告中包括精度(precision)、召回率(recall)、F1得分等指标,用于评估模型的分类效果。
# 输出分类报告,显示模型在回测中对目标数据的预测性能
print(classification_report(backtestpredictions['Target'], backtestpredictions['Predictions']))
执行后会输出:
precision recall f1-score support
0.0 0.80 0.85 0.82 1733
1.0 0.85 0.80 0.83 1904
accuracy 0.82 3637
macro avg 0.82 0.83 0.82 3637
weighted avg 0.83 0.82 0.82 3637
通过上面的输出结果可以看到,通过添加更多的预测变量,我们已经成功提高了精度百分比。接下来,将使用统计模型 ARIMA 来进行预测。我们将利用开盘价(Open)、最高价(High)和最低价(Low)来预测收盘价(Close)。