(29-5)通过回测、ARIMA 和 GRU 预测股票价格:交易回测

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)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

感谢鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值