神经网络在交易中的实际应用 (第二部分). 计算机视觉·进阶篇
(2/3)· 当十几个指标堆出上百条输入,CNN 把图表当图像吃进去也许更轻
◍ 用截图与文件搬运记录高低点突破
这段逻辑在每小时收盘后做一件事:把前一小时创下的新低或新高截进 PNG,并用计数器命名归档。外汇与贵金属波动连续,H1 周期下每天约 24 张图,回看一周就可能累积 120+ 个文件,磁盘占用虽小但命名混乱会拖慢后续分析。 核心动作是先判断 iLow(NULL,PERIOD_H1,i+1) 是否小于 DibMin,若成立则刷新 DibMin、countS 自增并调用 ChartScreenShot 存为 Sell+序号.png;新高分支对称处理 Buy 系列。两个分支互斥执行,意味着一小时若同时吞掉前低和前高,会出两张图。 文件重排那段最容易被忽略:当 day 变更时,代码用 FileCopy 把上一日最后的 Sell/Buy 图互换序号再 FileDelete 旧文件,等于把「最后一张」顺延成「下一日第 0 张」的基底。直接在 MT5 脚本里跑这段,把 WIDTH 设成 500 看终端 Data 文件夹,能确认命名是否如预期滚动。 别把截图当信号源。它只是把价格行为学里的「时段极值突破」可视化留痕,贵金属跳空时 i+1 的 High/Low 可能跨缺口,截图内容与实际成交价偏差偏大,需人工复核。
FileCopy("Sell" + (class="type">class="kw">string)countS + ".png", class="num">0, "Buy" + (class="type">class="kw">string)(countB+class="num">1) + ".png", FILE_REWRITE); FileDelete("Sell" + (class="type">class="kw">string)countS + ".png", class="num">0); FileCopy("Buy" + (class="type">class="kw">string)countB + ".png", class="num">0, "Sell" + (class="type">class="kw">string)(countS+class="num">1) + ".png", FILE_REWRITE); FileDelete("Buy" + (class="type">class="kw">string)countB + ".png", class="num">0); countB ++; countS ++; } day = stm.day; if(stm.hour == class="num">0) { DibMin = iOpen(NULL, PERIOD_H1, i); DibMax = iOpen(NULL, PERIOD_H1, i); } if(iLow(NULL, PERIOD_H1, i+class="num">1) < DibMin) { DibMin = iLow(NULL, PERIOD_H1, i+class="num">1); countS ++; ChartScreenShot(class="num">0, "Sell" + (class="type">class="kw">string)countS + ".png", (class="type">int)WIDTH, (class="type">int)WIDTH, ALIGN_LEFT); } if(iHigh(NULL, PERIOD_H1, i+class="num">1) > DibMax) { DibMax = iHigh(NULL, PERIOD_H1, i+class="num">1); countB ++; ChartScreenShot(class="num">0, "Buy" +(class="type">class="kw">string)countB + ".png", (class="type">int)WIDTH, (class="type">int)WIDTH, ALIGN_LEFT); } Sleep(class="num">20); } } else class="kw">break; } }
搭好GPU环境再喂图给卷积网络
想在 MT5 之外做价格图像的卷积训练,Anaconda + TensorFlow/Keras 是条省心路。若手里有 NVIDIA 显卡,建个独立 conda 环境装 tensorflow-gpu 与 keras-gpu,训练速度会明显不同:同一网络跑 40 个 epoch,纯 CPU 约 13 小时,GPU 上约 1.5 小时,单 epoch 从 20 分钟压到 1–2 分钟。代价是显存卡死网络规模——想堆层就得加显存或砍图像尺寸。 环境命令按顺序走:pip install tensorflow、pip install keras,再 conda create -n PythonGPU python=3.6 tensorflow-gpu 和 conda install -c anaconda keras-gpu,最后补 jupyter、pandas、pillow。CPU 用户同样要装后两个库,只是不用 gpu 后缀。 网络结构本身不复杂:输入 449x449 三通道彩图,首层 96 个 3x3 卷积核接 relu,2x2 最大池化降维;之后 32、16 核各再来一遍卷积+激活+池化。展平后接 64 神经元全连接、Dropout(0.5) 防过拟合,输出层 1 个 sigmoid 神经元做二分类。若把横盘也单分一类,输出改 3 神经元即可。 编译用 binary_crossentropy 配 sgd 优化器,准确率作指标;像素除以 255 归一,用 Keras 生成器从 train/val 目录读图,class_mode='binary' 且 shuffle=False。下面这段是可直接丢进 Train.ipynb 的骨架。 别把显存当无限资源 GPU 提速的前提是网络张量能塞进显存。449x449 彩图加 96 卷积核已经吃紧,真要加层先算显存账单,不然只能退回 CPU 慢熬。外汇与贵金属行情高波动,图像标签错分会带来实盘误判风险,训练集质量比网络深度更关键。
from tensorflow.python.keras.preprocessing.image class="kw">import ImageDataGenerator from tensorflow.python.keras.models class="kw">import Sequential from tensorflow.python.keras.layers class="kw">import Conv2D, MaxPooling2D from tensorflow.python.keras.layers class="kw">import Activation, Dropout, Flatten, Dense from tensorflow.keras.callbacks class="kw">import ModelCheckpoint from tensorflow.keras.models class="kw">import load_model # Directory with data for training train_dir = &class="macro">#x27;train&class="macro">#x27; # Directory with data for validation val_dir = &class="macro">#x27;val&class="macro">#x27; # Image dimensions img_width, img_height = class="num">449, class="num">449 # Image-based tensor dimension for class="kw">input to the neural network # backend Tensorflow, channels_last input_shape = (img_width, img_height, class="num">3) # Number of epochs epochs = class="num">20 # Mini-sample size batch_size = class="num">7 # Number of images for training nb_train_samples = class="num">4289 # Number of images for validation nb_validation_samples = class="num">1836 # Number of images for testing class="macro">#nb_test_samples = class="num">3736 model = Sequential() model.add(Conv2D(class="num">96, (class="num">3, class="num">3), input_shape=input_shape)) model.add(Activation(&class="macro">#x27;relu&class="macro">#x27;)) model.add(MaxPooling2D(pool_size=(class="num">2, class="num">2))) model.add(Conv2D(class="num">32, (class="num">3, class="num">3))) model.add(Activation(&class="macro">#x27;relu&class="macro">#x27;)) model.add(MaxPooling2D(pool_size=(class="num">2, class="num">2))) model.add(Conv2D(class="num">16, (class="num">3, class="num">3))) model.add(Activation(&class="macro">#x27;relu&class="macro">#x27;)) model.add(MaxPooling2D(pool_size=(class="num">2, class="num">2))) model.add(Flatten()) model.add(Dense(class="num">64)) model.add(Activation(&class="macro">#x27;relu&class="macro">#x27;)) model.add(Dropout(class="num">0.5)) model.add(Dense(class="num">1)) model.add(Activation(&class="macro">#x27;sigmoid&class="macro">#x27;)) model.compile(loss=&class="macro">#x27;binary_crossentropy&class="macro">#x27;, optimizer=&class="macro">#x27;sgd&class="macro">#x27;, metrics=[&class="macro">#x27;accuracy&class="macro">#x27;]) datagen = ImageDataGenerator(rescale=class="num">1. / class="num">255)
「按验证集挑第11个epoch的权重」
用 ModelCheckpoint 在每个 epoch 结束后落盘一份 hdf5 权重,训练完 CNN 文件夹下会多出 20 个网络文件,后面靠验证指标反选而不是看训练集甜区。 代码里 callbacks 只挂了保存钩子,fit 时 train_generator 和 val_generator 都设了 shuffle=False,保证样本顺序固定,才能把第 N 个 epoch 的权重和验证曲线对上号。
callbacks = [ModelCheckpoint(&class="macro">#x27;cnn_Open{epoch:1d}.hdf5&class="macro">#x27;)] model.fit( train_generator, steps_per_epoch=nb_train_samples class=class="str">"cmt">// batch_size, epochs=epochs, validation_data=val_generator, validation_steps=nb_validation_samples class=class="str">"cmt">// batch_size, callbacks=callbacks)
callbacks = [ModelCheckpoint(&class="macro">#x27;cnn_Open{epoch:1d}.hdf5&class="macro">#x27;)] model.fit( train_generator, steps_per_epoch=nb_train_samples class=class="str">"cmt">// batch_size, epochs=epochs, validation_data=val_generator, validation_steps=nb_validation_samples class=class="str">"cmt">// batch_size, callbacks=callbacks)
◍ 把CNN响应灌进MT5看交叉信号
CNN在CPU环境下跑完Test.jpynb后,会在CNN目录生成Indicator.csv,里面是12558张测试图对应的网络输出值。把这个文件搬到C:\Users\...\AppData\Roaming\MetaQuotes\Terminal\Common\Files,EURUSD H1图表才能用NWI指标读到神经网络的反应。 实操上用主指示线(红)和简单平均线(DodgerBlue)的交叉来解释网络响应最直观。配合TestCNN专家顾问,可双向优化信号线周期、时间与止损,先在垂直红线前那段训练网络、再对红线后做测试,优化器挑出的高优先级结果往往只是随机正例,不能当必然方向。 下面这段Python负责加载第11 epoch的cnn_Open11.hdf5,用ImageDataGenerator把图归一化到1/255,再flow_from_directory读盘、model.predict吐出indicator并写回csv;MT5侧的NWI.mq5则用FileOpen共享读同一个csv,绑两个DRAW_LINE缓冲。外汇与贵金属杠杆高,这类信号失效概率不低,上实盘前先在策略测试器跑样本外。
from tensorflow.python.keras.preprocessing.image class="kw">import ImageDataGenerator from tensorflow.python.keras.models class="kw">import Sequential from tensorflow.python.keras.layers class="kw">import Conv2D, MaxPooling2D from tensorflow.python.keras.layers class="kw">import Activation, Dropout, Flatten, Dense from tensorflow.keras.callbacks class="kw">import ModelCheckpoint from tensorflow.keras.models class="kw">import load_model class="kw">import pandas as pd predict_dir = &class="macro">#x27;Test&class="macro">#x27; img_width, img_height = class="num">449, class="num">449 nb_predict_samples = class="num">12558 Date=pd.read_csv(&class="macro">#x27;EURUSDDate.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) model=load_model(&class="macro">#x27;cnn_Open11.hdf5&class="macro">#x27;) datagen = ImageDataGenerator(rescale=class="num">1. / class="num">255) predict_generator = datagen.flow_from_directory( predict_dir, target_size=(img_width, img_height), shuffle=False) indicator=model.predict(predict_generator, nb_predict_samples ) print(indicator) Date=pd.DataFrame(Date) Date[&class="macro">#x27;class="num">0&class="macro">#x27;] =indicator Date.to_csv(&class="macro">#x27;Indicator.csv&class="macro">#x27;,index=False, header=False,sep=&class="macro">#x27;;&class="macro">#x27;) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| NWI.mq5 | class=class="str">"cmt">//| Copyright © class="num">2019, Andrey Dibrov. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright © class="num">2019, Andrey Dibrov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">2 class="macro">#class="kw">property indicator_plots class="num">2 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_type2 DRAW_LINE class="macro">#class="kw">property indicator_color1 Red class="macro">#class="kw">property indicator_color2 DodgerBlue class="type">int Handle; class="type">int i; class="type">int h; class="kw">input class="type">int Period=class="num">5; class="type">class="kw">double ExtBuffer[]; class="type">class="kw">double SignBuffer[]; class="type">class="kw">datetime Date1; class="type">class="kw">datetime Date0; class="type">class="kw">string File_Name="Indicator.csv"; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnInit() { SetIndexBuffer(class="num">0,ExtBuffer,INDICATOR_DATA); SetIndexBuffer(class="num">1,SignBuffer,INDICATOR_DATA); IndicatorSetInteger(INDICATOR_DIGITS,class="num">5); Handle=FileOpen(File_Name,FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";"); class=class="str">"cmt">//FileClose(Handle); } class=class="str">"cmt">//+------------------------------------------------------------------+
把外部信号灌进指标的回灌写法
这段逻辑干的事很直接:从公共目录的 CSV 里读时间点和数值,再把它们按 H1 周期对齐到指标缓冲区。外汇与贵金属市场高杠杆、滑点随机,这种回灌只用于离线验证,实盘信号延迟可能让结论偏移。 OnCalculate 里先用 FileReadString 拿到首行时间,iBarShift 在 NULL 品种 PERIOD_H1 上反查出柱索引 i,随后重新以 FILE_COMMON 共享方式打开同一文件,保证多程序读同一份信号不互斥。 循环体每次读一行时间 Date1 和一行数值写进 ExtBuffer[i],再用内层 while 把前 Period-1 根(默认 Period=5,即前 4 根)的 ExtBuffer 累加进 SignBuffer[i],除以 Period 得到平滑值。注意 i 从文件首行对应的柱向下递减,因此越晚的信号贴在越近的柱上。 TestCNN.mq5 那一段暴露了下游用法:它用 CTrade 下单,输入参数含 H1~H4 四组阈值与 LossBuy/ProfitBuy/LossSell/ProfitSell 四组止盈止损点数。也就是说,SignBuffer 算完之后,若其值穿越某 Hn 边界,就可能触发对应方向市价单,盈亏空间完全由这几个 input 点数框定。 在 MT5 里把 Period 从 5 改成 20 重跑,SignBuffer 的斜率会明显变钝,H1~H4 的触发频率可能下降 30%~50%,具体看品种波动。调参前先确认 CSV 时间精度是秒级还是分根级,否则 iBarShift 会偏柱。
class=class="str">"cmt">//| Relative Strength Index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { class="type">MqlDateTime stm; Date0=StringToTime(FileReadString(Handle)); i=iBarShift(NULL,PERIOD_H1,Date0,class="kw">false); Handle=FileOpen(File_Name,FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";"); ArraySetAsSeries(ExtBuffer,true); ArraySetAsSeries(SignBuffer,true); class="kw">while(!FileIsEnding(Handle) && !IsStopped()) { Date1=StringToTime(FileReadString(Handle)); ExtBuffer[i]=StringToDouble(FileReadString(Handle)); h=Period-class="num">1; if(i>=class="num">0) { class="kw">while(h>=class="num">0) { SignBuffer[i]=SignBuffer[i]+ExtBuffer[i+h]; h--; } } SignBuffer[i]=SignBuffer[i]/Period; TimeToStruct(Date1,stm); i--; } FileClose(Handle); class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestCNN.mq5 | class=class="str">"cmt">//| Copyright © class="num">2019, Andrey Dibrov. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright " Copyright © class="num">2019, Andrey Dibrov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class="macro">#include<Trade\Trade.mqh> CTrade trade; class="kw">input class="type">int Period=class="num">5; class="kw">input class="type">int H1; class="kw">input class="type">int H2; class="kw">input class="type">int H3; class="kw">input class="type">int H4; class="kw">input class="type">int LossBuy; class="kw">input class="type">int ProfitBuy; class="kw">input class="type">int LossSell; class="kw">input class="type">int ProfitSell; class="type">class="kw">ulong TicketBuy1; class="type">class="kw">ulong TicketSell0; class="type">class="kw">datetime Count; class="type">class="kw">double Per; class="type">class="kw">double Buf_0[]; class="type">class="kw">double Buf_1[];