神经网络在交易中的实际应用。 是时候进行实践了·进阶篇
(2/3)· 承接基础篇,手把手在 MT5 把训练好的神经网络跑成自动交易系统
「EA加开仓限制后的回测表现与NNM中断排查」
在 EA 里补上开仓限制后重新跑测试,用最高可视化速度、即时报价模式进行,得到的结果与参考曲线接近。我倾向认为即便把测试速度降下来,旧版测试也可能靠拢参考值,你可以自己降速验证一遍。 NNM 模块终止的根因找到了:若神经网络没训练完,或交易 EA 启动前就点了模块,Input_mat.txt 不会生成,点“开始”后 NNM 无响应。早期我加了警告窗并从内部计时器强退,后来删了错误逻辑只留警告,弹窗点“确定”即可继续。在测试模式里暴露这问题比实盘才发现要好得多。 MT5 还有个坑:用策略数据写文件时,生成柱线数会大于图表显示数量,导致指标“超长”,但不影响测试质量,多余柱线只在测试中被取用。MT4 没这现象,其 1_MT4 指标无偏移。 下面这段初始化代码演示了如何从公共 CSV 读 EURUSD 测试数据填进输入数组,文件缺失会弹 Alert,是排查数据供给的第一道哨。
class="macro">#include<Trade\Trade.mqh> class=class="str">"cmt">//--- An object for performing trading operations CTrade trade; 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">int BarMax; class="type">int BarMin; class="type">int handleInput; class="type">int handleInputPodkach; class="type">int handleBar; class="type">int Con; class="type">int Bar; class="type">class="kw">double DibMax; class="type">class="kw">double DibMin; class="type">class="kw">double in[class="num">32]; class="type">int Order01; class="type">int Order1; class="type">class="kw">ulong TicketBuy1; class="type">class="kw">ulong TicketSell0; class="type">bool send1; class="type">bool send0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- handleInputPodkach=FileOpen("EURUSDTestPodkach.csv",FILE_READ|FILE_CSV|FILE_ANSI|FILE_COMMON,";"); if(handleInputPodkach==INVALID_HANDLE) Alert("No file EURUSDTestPodkach.csv"); in[class="num">0]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">1]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">2]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">3]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">4]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">5]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">6]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">7]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">8]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">9]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">10]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">11]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">12]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
◍ 从文件读参后怎么把交易内核钉死
初始化尾部这段把 13 到 29 号输入数组元素一口气塞满:每个值都来自同一个文件句柄 handleInputPodkach,用 FileReadString 读成字符串再 StringToDouble 转双精度,最后做 1/值-1 的换算。也就是说这 17 个参数本质是同一串外部文本的倒数偏移,改文件不改代码就能换一整组系数。 读完后立刻 FileClose(handleInputPodkach) 收掉句柄,避免 MT5 报文件句柄泄漏。紧接着把交易对象的身份与容差钉死:MagicNumber 写死 123456 用于区分本 EA 订单,deviation 设 10 点允许滑点,SetTypeFilling 用 ORDER_FILLING_RETURN 以适配大部分经纪商回退模式。 SetAsyncMode(true) 让下单走 OrderSendAsync,主线程不被确认阻塞, tick 内做高频判断时更不容易掉帧。最后 return(INIT_SUCCEEDED) 告诉终端初始化过关。 OnDeinit 里只干一件事:把 handleInput 与 handleBar 两个句柄关掉,防止退出时文件占用。外汇与贵金属杠杆高,异步下单若服务端拒绝填充会静默回退,实盘前务必在策略测试器用 10 点偏差跑一遍验证回报路径。
in[class="num">13]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">14]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">15]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">16]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">17]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">18]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">19]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">20]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">21]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">22]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">23]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">24]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">25]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">26]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">27]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">28]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; in[class="num">29]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1; FileClose(handleInputPodkach); class=class="str">"cmt">//--- Setting MagicNumber to identify EA&class="macro">#x27;s orders class="type">int MagicNumber=class="num">123456; trade.SetExpertMagicNumber(MagicNumber); class=class="str">"cmt">//--- Setting allowable slippage in points for buying/selling class="type">int deviation=class="num">10; trade.SetDeviationInPoints(deviation); class=class="str">"cmt">//--- order filling mode, use the mode that is allowed by the server trade.SetTypeFilling(ORDER_FILLING_RETURN); class=class="str">"cmt">//--- The function to be used for trading: true - OrderSendAsync(), class="kw">false - OrderSend() trade.SetAsyncMode(true); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- FileClose(handleInput); FileClose(handleBar); }
用 OnTick 抓取日线开盘与小时极值差
这段逻辑跑在 MT5 的 OnTick 事件里,核心是在每天 01:00(服务器时区)重置参考基准,随后用小时 K 线去刷新日内的高点和低点边界。外汇与贵金属市场高杠杆、跳空频繁,这类边界值只作概率参考,不等于方向确认。 代码先用 TimeToStruct 拿当前时间结构,hour==1 时把 DibMax 设为上一根 H1 高点;hour>0 后只要上一根 H1 高点突破 DibMax 且当前 H1 开盘时间大于 1,就往 in[20]~in[24] 写五组日线开盘价与 H1/D1 低高差的数值。 下半段对称处理低点:hour==1 重置 DibMin,hour>0 后若上一根 H1 低点更低且时间条件满足,则填充 in[25]~in[27] 三组差价。开 MT5 新建 EA 把这段粘进 OnTick,把 in 数组打印到日志,能直接看到每天 01:00 后边界差如何随价格波动更新。
class="type">void OnTick() { class="type">MqlDateTime stm; TimeToStruct(TimeCurrent(),stm); if(stm.hour==class="num">1) DibMax=iHigh(NULL,PERIOD_H1,class="num">1); if(stm.hour>class="num">0) { if(iHigh(NULL,PERIOD_H1,class="num">1)>DibMax && iTime(NULL,PERIOD_H1,class="num">0)>class="num">1) { in[class="num">20]=iOpen(NULL,PERIOD_D1,class="num">0)-iLow(NULL,PERIOD_H1,class="num">1); in[class="num">21]=iHigh(NULL,PERIOD_H1,class="num">1)-iOpen(NULL,PERIOD_D1,class="num">0); in[class="num">22]=iHigh(NULL,PERIOD_D1,class="num">1)-iLow(NULL,PERIOD_D1,class="num">1); in[class="num">23]=iHigh(NULL,PERIOD_D1,class="num">1)-iOpen(NULL,PERIOD_H1,class="num">0); in[class="num">24]=iOpen(NULL,PERIOD_H1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">1); } } if(iHigh(NULL,PERIOD_H1,class="num">1)>DibMax) DibMax=iHigh(NULL,PERIOD_H1,class="num">1); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ if(stm.hour==class="num">1) DibMin=iLow(NULL,PERIOD_H1,class="num">1); if(stm.hour>class="num">0) { if(iLow(NULL,PERIOD_H1,class="num">1)<DibMin && iTime(NULL,PERIOD_H1,class="num">0)>class="num">1) { in[class="num">25]=iOpen(NULL,PERIOD_D1,class="num">0)-iLow(NULL,PERIOD_H1,class="num">1); in[class="num">26]=iHigh(NULL,PERIOD_H1,class="num">1)-iOpen(NULL,PERIOD_D1,class="num">0); in[class="num">27]=iHigh(NULL,PERIOD_D1,class="num">1)-iLow(NULL,PERIOD_D1,class="num">1);
「把跨周期价差塞进模型输入数组」
这段逻辑干的事很直接:用日线前一根的高低点与小时线当前开盘价做差,把跨周期相对位置量化成 in[28]、in[29] 这类特征。in[28] 取昨日最高减当前 H1 开盘,in[29] 取当前 H1 开盘减昨日最低,本质是在量小时线开盘相对昨日波幅区间的偏移。 当小时线前一根低点刷新 DibMin 时同步更新该变量,随后 in[30]、in[31] 再算一遍同样的日高减 H1 开、H1 开减日低,等于把最近两根 H1 棒的相对位置都留了备份。 每天 0 点(stm.hour==0 且 Bar 未超总棒数)触发一次数组前移:索引 19~10 挪到 9~0,29~20 挪到 19~10,老数据被覆盖,相当于滚动窗口只留最近约 20 根 H1 的跨周期差。 最后 FileOpen 打开公共目录下的 Input_mat.txt,以分号分隔写出 1/(in[i]+1) 的归一化值——加 1 是为避免零或负差导致发散,倒数压缩把大价差也拉回 0~1 区间,方便外部矩阵读取。外汇与贵金属波动跳空频繁,这类跨周期差在重大数据后会瞬时放大,回测时务必用真实点差验证。
in[class="num">28]=iHigh(NULL,PERIOD_D1,class="num">1)-iOpen(NULL,PERIOD_H1,class="num">0); in[class="num">29]=iOpen(NULL,PERIOD_H1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">1); } } if(iLow(NULL,PERIOD_H1,class="num">1)<DibMin) DibMin=iLow(NULL,PERIOD_H1,class="num">1); in[class="num">30]=iHigh(NULL,PERIOD_D1,class="num">1)-iOpen(NULL,PERIOD_H1,class="num">0); in[class="num">31]=iOpen(NULL,PERIOD_H1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">1); if(Bar<Bars(NULL,PERIOD_H1)&& stm.hour==class="num">0) { for(class="type">int i=class="num">19; i>=class="num">10; i--) { in[i-class="num">10]=in[i]; } for(class="type">int i=class="num">29; i>=class="num">20; i--) { in[i-class="num">10]=in[i]; } } handleInput=FileOpen("Input_mat.txt",FILE_TXT|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ|FILE_COMMON,";"); FileWrite(handleInput, class="num">1/(in[class="num">0]+class="num">1),class="num">1/(in[class="num">1]+class="num">1),class="num">1/(in[class="num">2]+class="num">1),class="num">1/(in[class="num">3]+class="num">1),class="num">1/(in[class="num">4]+class="num">1),class="num">1/(in[class="num">5]+class="num">1),class="num">1/(in[class="num">6]+class="num">1),class="num">1/(in[class="num">7]+class="num">1),class="num">1/(in[class="num">8]+class="num">1),class="num">1/(in[class="num">9]+class="num">1),class="num">1/(in[class="num">10]+class="num">1),class="num">1/(in[class="num">11]+class="num">1),class="num">1/(in[class="num">12]+class="num">1),class="num">1/(in[class="num">13]+class="num">1),class="num">1/(in[class="num">14]+class="num">1),class="num">1/(in[class="num">15]+class="num">1),
◍ 输入权重落盘与订单触发闸门
这段逻辑先把 16~31 号输入节点的倒数权重写回内存数组,随后用 FileClose 关掉输入文件,再开 Bar.txt 以分号分割把当前小时 stm.hour 落盘,相当于给外部调度脚本留一个时间锚点。 接着从 Open1.txt 读入一行字符串并转成整数 Order1,这个值决定后续是偏多还是偏空。注意 FILE_COMMON 标志让文件落在终端公共目录,多 EA 共享时才不会各写各的。 下面的 digits、point、Ask、Bid 全部用 SymbolInfo 系列实时取,SL/TP 按 LossBuy、ProfitBuy 等参数乘 point 后 NormalizeDouble 到品种精度;若对应止损止盈参数为 0,则直接把挂单价置 0 表示不设。 触发闸门看三条件:Order1 为 0 或 1、Bar 小于 H1 周期 Bars 总数、Con 为 0,分别把 send0 / send1 置真。外汇与贵金属杠杆高,这套自动发单逻辑在滑点扩大时可能漏单或错向,上 MT5 前先把 LossBuy 等参数打印一遍核对。
class="num">1/(in[class="num">16]+class="num">1),class="num">1/(in[class="num">17]+class="num">1),class="num">1/(in[class="num">18]+class="num">1),class="num">1/(in[class="num">19]+class="num">1),class="num">1/(in[class="num">20]+class="num">1),class="num">1/(in[class="num">21]+class="num">1),class="num">1/(in[class="num">22]+class="num">1),class="num">1/(in[class="num">23]+class="num">1),class="num">1/(in[class="num">24]+class="num">1),class="num">1/(in[class="num">25]+class="num">1),class="num">1/(in[class="num">26]+class="num">1),class="num">1/(in[class="num">27]+class="num">1),class="num">1/(in[class="num">28]+class="num">1),class="num">1/(in[class="num">29]+class="num">1),class="num">1/(in[class="num">30]+class="num">1),class="num">1/(in[class="num">31]+class="num">1)); FileClose(handleInput); handleBar=FileOpen("Bar.txt",FILE_TXT|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ|FILE_COMMON,";"); FileWrite(handleBar,stm.hour); FileClose(handleBar); Order01=FileOpen("Open1.txt",FILE_CSV|FILE_READ|FILE_ANSI|FILE_SHARE_READ|FILE_COMMON," "); Order1=StringToInteger(FileReadString(Order01)); FileClose(Order01); class="type">int digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS); class="type">class="kw">double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT); class="type">class="kw">double PriceAsk=SymbolInfoDouble(_Symbol,SYMBOL_ASK); class="type">class="kw">double PriceBid=SymbolInfoDouble(_Symbol,SYMBOL_BID); class="type">class="kw">double SL1=NormalizeDouble(PriceBid-LossBuy*point,digits); class="type">class="kw">double TP1=NormalizeDouble(PriceAsk+ProfitBuy*point,digits); class="type">class="kw">double SL0=NormalizeDouble(PriceAsk+LossSell*point,digits); class="type">class="kw">double TP0=NormalizeDouble(PriceBid-ProfitSell*point,digits); if(Bar<Bars(NULL,PERIOD_H1)) Con=class="num">0; Comment(Order1," ",Con); if(LossBuy==class="num">0) SL1=class="num">0; if(ProfitBuy==class="num">0) TP1=class="num">0; if(LossSell==class="num">0) SL0=class="num">0; if(ProfitSell==class="num">0) TP0=class="num">0; if(Order1==class="num">0 && Bar<Bars(NULL,PERIOD_H1) && Con==class="num">0) send0=true; if(Order1==class="num">1 && Bar<Bars(NULL,PERIOD_H1) && Con==class="num">0) send1=true; class=class="str">"cmt">//---------Buy0
H1 时段内的双向触发与收尾兜底
这段逻辑把买单和卖单都绑在 H1 新柱生成那一刻:只有 Bar==Bars(NULL,PERIOD_H1) 才放行,避免一根柱子内重复发单。买侧限定在小时数 15–20 之间,且第 1 根 H1 低点低于第 2 根才允许开多;卖侧则卡在 11–14 点,第 1 根高点高于第 2 根才开空,属于典型的欧盘早段与美盘初段分时段处理。 平仓条件对称:买仓在 Order1 翻 0、且新低出现时平;卖仓在 Order1 翻 1、且新高出现时平。Con 计数每次循环自增,配合 Bar 刷新记录,用来过滤历史柱上的误触发。 代码末尾还有一段脱离 OnTick 的兜底:若 send1 为 false 且 Order1==1 直接 trade.Buy(1),等于在条件漏判时强行补一单,实盘里可能带来非预期敞口,建议先在策略测试器里把这段注释掉跑一遍。外汇与贵金属杠杆高,这类强制补单逻辑若未经回测直接上实盘,亏损概率会明显放大。 输入参数里 Period=5 与 H1~H4、Loss/Profit 系列都留空给外部调,说明止损止盈是在别处算好传进来的。想验证就开 MT5 把 SL1/TP1、SL0/TP0 接上 iATR 或固定点数,看 15–20 点买、11–14 点卖的胜率分布。
if(send1==class="kw">false && Bar==Bars(NULL,PERIOD_H1) && Order1==class="num">1 && Con>=class="num">1 && iLow(NULL,PERIOD_H1,class="num">1)<iLow(NULL,PERIOD_H1,class="num">2) && stm.hour>class="num">15 && stm.hour<class="num">20) { send1=trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,class="num">1,PriceAsk,SL1,TP1); TicketBuy1 = trade.ResultDeal(); } if(send1==true && Bar==Bars(NULL,PERIOD_H1) && Order1==class="num">0 && Con>=class="num">1 && iHigh(NULL,PERIOD_H1,class="num">1)>iHigh(NULL,PERIOD_H1,class="num">2)) { trade.PositionClose(TicketBuy1); send1=class="kw">false; } class=class="str">"cmt">//---------Sell0 if(send0==class="kw">false && Bar==Bars(NULL,PERIOD_H1) && Order1==class="num">0 && Con>=class="num">1 && iHigh(NULL,PERIOD_H1,class="num">1)>iHigh(NULL,PERIOD_H1,class="num">2) && stm.hour>class="num">11 && stm.hour<class="num">14) { send0=trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,class="num">1,PriceBid,SL0,TP0); TicketSell0 = trade.ResultDeal(); } if(send0==true && Bar==Bars(NULL,PERIOD_H1) && Order1==class="num">1 && Con>=class="num">1 && iLow(NULL,PERIOD_H1,class="num">1)<iLow(NULL,PERIOD_H1,class="num">2)) { trade.PositionClose(TicketSell0); send0=class="kw">false; } class=class="str">"cmt">//----------------------------------------------------------------------- Bar=Bars(NULL,PERIOD_H1); Con++; } class=class="str">"cmt">//------------------------------------------------------------------------ if(send1==class="kw">false && Order1==class="num">1) { send1=trade.Buy(class="num">1); TicketBuy1 = trade.ResultDeal(); } if(send1==true && Order1==class="num">0) { trade.PositionClose(TicketBuy1); send1=class="kw">false; } 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[]; class="type">bool send1; class="type">bool send0; class="type">int h=class="num">4; class="type">int k; class="type">int K; class="type">int bars;