神经网络在交易中的实际应用 (第二部分). 计算机视觉(基础篇)
◍ 用 CNN 读价格图:MT5 里的计算机视觉入口
MQL5 官方在 2021-03-16 发布过一篇关于神经网络实盘应用的系列文,其中第二部分专门讲计算机视觉(CV)在 MetaTrader 5 中的落地。原文发布后阅读量约 3349、收藏 26,说明这类把截图当特征喂给模型的做法,在零售交易者里已经有真实需求。 传统指标依赖数值序列,而 CV 思路直接把 K 线图当图像处理:卷积层提取局部形态,池化层压缩空间维度,全连接层输出多分类概率。外汇与贵金属杠杆高、滑点随机,任何信号都只是概率倾向,实盘前必须用历史 Tick 回测验证泛化能力。 在 MT5 里跑这套,核心是先通过 ChartScreenShot 把图表存成位图,再用 ONNX 模型推理。下面这段是官方示例里截取的特征预处理骨架,注意输入尺寸必须和训练时严格一致,否则输出会漂移。
「把十几个指标压成图再喂给CNN」
给交易用的神经网络做训练数据,第一道坎就是输入怎么组织。假设你同时挂了十几个指标,每个指标再往历史里回算一定深度,平铺开来很容易突破100个特征值,极端情况下还会更多。 这种高维平铺对全连接网络很不友好,不仅参数量爆炸,指标间的局部空间关系也丢了。 一个能直接在MT5里验证的思路是:把指标阵列当成多通道图像,交给卷积神经网络(CNN)去做分类或识别。CNN本就是为图像局部特征提取设计的,用它替我们压缩和抽取信息,比手工降维更省事。
CNN 逐层尺寸怎么塌下来的
这套用于图像输入的卷积网络,输入固定为 449×449 像素单图。第一卷积层用 3×3 核扫出 96 张 447×447 特征图,紧接着 2×2 子采样把空间压到 223×223,特征图数不变。 第二卷积层降到 32 张 221×221 特征图,再经 2×2 子采样成 110×110;第三卷积层继续收至 16 张 108×108,子采样后剩 54×54(原文图里没画这一级)。 末端接 64 个神经元的全连接层,最后 1 个神经元做二分类输出。整套结构不需要手搭,只要把输入尺寸、各层特征图数和核大小这几个主参数设对,网络就会自动按这个尺寸链生成。 验证时可以拿 449 平方输入跑一遍前向,看每一级输出维度是否严格落到 447→223→221→110→108→54,差一个像素就说明卷积或池化参数配错了。
◍ 用MT5脚本切出CNN训练图集
做图像分类之前先定用途:本实验不碰轴枢点截图那种没实战价值的样本,只聚焦两类——价格上涨或触日低判为买入类,下跌或触日高判为卖出类。趋势方向上出新极值、以及日高日低反转瞬间,才是截图触发点。 图表先套 CNN.tpl 模板,丢进 Terminal 目录下 MQL5\Profiles\Templates,图表属性里把文字设成 White,再按自己显卡能力定最佳像素。作者实机用的是 449x449。 脚本分两种模式:Train 造训练集,Test 造连续每小时截图供网络出响应。以 Train 跑完,Files 目录落了 6125 张图,其中 Buy 类 3139、Sell 类 2986。桌面建 CNN\Train、Val、Test,Train/Buy 与 Train/Sell 各塞满,再从尾部剔 30% 进 Val——最终 Train\Buy 2198、Val\Buy 941、Train\Sell 2091、Val\Sell 895。 切到 Test 模式再跑,Files 里生成 12558 张连续每小时图,不分组直接整包挪到 CNN\Test\Resp;网络自己会给每张图打「贴合买入或卖出条件」的概率。EURUSDDate 文件留在原目录,另行移进 CNN 根目录。 提一句 MT5 的坑:用策略测试器里的 EA 去备图更稳,但 MT5 不支持在测试器内调 ChartScreenshot,所以只能脚本现截。外汇与贵金属自带高杠杆风险,图集只是分类输入,不预示任何方向盈亏。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CNNet.mq5 | class=class="str">"cmt">//| Copyright class="num">2021, Andrey Dibrov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2021, 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">#class="kw">property script_show_inputs class="kw">input class="type">class="kw">string Date="class="num">2017.01.class="num">02 class="num">00:class="num">00"; class="kw">input class="type">class="kw">string DateOut="class="num">2018.12.class="num">13 class="num">23:class="num">00"; class="kw">input class="type">class="kw">string DateTest="class="num">2019.01.class="num">02 class="num">00:class="num">00"; class="kw">input class="type">class="kw">string Dataset="Train"; class="type">class="kw">string Date1; class="type">int count,countB,countS; class="type">int day; class="type">class="kw">double DibMin; class="type">class="kw">double DibMax; class="type">int HandleDate; class="type">long WIDTH; class="type">long HEIGHT; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- class="type">MqlDateTime stm; ChartSetInteger(class="num">0,CHART_SHIFT,class="kw">false); ChartSetInteger(class="num">0,CHART_AUTOSCROLL,class="kw">false); ChartSetInteger(class="num">0,CHART_SHOW_OBJECT_DESCR,class="kw">false); WIDTH=ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS); ChartSetInteger(class="num">0,CHART_SHOW_PRICE_SCALE,class="kw">false); if(Dataset=="Test") {
「用脚本把历史K线批量截成训练图」
把 MT5 图表按 H1 一根根回拨并截图,是做价格行为数据集最直接的办法。下面这段逻辑先开一个以品种名+Date.csv 命名的 ANSI 文件,用来记录每帧对应的时间,再跳到指定日期的 K 线位置。
| HandleDate=FileOpen(Symbol()+"Date.csv",FILE_CSV | FILE_READ | FILE_WRITE | FILE_ANSI,";"); |
|---|
ChartNavigate(0,CHART_END,-(iBarShift(NULL,PERIOD_H1,StringToTime(DateTest)))); Sleep(1000); for(int i=iBarShift(NULL,PERIOD_H1,StringToTime(DateTest)); i>0; i--) { Date1=TimeToString(iTime(NULL,PERIOD_H1,i)); if(DateTest<=Date1) { if(ChartNavigate(0,CHART_END,-i)) { Sleep(20); if(ChartScreenShot(0, (string)count + ".png", (int)WIDTH, (int)WIDTH, ALIGN_LEFT)) { FileWrite(HandleDate,TimeToString(iTime(NULL,PERIOD_H1,i))); count++; Sleep(20); } } } } 第 1 行:FileOpen 以 CSV 读写+ANSI 打开「品种Date.csv」,分隔符用分号。第 2 行:ChartNavigate 把 0 号图表滚到 DateTest 那根 H1 棒(iBarShift 算偏移)。第 4 行起:从那根棒往前遍历到最新,每帧 ChartNavigate 定位、Sleep(20) 防卡顿,ChartScreenShot 按 WIDTH×WIDTH 截左对齐 PNG,成功就写时间进 CSV 并 count++。 如果是 Train 模式,则改为从 Date 扫到 DateOut:ChartNavigate(0,CHART_END,-iBarShift(NULL,PERIOD_H1,StringToTime(Date))); 之后 for 里用 i>=iBarShift(...DateOut) 控制区间,并用 TimeToStruct 取 stm.day 判断换日。实盘品种波动剧烈、杠杆风险高,截图仅供离线模型训练,不等于任何方向预判。 开 MT5 把 WIDTH 设成 224 或 256,接自己的 DateTest 跑一遍,数一下 count 最终落在几千张,就能知道这段循环在你机器上的吞吐。
HandleDate=FileOpen(Symbol()+"Date.csv",FILE_CSV|FILE_READ|FILE_WRITE|FILE_ANSI,";"); ChartNavigate(class="num">0,CHART_END,-(iBarShift(NULL,PERIOD_H1,StringToTime(DateTest)))); Sleep(class="num">1000); for(class="type">int i=iBarShift(NULL,PERIOD_H1,StringToTime(DateTest)); i>class="num">0; i--) { Date1=TimeToString(iTime(NULL,PERIOD_H1,i)); if(DateTest<=Date1) { if(ChartNavigate(class="num">0,CHART_END,-i)) { Sleep(class="num">20); if(ChartScreenShot(class="num">0, (class="type">class="kw">string)count + ".png", (class="type">int)WIDTH, (class="type">int)WIDTH, ALIGN_LEFT)) { FileWrite(HandleDate,TimeToString(iTime(NULL,PERIOD_H1,i))); count++; Sleep(class="num">20); } } } } if(Dataset=="Train") { ChartNavigate(class="num">0,CHART_END,-iBarShift(NULL,PERIOD_H1,StringToTime(Date))); Sleep(class="num">1000); for(class="type">int i=iBarShift(NULL,PERIOD_H1,StringToTime(Date)); i>=iBarShift(NULL,PERIOD_H1,StringToTime(DateOut)); i--) { TimeToStruct(iTime(NULL,PERIOD_H1,i),stm); Date1=TimeToString(iTime(NULL,PERIOD_H1,i)); if(DateOut>=Date1 && Date<=Date1) { if(ChartNavigate(class="num">0,CHART_END,-i)) { Sleep(class="num">20); if(day != stm.day) {