数据科学和机器学习(第 30 部分):预测股票市场的幂对、卷积神经网络(CNN)、和递归神经网络(RNN)·进阶篇
「把 CNN 特征流改造成 RNN 时态输入」
上一步抽出的特征不能直接喂给普通全连接层,要先过 RNN 这一关。RNN 在扫这些特征时会顺着时间轴走,把前后步的依赖关系带进权重里,这和本系列之前用 CNN 那套只吃空间局部性的逻辑不一样。 关键改动在 Flatten 层:原 CNN 架构里它把 3D 张量压成 2D,而 RNN、LSTM、GRU 全都要(批次大小、时间步长、特征)这种 3D 输入。所以动手搭网络时,Flatten 必须删掉,否则维度对不上直接报 shape 错。 下面这段 Keras 式堆叠就是替换后的主干:先 1D 池化降采样,接 50 单元的 SimpleRNN,两次 Dropout 0.5 防过拟合,末端 softmax 出信号分类。外汇与贵金属行情高波动、高杠杆,此类模型仅给出概率倾向,实盘前务必在 MT5 历史数据上回测。
model.add(MaxPooling1D(pool_size=class="num">2)) model.add(SimpleRNN(class="num">50, activation=&class="macro">#x27;relu&class="macro">#x27;)) model.add(Dropout(class="num">0.5)) model.add(Dense(class="num">50, activation=&class="macro">#x27;relu&class="macro">#x27;)) model.add(Dropout(class="num">0.5)) model.add(Dense(units=len(np.unique(y)), activation=&class="macro">#x27;softmax&class="macro">#x27;)) # Softmax for binary classification(class="num">1 buy, class="num">0 sell signal)
把 RNN+CNN 模型塞进 MT5 实盘前要做的事
模型在 Python 端跑完 14 个 epoch 后,测试集准确率停在 54%,分类报告里类 0(卖出)precision 0.70 / recall 0.40,类 1(买入)precision 0.45 / recall 0.74,样本量 195。这个水平不算高,但作为简单 RNN 叠加 CNN 的基线已经能区分多空倾向。 训练好后必须存成 ONNX,同时把 StandardScaler 的 mean 和 scale 写成 bin 文件。MQL5 里用 #resource 把 onnx 和 scaler 参数挂成资源,在 OnInit 中初始化 CConvNet 与 StandardizationScaler,预测前先拿 scaler 归一化输入,再丢进 CNN 模型取信号和概率。 特征矩阵 x_data 按 3 列(Open、High、Low)和训练时相同的时间步长排布,类标识须升序(0 卖、1 买)。回测区间取 2020.01.01–2024.09.01,EA 挂在 4 小时图而非日线,因为日线新烛在收市才生成,会漏单;数据仍用 CopyRates 从日线取,不影响决策。该设置下 EA 盈利交易占比约 90%,但外汇/贵金属品种切换时此结果不必然复现,属高风险验证。 下面这段是 Python 训练与导出核心,以及 MQL5 资源挂载片段,逐行看便知衔接点。
model.summary() # Compile the model optimizer = Adam(learning_rate=class="num">0.0001) model.compile(optimizer=optimizer, loss=&class="macro">#x27;binary_crossentropy&class="macro">#x27;, metrics=[&class="macro">#x27;accuracy&class="macro">#x27;]) # Train the model early_stopping = EarlyStopping(monitor=&class="macro">#x27;val_loss&class="macro">#x27;, patience=class="num">10, restore_best_weights=True) history = model.fit(X_train, y_train_encoded, epochs=class="num">1000, batch_size=class="num">16, validation_split=class="num">0.2, callbacks=[early_stopping]) plt.figure(figsize=(class="num">7.5, class="num">6)) plt.plot(history.history[&class="macro">#x27;loss&class="macro">#x27;], label=&class="macro">#x27;Training Loss&class="macro">#x27;) plt.plot(history.history[&class="macro">#x27;val_loss&class="macro">#x27;], label=&class="macro">#x27;Validation Loss&class="macro">#x27;) plt.xlabel(&class="macro">#x27;Epochs&class="macro">#x27;) plt.ylabel(&class="macro">#x27;Loss&class="macro">#x27;) plt.title(&class="macro">#x27;Training Loss Curve&class="macro">#x27;) plt.legend() plt.savefig("training loss curve-rnn-cnn-clf.png") plt.show() # Evaluating the Trained Model y_pred = model.predict(X_test) classes_in_y = np.unique(y) y_pred_binary = classes_in_y[np.argmax(y_pred, axis=class="num">1)] # Confusion Matrix cm = confusion_matrix(y_test, y_pred_binary) sns.heatmap(cm, annot=True, fmt=&class="macro">#x27;d&class="macro">#x27;, cmap=&class="macro">#x27;Blues&class="macro">#x27;) plt.xlabel("Predicted Label") plt.ylabel("True Label") plt.title("Confusion Matrix") plt.savefig("confusion-matrix RNN + CNN.png") print("Classification Report\n", classification_report(y_test, y_pred_binary)) onnx_file_name = "rnn+cnn.TSLA.D1.onnx" spec = (tf.TensorSpec((None, time_step, X_train.shape[class="num">2]), tf.float16, name="class="kw">input"),) model.output_names = [&class="macro">#x27;outputs&class="macro">#x27;] onnx_model, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=class="num">13) with open(onnx_file_name, "wb") as f: f.write(onnx_model.SerializeToString()) scaler.mean_.tofile(f"{onnx_file_name.replace(&class="macro">#x27;.onnx&class="macro">#x27;,&class="macro">#x27;&class="macro">#x27;)}.standard_scaler_mean.bin") scaler.scale_.tofile(f"{onnx_file_name.replace(&class="macro">#x27;.onnx&class="macro">#x27;,&class="macro">#x27;&class="macro">#x27;)}.standard_scaler_scale.bin") class="macro">#include <MALE5\Convolutional Neural Networks(CNNs)\ConvNet.mqh> class="macro">#include <MALE5\preprocessing.mqh> CConvNet cnn; StandardizationScaler *scaler; class="macro">#resource "\Files\rnn+cnn.TSLA.D1.onnx" as class="type">uchar onnx_model[] class="macro">#resource "\Files\rnn+cnn.TSLA.D1.standard_scaler_mean.bin" as class="type">class="kw">double standardization_mean[]
◍ 把CNN信号接进D1实盘前要先过标准化
在 MT5 里加载训练好的 CNN 模型,第一道坎不是预测,而是把盘口数据先过一遍与训练时一致的标准化。代码里用 #resource 把 rnn+cnn.TSLA.D1.standard_scaler_scale.bin 读成 standardization_std[],再 new StandardizationScaler(standardization_mean, standardization_std) 把均值和标准差灌进缩放器,这一步漏掉,模型输入分布偏移,输出信号会直接失真。
新 K 线开盘才触发推理:NewBar() 为真时 CopyRates(Symbol(), PERIOD_D1, 1, time_step, rates) 取最近 time_step=5 根日线,把每根 open/high/low 填进 x_data 这个 5×3 矩阵。注意这里只用了三个价格字段,训练时若含 close 或 vol 就得改列数,否则维度对不上会初始化失败。
填完矩阵后 x_data = scaler.transform(x_data) 做归一,随后 cnn.predict_bin() 出 0/1 信号、cnn.predict_proba() 出各类概率,用 Comment 打到图表上方便肉眼核对。实盘里 signal==1 时先 ClosePos(POSITION_TYPE_SELL) 平空,再检查无多单后 m_trade.Buy(min_lot, ...) 开最小手数多单——外汇与贵金属杠杆高,这类基于历史分布的模型信号仅代表概率倾向,实盘前务必用策略测试器跑样本外验证。
class="macro">#resource "\\Files\\rnn+cnn.TSLA.D1.standard_scaler_scale.bin" as class="type">class="kw">double standardization_std[] class="type">int OnInit() { class=class="str">"cmt">//--- if (!cnn.Init(onnx_model)) class=class="str">"cmt">//Initialize the Convolutional neural network class="kw">return INIT_FAILED; scaler = new StandardizationScaler(standardization_mean, standardization_std); class=class="str">"cmt">//Initialize the saved scaler by populating it with values ... ... class="kw">return (INIT_SUCCEEDED); } if (NewBar()) class=class="str">"cmt">//Trade at the opening of a new candle { CopyRates(Symbol(), PERIOD_D1, class="num">1, time_step, rates); for (class="type">ulong i=class="num">0; i<x_data.Rows(); i++) { x_data[i][class="num">0] = rates[i].open; x_data[i][class="num">1] = rates[i].high; x_data[i][class="num">2] = rates[i].low; } class=class="str">"cmt">//--- x_data = scaler.transform(x_data); class=class="str">"cmt">//Normalize the data class="type">int signal = cnn.predict_bin(x_data, classes_in_data_); class=class="str">"cmt">//getting a trading signal from the RNN model vector probabilities = cnn.predict_proba(x_data); class=class="str">"cmt">//probability for each class Comment("Probability = ",probabilities,"\nSignal = ",signal); class="kw">input class="type">int time_step = class="num">5; class="kw">input class="type">int magic_number = class="num">24092024; class="kw">input class="type">int slippage = class="num">100; class="type">MqlRates rates[]; matrix x_data(time_step, class="num">3); class=class="str">"cmt">//class="num">3 columns for open, high and low vector classes_in_data_ = {class="num">0, class="num">1}; class=class="str">"cmt">//unique target variables as they are in the target variable in your training data class="type">int OldNumBars = class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class="type">class="kw">double min_lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); class="type">MqlTick ticks; SymbolInfoTick(Symbol(), ticks); if (signal==class="num">1) class=class="str">"cmt">//if the signal is bullish { ClosePos(POSITION_TYPE_SELL); class=class="str">"cmt">//close sell trades when the signal is buy if (!PosExists(POSITION_TYPE_BUY)) class=class="str">"cmt">//There are no buy positions { if (!m_trade.Buy(min_lot, Symbol(), ticks.ask, class="num">0 , class="num">0)) class=class="str">"cmt">//Open a buy trade
「空头信号下的平仓与反向开仓逻辑」
当指标返回 signal==0 时,这段逻辑判定为空头信号,先平掉所有多单再决定是否开空。ClosePos(POSITION_TYPE_BUY) 会扫掉账户里全部买入持仓,避免多空对锁或逆势扛单。 紧接着用 PosExists(POSITION_TYPE_SELL) 检查是否已有卖单;若没有,才以 m_trade.Sell(min_lot, Symbol(), ticks.bid, 0, 0) 按当前 bid 价开最小手数空单。两个止损止盈参数传 0,意味着本段不挂硬性止损,实盘中外汇与贵金属波动剧烈,这种写法可能让单笔回撤失控。 若 Sell 返回失败,printf 会把 GetLastError() 的错误码打进日志,方便在 MT5 终端直接排查 4109(交易被禁)或 4756(报价过期)之类问题。建议把这段粘进 EA 的 OnTick 末尾,用欧美 5 分钟图跑一周模拟盘验证信号翻转时的成交表现。
printf("Failed to open a buy position err=%d",GetLastError()); } } else if (signal==class="num">0) class=class="str">"cmt">//Bearish signal { ClosePos(POSITION_TYPE_BUY); class=class="str">"cmt">//close all buy trades when the signal is sell if (!PosExists(POSITION_TYPE_SELL)) class=class="str">"cmt">//There are no Sell positions { if (!m_trade.Sell(min_lot, Symbol(), ticks.bid, class="num">0 , class="num">0)) class=class="str">"cmt">//open a sell trade printf("Failed to open a sell position err=%d",GetLastError()); } } else class=class="str">"cmt">//There was an error class="kw">return;