
《数据科学与机器学习(第25部分):使用循环神经网络(RNN)进行外汇时间序列预测》
内容
- 循环神经网络(RNNs)是什么
- 理解RNNs
- 循环神经网络(RNN)背后的数学原理
- 在Python中构建循环神经网络(RNN)模型
- 创建序列数据
- 为回归问题训练简单的循环神经网络(RNN)
- RNN特征的重要性
- 训练RNN用于分类问题
- 将循环神经网络模型保存为ONNX格式
- 循环神经网络(RNN)EA
- 在策略测试器上测试RNN EA
- 使用简单RNN进行时间序列预测的优势
- 结论
循环神经网络(RNNs)是什么
循环神经网络(RNNs)是设计用于识别数据序列(如时间序列、语言或视频)中模式的人工神经网络。与传统 神经网络不同,传统神经网络假设输入是相互独立的,而RNN能够检测和理解来自数据序列(信息)的模式。
请注意,为了避免与本文中的术语混淆,当我说循环神经网络时,我指的是简单RNN作为模型;而当我使用循环神经网络(RNNs)时,我指的是循环神经网络模型家族,如简单RNN、长短期记忆(LSTM)和门控循环单元(GRU)。
要完全理解本文的内容,需要具备Python、MQL5中的ONNX和Python机器学习的基础知识。
理解RNNs
RNNs具有一种称为序列记忆的特性,这指的是在序列中保留和利用之前时间步的信息来为后续时间步的处理提供信息的概念。
序列记忆类似于人类大脑中的记忆,它是那种让你更容易识别序列中模式的记忆,比如你在说话时组织词汇的过程。
循环神经网络(RNNs)的核心是前馈神经网络,它们以这样一种方式相互连接,即下一个网络拥有来自前一个网络的信息,从而使简单的RNN能够基于先前的信息来学习和理解当前的信息。
为了更好地理解这一点,让我们来看一个例子,在这个例子中,我们想训练一个用于聊天机器人的RNN模型,我们希望聊天机器人能够理解用户的单词和句子,假设收到的句子是:现在几点了?
这些单词将被拆分成各自的时间步,并依次输入到RNN中,如下图所示。
观察网络中的最后一个节点,你可能会注意到一个奇怪的颜色排列,它代表了来自之前网络和当前网络的信息。观察颜色,在时间t=0和时间t=1时,RNN最后一个节点中的信息非常少(几乎不存在)。
随着RNN处理更多的步骤,它很难保留之前步骤的信息。如上图所示,单词what和time在网络的最终节点中几乎不存在。
这就是我们所说的短期记忆。它是由许多因素造成的,其中反向传播是主要因素之一。
循环神经网络(RNNs)有其独特的反向传播过程,被称为随时间反向传播。在反向传播过程中,随着网络按每时间步长向后传播到,梯度值会呈指数级缩小。梯度用于调整神经网络参数(权重和偏置),这种调整是神经网络能够学习的基础。梯度较小意味着调整幅度较小。由于早期层接收到的梯度较小,这导致它们的学习效果不如应有的那么有效。这被称为梯度消失问题。
由于梯度消失问题,简单RNN无法学习时间步之间的长期依赖关系。在上面的图像示例中,当我们的聊天机器人RNN模型试图理解用户给出的示例句子时,像what和time这样的单词很可能完全被忽略。网络只能根据仅有三个单词的半句话做出最佳猜测:is it ?。这使得RNN的效果不佳,因为其记忆太短,无法理解现实世界应用中常见的长时间序列数据。
为了缓解短期记忆问题,引入了两种专门的循环神经网络:长短期记忆(LSTM)和门控循环单元(GRU)。
LSTM和GRU在许多方面与RNN工作方式相似,但它们能够使用称为门的机制来理解长期依赖关系。我们将在下一篇文章中详细讨论它们敬请期待。
循环神经网络(RNN)背后的数学原理
与前馈神经网络不同,循环神经网络(RNNs)具有形成循环的连接,允许信息持续存在。下面简化的图像展示了RNN单元/细胞在分解后的样子。
其中:
表示时间t时的输入。
表示时间t时的隐藏状态。
隐藏状态
表示为,这是一个向量,存储了来自之前时间步的信息。它作为网络的记忆,使其能够随时间捕获输入数据中的时间依赖性和模式。
隐藏状态对网络的作用
在RNN中,隐藏状态具有几个关键功能,如:
- 它保留了来自之前输入的信息,这使网络能够从整个序列中学习。
- 它为当前输入提供了上下文,这使网络能够根据过去的数据做出有根据的预测。
- 它构成了网络内循环连接的基础,这允许隐藏层在不同时间步上影响自身。
了解RNN背后的数学原理并不像知道如何、在哪里以及何时使用它们那么重要。如果你希望的话,可以自由跳转到本文的下一节。
数学公式
在时间步时,隐藏状态是通过时间步
的输入、前一个时间步
(的隐藏状态以及相应的权重矩阵和偏置来计算的。这个公式如下:
其中:
是输入到隐藏状态的权重矩阵。
是隐藏状态到隐藏状态的权重矩阵。
是隐藏状态的偏置项。
σ 是激活函数(例如,tanh 或 ReLU)
输出层
在时间步时的输出是通过时间步的隐藏状态来计算的
。
其中
是在时间步
时的输出。
是从隐藏状态到输出的权重矩阵。
是输出层的偏置。
损失计算
假设有一个损失函数(这可以是任何损失函数,例如,回归问题的均方误差或分类问题的交叉熵)。
所有时间步上的总损失是:
随时间反向传播(BPTT):
为了更新权重和偏置,我们需要分别计算损失相对于每个权重和偏置的梯度,然后使用获得的梯度进行更新。这涉及以下步骤:
步长 | 对于权重 | 对于偏差 |
---|---|---|
计算输出层的梯度 | 关于权重的梯度:![]() 其中 ![]() ![]() | 关于偏差: ![]() 由于输出偏置 ![]() ![]() 因此 ![]() |
计算隐藏状态相对于权重和偏置的梯度 | 损失相对于隐藏状态的梯度既包括来自当前时间步的直接贡献,也包括通过后续时间步的间接贡献。 ![]() 隐藏状态相对于前一时间步的梯度。 ![]() 隐藏状态激活的梯度。 ![]() 隐藏层权重的梯度。 ![]() 总梯度是所有时间步上梯度的和。 ![]() | 损失相对于隐藏偏置 ![]() ![]() ![]() 由于隐藏偏移 ![]() ![]() 使用链式法则,并注意到: ![]() 其中, ![]() 因此: ![]() 隐藏偏置的总梯度是所有时间步上梯度的和。 ![]() |
更新权重和偏置。 使用上面计算得到的梯度,我们可以使用梯度下降或其变体(例如Adam)来更新权重,了解更多。 | ![]() ![]() ![]() | ![]() ![]() |
尽管简单的循环神经网络(RNN)不具备很好地学习长时序数据的能力,但它们仍然擅长利用不久前的信息来预测未来的值。我们可以构建一个简单的RNN来帮助我们做出交易决策。
在Python中构建循环神经网络(RNN)模型
使用Keras库在Python中构建和编译一个RNN模型非常简单,只需要几行代码即可完成。
Python:
import tensorflow as tf from tensorflow.keras.models import Sequential #import sequential neural network layer from sklearn.preprocessing import StandardScaler from tensorflow.keras.layers import SimpleRNN, Dense, Input from keras.callbacks import EarlyStopping from sklearn.preprocessing import MinMaxScaler from keras.optimizers import Adam reg_model = Sequential() reg_model.add(Input(shape=(time_step, x_train.shape[1]))) # input layer reg_model.add(SimpleRNN(50, activation='sigmoid')) #first hidden layer reg_model.add(Dense(50, activation='sigmoid')) #second hidden layer reg_model.add(Dense(units=1, activation='relu')) # final layer adam_optimizer = Adam(learning_rate = 0.001) reg_model.compile(optimizer=adam_optimizer, loss='mean_squared_error') # Compile the model reg_model.summary()
上面的代码是为回归循环神经网络编写的,这就是为什么输出层有1个节点,并且在最后一层使用了ReLU激活函数,这是有原因的。正如在文章《前馈神经网络揭秘》中所讨论的那样。
使用我们在之前文章《使用常规机器学习模型进行外汇时间序列预测》(必读)中收集的数据,我们想看看如何使用RNN模型,因为它们能够理解时间序列数据,从而在它们擅长的方面为我们提供帮助。
最后,我们将评估RNN与在之前文章中构建的LightGBM在同一数据集上的性能。希望这能帮助你巩固对时间序列预测的一般理解。
创建序列数据
在我们的数据集中,有28列,这些列都是为了非时间序列模型而设计的。
然而,我们收集和设计的这些数据包含了很多滞后变量,这些变量对于非时间序列模型来说非常有用,因为它们可以检测到时间依赖的模式。正如我们所知,RNN能够理解给定时间步内的模式。
现在我们不需要这些滞后值,必须将它们删除。
Python:
lagged_columns = [col for col in data.columns if "lag" in col.lower()] #let us obtain all the columns with the name lag print("lagged columns: ",lagged_columns) data = data.drop(columns=lagged_columns) #drop them
输出
lagged columns: ['OPEN_LAG1', 'HIGH_LAG1', 'LOW_LAG1', 'CLOSE_LAG1', 'OPEN_LAG2', 'HIGH_LAG2', 'LOW_LAG2', 'CLOSE_LAG2', 'OPEN_LAG3', 'HIGH_LAG3', 'LOW_LAG3', 'CLOSE_LAG3', 'DIFF_LAG1_OPEN', 'DIFF_LAG1_HIGH', 'DIFF_LAG1_LOW', 'DIFF_LAG1_CL
新的数据有12列。
我们可以将70%的数据划分为训练集,剩下的30%作为测试集。如果你正在使用Scikit-Learn中的train_test_split函数,请确保设置shuffle=False。这样做可以确保该函数在划分数据时保持原始数据的顺序不变,从而保留信息的时间顺序。
请记住!这是时间序列预测。
# Split the data X = data.drop(columns=["TARGET_CLOSE","TARGET_OPEN"]) #dropping the target variables Y = data["TARGET_CLOSE"] test_size = 0.3 #70% of the data should be used for training purpose while the rest 30% should be used for testing x_train, x_test, y_train, y_test = train_test_split(X, Y, shuffle=False, test_size = test_size) # this is timeseries data so we don't shuffle print(f"x_train {x_train.shape}\nx_test {x_test.shape}\ny_train{y_train.shape}\ny_test{y_test.shape}")
在删除了两个目标变量之后,我们的数据现在剩下10个特征。我们需要将这些10个特征转换为RNN能够处理的序列数据。
def create_sequences(X, Y, time_step): if len(X) != len(Y): raise ValueError("X and y must have the same length") X = np.array(X) Y = np.array(Y) Xs, Ys = [], [] for i in range(X.shape[0] - time_step): Xs.append(X[i:(i + time_step), :]) # Include all features with slicing Ys.append(Y[i + time_step]) return np.array(Xs), np.array(Ys)
上述功能用于根据给定的x和y数组以及指定的时间步长生成序列。为了理解这个函数是如何工作的,请阅读以下示例;
假设我们有一个包含10个样本和2个特征的数据集,并且我们想要创建一个时间步长为3的序列。
X是一个形状为(10, 2)的矩阵。Y是一个长度为10的向量。该函数将按如下方式创建序列:
当i=0时:Xs获取[0:3, :] X[0:3, :]的数据,Ys获取Y[3]的值。当i=1时:Xs获取X[1:4, :]的数据,Ys获取Y[4]的值。
以此类推,直到i=6。
在将我们拆分后的自变量标准化之后,我们可以应用函数create_sequences来生成序列信息。
time_step = 7 # we consider the past 7 days from sklearn.preprocessing import StandardScaler scaler = StandardScaler() x_train = scaler.fit_transform(x_train) x_test = scaler.transform(x_test) x_train_seq, y_train_seq = create_sequences(x_train, y_train, time_step) x_test_seq, y_test_seq = create_sequences(x_test, y_test, time_step) print(f"Sequential data\n\nx_train {x_train_seq.shape}\nx_test {x_test_seq.shape}\ny_train{y_train_seq.shape}\ny_test{y_test_seq.shape}")
输出
Sequential data x_train (693, 7, 10) x_test (293, 7, 10) y_train(693,) y_test(293,)
时间步长为7确保了RNN(循环神经网络)在每个时刻都能获取到过去7天的信息,这是基于我们已从数据集中收集了所有每日时间框架内的信息。这与我们在本系列的前一篇文章中手动获取当前柱形前7天的滞后数据是类似的。
为回归问题训练简单的循环神经网络(RNN)
early_stopping = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True) history = reg_model.fit(x_train_seq, y_train_seq, epochs=100, batch_size=64, verbose=1, validation_data=(x_test_seq, y_test_seq), callbacks=[early_stopping])
输出
Epoch 95/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 6.4504e-05 - val_loss: 4.4433e-05 Epoch 96/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 6.4380e-05 - val_loss: 4.4408e-05 Epoch 97/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 6.4259e-05 - val_loss: 4.4386e-05 Epoch 98/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 6.4140e-05 - val_loss: 4.4365e-05 Epoch 99/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 7ms/step - loss: 6.4024e-05 - val_loss: 4.4346e-05 Epoch 100/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 7ms/step - loss: 6.3910e-05 - val_loss: 4.4329e-05
在测试样本的性能测量之后。
Python:
from sklearn.metrics import r2_score y_pred = reg_model.predict(x_test_seq) # Make predictions on the test set # Plot the actual vs predicted values plt.figure(figsize=(12, 6)) plt.plot(y_test_seq, label='Actual Values') plt.plot(y_pred, label='Predicted Values') plt.xlabel('Samples') plt.ylabel('TARGET_CLOSE') plt.title('Actual vs Predicted Values') plt.legend() plt.show() print("RNN accuracy =",r2_score(y_test_seq, y_pred))
模型的准确率达78%。
如果你还记得前一篇文章的内容,LightGBM模型在回归问题上达到了86.76%的准确率。在这一点上,一个非时间序列模型的表现已经超过了时间序列模型。
特征重要性
我使用SHAP方法运行了一个测试,来检查各个变量是如何影响RNN模型决策过程的。
import shap # Wrap the model prediction for KernelExplainer def rnn_predict(data): data = data.reshape((data.shape[0], time_step, x_train.shape[1])) return reg_model.predict(data).flatten() # Use SHAP to explain the model sampled_idx = np.random.choice(len(x_train_seq), size=100, replace=False) explainer = shap.KernelExplainer(rnn_predict, x_train_seq[sampled_idx].reshape(100, -1)) shap_values = explainer.shap_values(x_test_seq[:100].reshape(100, -1), nsamples=100)
我运行了代码来绘制一个特征重要性的图表。
# Update feature names for SHAP feature_names = [f'{original_feat}_t{t}' for t in range(time_step) for original_feat in X.columns] # Plot the SHAP values shap.summary_plot(shap_values, x_test_seq[:100].reshape(100, -1), feature_names=feature_names, max_display=len(feature_names), show=False) # Adjust layout and set figure size plt.subplots_adjust(left=0.12, bottom=0.1, right=0.9, top=0.9) plt.gcf().set_size_inches(7.5, 14) plt.tight_layout() plt.savefig("regressor-rnn feature-importance.png") plt.show()
下面是输出结果。
最具影响力的变量是那些包含最新信息的变量,而影响力较小的变量则是包含最旧信息的变量。
这就像说,句子中最近说出的词对整个句子的意义最大。
尽管这对我们人类来说可能不太合逻辑,但对于机器学习模型来说可能是正确的。
正如前一篇文章所述,我们不能仅依赖特征重要性图来做出判断,因为我使用了KernelExplainer而不是推荐的DeepExplainer,后者我在尝试使用时遇到了很多错误。
同样如前一篇文章所述,拥有一个回归模型来预测下一个收盘价或开盘价,并不像拥有一个分类器那样实用,分类器可以告诉我们它认为市场在下一个时间段会朝哪个方向走。让我们构建一个RNN分类器模型来帮助我们完成这项任务。
训练RNN用于分类问题
我们可以遵循之前为回归器编写代码时采用的类似过程,但需要做一些修改;首先,我们需要为分类问题创建目标变量。
Python:
Y = [] target_open = data["TARGET_OPEN"] target_close = data["TARGET_CLOSE"] for i in range(len(target_open)): if target_close[i] > target_open[i]: # if the candle closed above where it opened thats a buy signal Y.append(1) else: #otherwise it is a sell signal Y.append(0) Y = np.array(Y) #converting this array to NumPy classes_in_y = np.unique(Y) # obtaining classes present in the target variable for the sake of setting the number of outputs in the RNN
然后,我们必须在序列创建后立即对目标变量进行独热编码(one-hot-encode),就像在构建回归模型时所讨论的那样。
from tensorflow.keras.utils import to_categorical
y_train_encoded = to_categorical(y_train_seq)
y_test_encoded = to_categorical(y_test_seq)
print(f"One hot encoded\n\ny_train {y_train_encoded.shape}\ny_test {y_test_encoded.shape}")
输出
One hot encoded y_train (693, 2) y_test (293, 2)
最后,我们可以创建RNN分类器模型并进行训练了。
cls_model = Sequential() cls_model.add(Input(shape=(time_step, x_train.shape[1]))) # input layer cls_model.add(SimpleRNN(50, activation='relu')) cls_model.add(Dense(50, activation='relu')) cls_model.add(Dense(units=len(classes_in_y), activation='sigmoid', name='outputs')) adam_optimizer = Adam(learning_rate = 0.001) cls_model.compile(optimizer=adam_optimizer, loss='binary_crossentropy') # Compile the model cls_model.summary() early_stopping = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True) history = cls_model.fit(x_train_seq, y_train_encoded, epochs=100, batch_size=64, verbose=1, validation_data=(x_test_seq, y_test_encoded), callbacks=[early_stopping])
对于分类器RNN模型,我在网络的最后一层使用了sigmoid激活函数。最后一层的神经元(单元)数量必须与目标变量(Y)中存在的类别数量相匹配,在这种情况下,我们将有两个单元。
Model: "sequential_1" ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ simple_rnn_1 (SimpleRNN) │ (None, 50) │ 3,050 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (Dense) │ (None, 50) │ 2,550 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ outputs (Dense) │ (None, 2) │ 102 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
在训练过程中,RNN分类器模型经过6个训练周期(epochs)就足够收敛了。
Epoch 1/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 2s 36ms/step - loss: 0.7242 - val_loss: 0.6872 Epoch 2/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - loss: 0.6883 - val_loss: 0.6891 Epoch 3/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 0.6817 - val_loss: 0.6909 Epoch 4/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 0.6780 - val_loss: 0.6940 Epoch 5/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 0.6743 - val_loss: 0.6974 Epoch 6/100 11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 0.6707 - val_loss: 0.6998
尽管与LightGBM回归器提供的准确率相比,在回归任务上的准确率较低,但RNN分类器模型的准确率却比LightGBM分类器高出3%。
10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 19ms/step Classification Report precision recall f1-score support 0 0.53 0.27 0.36 137 1 0.55 0.79 0.65 156 accuracy 0.55 293 macro avg 0.54 0.53 0.50 293 weighted avg 0.54 0.55 0.51 293
混淆矩阵热力图
将循环神经网络模型保存为ONNX格式
既然我们已经有了分类器的RNN模型,我们就可以将其保存为MetaTrader 5能够理解的ONNX格式。
与Scikit-learn模型不同,保存像RNN这样的Keras深度学习模型并不简单直接。对于RNN来说,Pipeline(管道)也不是一个轻松的解决方案。
正如在《克服ONNX挑战》一文中所述,我们可以在收集数据后立即在MQL5中对数据进行缩放,或者我们可以在Python中保存我们的缩放器,并使用MQL5的预处理库在MQL5中加载它。
保存模型
import tf2onnx # Convert the Keras model to ONNX spec = (tf.TensorSpec((None, time_step, x_train.shape[1]), tf.float16, name="input"),) cls_model.output_names=['output'] onnx_model, _ = tf2onnx.convert.from_keras(cls_model, input_signature=spec, opset=13) # Save the ONNX model to a file with open("rnn.EURUSD.D1.onnx", "wb") as f: f.write(onnx_model.SerializeToString())
保存标准化缩放器参数
# Save the mean and scale parameters to binary files scaler.mean_.tofile("standard_scaler_mean.bin") scaler.scale_.tofile("standard_scaler_scale.bin")
通过保存均值和标准差(它们是标准化缩放器的主要组成部分),我们可以确信已经成功保存了标准化缩放器。
循环神经网络(RNN)EA
在我们的EA内部,首先要做的是将ONNX格式的RNN模型和标准化缩放器的二进制文件作为资源文件添加到我们的EA中。
MQL5 | RNN timeseries forecasting.mq5
#resource "\\Files\\rnn.EURUSD.D1.onnx" as uchar onnx_model[]; //rnn model in onnx format #resource "\\Files\\standard_scaler_mean.bin" as double standardization_mean[]; #resource "\\Files\\standard_scaler_scale.bin" as double standardization_std[];
然后,我们可以加载用于加载ONNX格式的RNN模型和标准化缩放器的库。
MQL5
#include <MALE5\Recurrent Neural Networks(RNNs)\RNN.mqh> CRNN rnn; #include <MALE5\preprocessing.mqh> StandardizationScaler *scaler;
在OnInit函数中。
vector classes_in_data_ = {0,1}; //we have to assign the classes manually | it is very important that their order is preserved as they can be seen in python code, HINT: They are usually in ascending order //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize ONNX model if (!rnn.Init(onnx_model)) return INIT_FAILED; //--- Initializing the scaler with values loaded from binary files scaler = new StandardizationScaler(standardization_mean, standardization_std); //--- Initializing the CTrade library for executing trades m_trade.SetExpertMagicNumber(magic_number); m_trade.SetDeviationInPoints(slippage); m_trade.SetMarginMode(); m_trade.SetTypeFillingBySymbol(Symbol()); lotsize = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); //--- Initializing the indicators ma_handle = iMA(Symbol(),timeframe,30,0,MODE_SMA,PRICE_WEIGHTED); //The Moving averaege for 30 days stddev_handle = iStdDev(Symbol(), timeframe, 7,0,MODE_SMA,PRICE_WEIGHTED); //The standard deviation for 7 days return(INIT_SUCCEEDED); }
在我们可以将模型部署到OnTick函数中进行实时交易之前,我们必须收集数据,这与我们收集训练数据的方式类似。但是,这次我们必须避免在训练过程中丢弃的特征。
记住! 我们训练模型时仅使用了10个特征(自变量)。
让我们创建一个名为GetInputData的函数,用于仅收集那10个自变量。
matrix GetInputData(int bars, int start_bar=1) { vector open(bars), high(bars), low(bars), close(bars), ma(bars), stddev(bars), dayofmonth(bars), dayofweek(bars), dayofyear(bars), month(bars); //--- Getting OHLC values open.CopyRates(Symbol(), timeframe, COPY_RATES_OPEN, start_bar, bars); high.CopyRates(Symbol(), timeframe, COPY_RATES_HIGH, start_bar, bars); low.CopyRates(Symbol(), timeframe, COPY_RATES_LOW, start_bar, bars); close.CopyRates(Symbol(), timeframe, COPY_RATES_CLOSE, start_bar, bars); vector time_vector; time_vector.CopyRates(Symbol(), timeframe, COPY_RATES_TIME, start_bar, bars); //--- ma.CopyIndicatorBuffer(ma_handle, 0, start_bar, bars); //getting moving avg values stddev.CopyIndicatorBuffer(stddev_handle, 0, start_bar, bars); //getting standard deviation values string time = ""; for (int i=0; i<bars; i++) //Extracting time features { time = (string)datetime(time_vector[i]); //converting the data from seconds to date then to string TimeToStruct((datetime)StringToTime(time), date_time_struct); //convering the string time to date then assigning them to a structure dayofmonth[i] = date_time_struct.day; dayofweek[i] = date_time_struct.day_of_week; dayofyear[i] = date_time_struct.day_of_year; month[i] = date_time_struct.mon; } matrix data(bars, 10); //we have 10 inputs from rnn | this value is fixed //--- adding the features into a data matrix data.Col(open, 0); data.Col(high, 1); data.Col(low, 2); data.Col(close, 3); data.Col(ma, 4); data.Col(stddev, 5); data.Col(dayofmonth, 6); data.Col(dayofweek, 7); data.Col(dayofyear, 8); data.Col(month, 9); return data; }
最后,我们部署RNN模型来为我们简单的策略提供交易信号。
void OnTick() { //--- if (NewBar()) //Trade at the opening of a new candle { matrix input_data_matrix = GetInputData(rnn_time_step); input_data_matrix = scaler.transform(input_data_matrix); //applying StandardSCaler to the input data int signal = rnn.predict_bin(input_data_matrix, classes_in_data_); //getting trade signal from the RNN model Comment("Signal==",signal); //--- MqlTick ticks; SymbolInfoTick(Symbol(), ticks); if (signal==1) //if the signal is bullish { if (!PosExists(POSITION_TYPE_BUY)) //There are no buy positions { if (!m_trade.Buy(lotsize, Symbol(), ticks.ask, ticks.bid-stoploss*Point(), ticks.ask+takeprofit*Point())) //Open a buy trade printf("Failed to open a buy position err=%d",GetLastError()); } } else if (signal==0) //Bearish signal { if (!PosExists(POSITION_TYPE_SELL)) //There are no Sell positions if (!m_trade.Sell(lotsize, Symbol(), ticks.bid, ticks.ask+stoploss*Point(), ticks.bid-takeprofit*Point())) //open a sell trade printf("Failed to open a sell position err=%d",GetLastError()); } else //There was an error return; } }
在策略测试器上测试RNN EA
在制定了交易策略之后,让我们在策略测试器中进行测试。我使用的是与LightGBM模型相同的止损和止盈值,包括测试器的设置。
input group "rnn"; input uint rnn_time_step = 7; //this value must be the same as the one used during training in a python script input ENUM_TIMEFRAMES timeframe = PERIOD_D1; input int magic_number = 1945; input int slippage = 50; input int stoploss = 500; input int takeprofit = 700;
策略测试器设置:
该智能交易系统(EA)在561次交易中,有44.56%的交易是盈利的。
根据当前的止损和止盈值,可以公平地说,在时间序列预测方面,LightGBM模型的表现优于简单的RNN模型,因为LightGBM模型的净利润为572美元,而RNN模型的净利润仅为100美元。
我进行了一次优化,以找到最佳的止损和止盈值,其中最佳值之一为止损1000点,止盈700点。
使用简单RNN进行时间序列预测的优势
- 它们能够处理序列数据
简单循环神经网络(RNNs)被设计用来处理序列数据,非常适合处理数据点顺序很重要的任务,如时间序列预测、语言建模和语音识别。 - 它们在不同时间步上共享参数
它们在不同时间步上共享参数参数共享使得模型在参数数量上更加高效,尤其是与那些独立处理每个时间步的模型相比。 - 它们能够捕获时间依赖性
它们能够捕获随时间变化的依赖性,这对于理解序列数据中的上下文至关重要。它们可以有效地建模短期时间依赖性。 - 序列长度灵活
简单RNNs能够处理可变长度的序列,这使得它们对于不同类型的序列数据输入具有灵活性。 - 易于使用和实现
简单RNN的架构相对容易实现。这种简单性有助于理解序列建模的基本概念。
最后的思考
本文将为您提供对简单循环神经网络(RNN)的深入了解,以及如何在MQL5编程语言中部署它。在整篇文章中,为了加深您对基于时间序列和非时间序列模型的时间序列预测的理解,我经常将RNN模型的结果与我们在本系列前一篇文章中构建的LightGBM模型的结果进行比较。
从很多方面来看,这种比较是不公平的,因为这两种模型在结构和预测方式上有着根本的不同。因此,由本文作者或读者在阅读时得出的任何结论都应不予考虑。
值得一提的是,与LightGBM模型相比,RNN模型输入的数据并不相同。在本文中,我们去除了一些滞后变量,这些变量是开盘价、最高价、最低价和收盘价之间差异值的滞后值(DIFF_LAG1_OPEN、DIFF_LAG1_HIGH、DIFF_LAG1_LOW和DIFF_LAG1_CLOSE)。
对于RNN来说,我们本可以使用非滞后值,让它自动检测这些值的滞后性,但我们选择完全不包含它们,因为原始数据集中就没有这些值。
此致敬礼。
在GitHub的这个仓库中,本系列文章将追踪机器学习模型的发展情况,并深入讨论更多内容。
附件表格
文件名 | 文件类型 | 说明和用法 |
---|---|---|
RNN timeseries forecasting.mq5 | EA | 用于在MetaTrader 5中加载RNN ONNX模型并测试最终交易策略的EA。 |
rnn.EURUSD.D1.onnx | ONNX | ONNX格式的RNN模型。 |
standard_scaler_mean.bin standard_scaler_scale.bin | 二进制文件 | 用于标准化缩放器的二进制文件 |
preprocessing.mqh | 一个包含(库)文件 | 包含标准化缩放器的库 |
RNN.mqh | 一个包含(库)文件 | 用于加载和部署ONNX模型的库 |
rnns-for-forex-forecasting-tutorial.ipynb | Python脚本/Jupyter笔记本 | 包含本文中讨论过的所有python代码 |
资料来源与参考文献
- 循环神经网络图解指南:理解其直觉(https://www.youtube.com/watch?v=LHXXI4-IEns)
- 循环神经网络 - 第9集(深度学习简化版)(https://youtu.be/_aCuOwF1ZjU)
- 循环神经网络(https://stanford.edu/~shervine/teaching/cs-230/cheatsheet-recurrent-neural-networks#)
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15114



