神经网络变得轻松(第二部分):网络训练和测试·综合运用
「回放历史K线喂给神经网」
这段循环把指定数量的 Historical Bars 逐根回放,用过去价格差训练一个小神经网络,顺便在图表左上角打印训练进度。外汇与贵金属杠杆高、滑点随机,回测吻合不代表实盘概率等同,务必先在 MT5 策略测试器跑样本外数据。 外层 for 从倒数第 HistoryBars+ 1 根 bar 往前扫,遇到 i==0 就置 add_loop=true,表示下一 tick 要追加新样本。内层先拼一段 StringFormat 文本,包含 Era 计数、误差 dError、未定义占比 dUndefine、预测命中率 dForecast 以及网络近期平均误差 Net.getRecentAverageError(),再用 Comment() 直接覆盖图表注释。 当 i 落在可用区间且 Time.GetData(i) 晚于已学习时间戳、前一根信号不是 -2 时,代码用 i+1 与 i+2 的高低点比较判定局部 sell/buy:若高点两连降且当前更低判 sell,低点两连抬且当前更高判 buy。判定结果转成 1 / -1 / 0 送入 TempData 做 backProp 反向传播;若上一次信号已明确,命中则 dForecast 按 smoothing 因子上调、否则下调,同时压缩 dUndefine。 真正构造输入向量在内层 b 循环:以 r = i + HistoryBars 为窗口起点,越界就 continue;对每个 bar_t 取 open 做基准,把 Close/High/Low 与 open 的差依次 Add 进 TempData。若 open 为 EMPTY_VALUE 或任意 Add 失败,则这次样本作废。
for(class="type">int i=(class="type">int)(bars-MathMax(HistoryBars,class="num">0)-class="num">1); i>=class="num">0 && !stop; i--) { if(i==class="num">0) add_loop=true; class="type">class="kw">string s=StringFormat("Study -> Era %d -> %.2f -> Undefine %.2f%% foracast %.2f%%\n %d of %d -> %.2f%% \nError %.2f\n%s -> %.2f",count,dError,dUndefine,dForecast,bars-i+class="num">1,bars,(class="type">class="kw">double)(bars-i+class="num">1.0)/bars*class="num">100,Net.getRecentAverageError(),EnumToString(DoubleToSignal(dPrevSignal)),dPrevSignal); Comment(s); if(i<(class="type">int)(bars-MathMax(HistoryBars,class="num">0)-class="num">1) && i>class="num">1 && Time.GetData(i)>dtStudied && dPrevSignal!=-class="num">2) { TempData.Clear(); class="type">bool sell=(High.GetData(i+class="num">2)<High.GetData(i+class="num">1) && High.GetData(i)<High.GetData(i+class="num">1)); class="type">bool buy=(Low.GetData(i+class="num">2)<Low.GetData(i+class="num">1) && Low.GetData(i)<Low.GetData(i+class="num">1)); TempData.Add(buy && !sell ? class="num">1 : !buy && sell ? -class="num">1 : class="num">0); Net.backProp(TempData); if(DoubleToSignal(dPrevSignal)!=Undefine) { if(DoubleToSignal(dPrevSignal)==DoubleToSignal(TempData.At(class="num">0))) dForecast+=(class="num">100-dForecast)/Net.recentAverageSmoothingFactor; else dForecast-=dForecast/Net.recentAverageSmoothingFactor; dUndefine-=dUndefine/Net.recentAverageSmoothingFactor; } else { if(sell || buy) dUndefine+=(class="num">100-dUndefine)/Net.recentAverageSmoothingFactor; } } TempData.Clear(); class="type">int r=i+(class="type">int)HistoryBars; if(r>bars) class="kw">continue; class=class="str">"cmt">//--- for(class="type">int b=class="num">0; b<(class="type">int)HistoryBars; b++) { class="type">int bar_t=r+b; class="type">class="kw">double open=Open.GetData(bar_t); TimeToStruct(Time.GetData(bar_t),sTime); if(open==EMPTY_VALUE || !TempData.Add(Close.GetData(bar_t)-open) || !TempData.Add(High.GetData(bar_t)-open) || !TempData.Add(Low.GetData(bar_t)-open) ||
◍ 训练循环里的退出条件与信号绘制
上面这段把神经网络在 MT5 历史数据上的训练收口逻辑摊开了。核心是一个 do-while:只要「上一根信号仍为 Undefine 且预测概率不大于 70」或者「平均误差没压到 0.1 以下、且与上轮误差差值的绝对值不小于 0.01」就继续跑,直到手动停止或达标。
TempData 每根 bar 塞了 12 个字段——成交量(除以1000)、月/时/星期几、RSI、CCI、ATR、MACD 主线与信号线,所以判断 TempData.Total() < (int)HistoryBars*12 就是确认数据没凑满设定长度就 break。
i<200 那段才是肉眼可见的落点:信号转成 Undefine 就删掉对应时间的对象,否则在 High/Low 位置画对象。你可以直接把 if(i<200) 改成更大的数,看前 200 根之外的神经网信号长什么样。
calcOutputGradients 里把目标值夹在 [-1,1],delta 为零时梯度直接置 0,避免无谓反向传播。外汇与贵金属波动剧烈,这类模型输出仅作概率参考,实盘前务必用策略测试器跑足样本。
if(!TempData.Add(Volumes.Main(bar_t)/class="num">1000) || !TempData.Add(sTime.mon) || !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(RSI.Main(bar_t)) || !TempData.Add(CCI.Main(bar_t)) || !TempData.Add(ATR.Main(bar_t)) || !TempData.Add(MACD.Main(bar_t)) || !TempData.Add(MACD.Signal(bar_t))) class="kw">break; } if(TempData.Total()<(class="type">int)HistoryBars*class="num">12) class="kw">break; Net.feedForward(TempData); Net.getResults(TempData); dPrevSignal=TempData[class="num">0]; bar_time=Time.GetData(i); if(i<class="num">200) { if(DoubleToSignal(dPrevSignal)==Undefine) DeleteObject(bar_time); else DrawObject(bar_time,dPrevSignal,High.GetData(i),Low.GetData(i)); } stop=IsStopped(); } if(add_loop) count++; if(!stop) { dError=Net.getRecentAverageError(); if(add_loop) { Net.Save(FileName+".nnw",dError,dUndefine,dForecast,dtStudied,class="kw">false); printf("Era %d -> error %.2f %% forecast %.2f",count,dError,dForecast); } } } class="kw">while((!(DoubleToSignal(dPrevSignal)!=Undefine || dForecast>class="num">70) || !(dError<class="num">0.1 && MathAbs(dError-prev_er)<class="num">0.01 && MathAbs(dUndefine-prev_up)<class="num">0.1 && MathAbs(dForecast-prev_for)<class="num">0.1)) && !stop); if(count>class="num">0) { dtStudied=bar_time; } } class="type">void CNeuron::calcOutputGradients(class="type">class="kw">double targetVals) { class="type">class="kw">double delta=(targetVals>class="num">1 ? class="num">1 : targetVals<-class="num">1 ? -class="num">1 : targetVals)-outputVal; gradient=(delta!=class="num">0 ? delta*CNeuron::activationFunctionDerivative(targetVals) : class="num">0); }
H1 上 EURUSD 的两类网络实测对照
在 MT5 的 H1 周期对 EURUSD 跑了神经网络训练测试,输入窗口取 20 根烛条,回看最近两年数据。为做对照,在同一终端并排挂了两个 EA:一个用回归网络(分形结构、输出层 1 神经元),另一个用分类网络(Fractal_2、输出层 3 神经元)。 首期训练耗了 12432 根柱线、实机用时 2 小时 20 分,两 EA 命中率都只略超 6%,且结果强烈受初始随机权重影响,这种抖动在外汇模型里属于高风险信号,别拿首期当结论。 跑满 35 期后差距才拉开:回归网络根均方误差 0.68、命中率 12.68%、未识别分形 20.22%;分类网络对应为 0.78、11.22%、24.65%。回归变体在误差与识别率上略占优,但两者仍属同一量级。 实测说明两类组织在训练耗时和预测精度上结果相近,但都吃算力和时间。贵金属与外汇品种叠加此类重训练成本,实盘前务必用策略测试器先跑通本地回测再谈部署。
「随包附带的代码与文件清单」
这一篇里给出的程序都集中在 Experts\NeuroNet_DNG\ 和 Files\ 两个目录下。智能交易系统有两只:Fractal.mq5 用回归神经网络(输出层 1 个神经元)做分形预测,Fractal_2.mq5 用分类神经网络(输出层 3 个神经元)做高低点分类;NeuroNet.mqh 是构建感知器的类库,整体规模在 500 行左右,适合直接读源码改参数。
Files 里的 Fractal 与 Fractal_2 两个目录分别存了两类网络的测试截图,打开 MT5 按路径放好就能在策略测试器里复现。有读者反馈编译 Fractal_2.mq5 时报错,把 NeuroNet.mqh 里 void feedForward(const CArrayObj *&prevLayer); 改成 void feedForward(const CArrayObj *prevLayer); 即可通过,作者已确认这种改法可用。
另有个值得注意的点:回归版 Fractal.mq5 的标签逻辑里,测试计数被当成 Era 数使用,导致图表上不只标最近 200 根,而是把相关蜡烛全打上标。若你跑回归网络发现标签铺满图表,先去查这个计数变量,外汇与贵金属波动剧烈,误标会干扰入场判断,属高风险环境。