基于通用 MLP 逼近器的EA·综合运用
(3/3)·不碰权重不写优化器,用现成MLP把指标转成信号并塞进EA的完整走通记录
「把参数边界喂给优化器」
这段逻辑在做一件事:先给每个待优化维度设定搜索边界与步长,再选一种优化算法把种群跑完。rangeMax 设为 5.0、rangeMin 为 -5.0、rangeStep 为 0.01,意味着该维度在 [-5.0, 5.0] 区间内以 0.01 为粒度遍历,共 1001 个候选刻度。 C_AO *ao = SelectAO(OptimizerSelect_P) 拿到算法对象后,把 popSize 赋给 ao.params[0].val 再 SetParams(),不设置就会掉回默认种群规模。随后 Init(rangeMin, rangeMax, rangeStep, epochCount) 把边界和迭代轮数一并灌入。 主循环按 epochCount 轮推进:每轮先 ao.Moving() 走一代,再对种群中每个解算一次 TargetFunction(输入权重、行情、RSI、Stochastic 与可交易时段标记),最后 ao.Revision() 据目标函数值刷新种群。跑完用 Print 输出算法名与最佳结果 ao.fB,把最优权重拷进 Weights 并 delete ao 释放内存。 TargetFunction 内部先取 bars = ArraySize(rates),然后初始化一堆归零变量:wCNT、max、min、signal、profit、allProfit、allLoss、dealsNumb、sells、buys、posType。这些是后续统计信号方向与盈亏归一化的底座,还没开始算,但变量结构已经决定了它按笔数而非单根 K 线记账。
rangeMax [i] = class="num">5.0; rangeMin [i] = -class="num">5.0; rangeStep [i] = class="num">0.01; } class=class="str">"cmt">//---------------------------------------------------------------------------- C_AO *ao = SelectAO(OptimizerSelect_P); class=class="str">"cmt">// Select an optimization algorithm ao.params [class="num">0].val = popSize; class=class="str">"cmt">// Assigning population size.... ao.SetParams(); class=class="str">"cmt">//... (optional, then class="kw">default population size will be used) ao.Init(rangeMin, rangeMax, rangeStep, epochCount); class=class="str">"cmt">// Initialize the algorithm with given boundaries and number of epochs class=class="str">"cmt">// Main loop by number of epochs for (class="type">int epochCNT = class="num">1; epochCNT <= epochCount; epochCNT++) { ao.Moving(); class=class="str">"cmt">// Execute one epoch of the optimization algorithm class=class="str">"cmt">// Calculate the value of the objective function for each solution in the population for (class="type">int set = class="num">0; set < ArraySize(ao.a); set++) { ao.a [set].f = TargetFunction(ao.a [set].c, rates, rsi, sto, truTradeTime); class=class="str">"cmt">//FF.CalcFunc(ao.a [set].c); //ObjectiveFunction(ao.a [set].c); // Apply the objective function to each solution } ao.Revision(); class=class="str">"cmt">// Update the population based on the results of the objective function } class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// Output the algorithm name, best result and number of function runs Print(ao.GetName(), ", best result: ", ao.fB); ArrayCopy(Weights, ao.cB); class="kw">delete ao; class=class="str">"cmt">// Release the memory occupied by the algorithm object class="kw">return true; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">class="kw">double TargetFunction(class="type">class="kw">double &weights [], class="type">MqlRates &rates [], class="type">class="kw">double &rsi [], class="type">class="kw">double &sto [], class="type">bool &truTradeTime []) { class="type">int bars = ArraySize(rates); class=class="str">"cmt">// Initialize variables to normalize data class="type">int wCNT = class="num">0; class="type">class="kw">double max = class="num">0.0; class="type">class="kw">double min = class="num">0.0; class="type">int signal = class="num">0; class="type">class="kw">double profit = class="num">0.0; class="type">class="kw">double allProfit = class="num">0.0; class="type">class="kw">double allLoss = class="num">0.0; class="type">int dealsNumb = class="num">0; class="type">int sells = class="num">0; class="type">int buys = class="num">0; class="type">int posType = class="num">0;
◍ 回测循环里怎么喂数据给神经网络
这段逻辑跑在 MT5 的历史遍历里,核心是把每个非交易时段的 K 线窗口压成神经网络输入,再反推多空信号。注意 truTradeTime[h] 为 false 时才进分析分支,意味着真实成交时段被刻意跳过,回测只评估「闭市窗口」的形态概率,外汇与贵金属杠杆品种这么做高风险,容易漏掉流动性突变。 先看持仓清算:若已有持仓且当前是非交易时间,就用 rates[h].open 平仓,profit 计算里固定扣了 0.00003 的点差/手续费近似。盈利累进 allProfit,亏损取负累进 allLoss,同时按 posType 记 buys/sells 次数——这一步没用复合收益,只是单边差分,复制时别以为它在算净值曲线。 窗口归一化是重点。内层 b 循环在 BarsAnalysis_P 根 K 线里找 high/low 的 max/min,随后 U.Scale 把每根的高、低、开、收以及 sto、rsi 全部线性映射到 [-1,1]。以 max=-DBL_MAX、min=DBL_MAX 初始化,若 BarsAnalysis_P=20,单根贡献 6 个输入,窗口共 120 维喂给 NN.ANN。 信号生成只看 Outs[0] 一个输出神经元:大于 SigThr_P 给 +1(买),小于 -SigThr_P 给 -1(卖)。反向持仓才触发平仓,比如 posType==1 且 signal==-1 时以 rates[h].open 清算,再扣 0.00003。想验证就改 SigThr_P,阈值拉大信号变少、回测交易次数倾向下降。
class="type">class="kw">double posOpPrice = class="num">0.0; class="type">class="kw">double posClPrice = class="num">0.0; class=class="str">"cmt">// Run through history for (class="type">int h = BarsAnalysis_P; h < bars - class="num">1; h++) { if (!truTradeTime [h]) { if (posType != class="num">0) { posClPrice = rates [h].open; profit = (posClPrice - posOpPrice) * signal - class="num">0.00003; if (profit > class="num">0.0) allProfit += profit; else allLoss += -profit; if (posType == class="num">1) buys++; else sells++; allProfit += profit; posType = class="num">0; } class="kw">continue; } max = -DBL_MAX; class=class="str">"cmt">// Initial value for maximum min = DBL_MAX; class=class="str">"cmt">// Initial value for minimum class=class="str">"cmt">// Find the maximum and minimum among high and low for (class="type">int b = class="num">1; b <= BarsAnalysis_P; b++) { if (rates [h - b].high > max) max = rates [h - b].high; class=class="str">"cmt">// Update maximum if (rates [h - b].low < min) min = rates [h - b].low; class=class="str">"cmt">// Update minimum } class=class="str">"cmt">// Normalization of input data for neural network wCNT = class="num">0; for (class="type">int b = BarsAnalysis_P; b >= class="num">1; b--) { Inputs [wCNT] = U.Scale(rates [h - b].high, min, max, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing high Inputs [wCNT] = U.Scale(rates [h - b].low, min, max, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing low Inputs [wCNT] = U.Scale(rates [h - b].open, min, max, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing open Inputs [wCNT] = U.Scale(rates [h - b].close, min, max, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing close Inputs [wCNT] = U.Scale(sto [h - b], class="num">0, class="num">100, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing Stochastic Inputs [wCNT] = U.Scale(rsi [h - b], class="num">0, class="num">100, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing RSI } class=class="str">"cmt">// Convert data from Inputs to Outs NN.ANN(Inputs, weights, Outs); class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// Generate a trading signal based on the output of a neural network signal = class="num">0; if (Outs [class="num">0] > SigThr_P) signal = class="num">1; class=class="str">"cmt">// Buy signal if (Outs [class="num">0] < -SigThr_P) signal = -class="num">1; class=class="str">"cmt">// Sell signal if ((posType == class="num">1 && signal == -class="num">1) || (posType == -class="num">1 && signal == class="num">1)) { posClPrice = rates [h].open; profit = (posClPrice - posOpPrice) * signal - class="num">0.00003;
多空失衡时给评分狠狠打折
这段逻辑干的事很直白:把每笔平仓的盈利累加到 allProfit,亏损取反累加到 allLoss;同时用 buys 和 sells 分别记多空开仓次数,最后 dealsNumb 就是总成交笔数。 关键在末尾的失衡过滤:若 sells 或 buys 有一边为 0,直接返回 -DBL_MAX 表示样本无效;若空单比多单、或多单比空单超过 1.5 倍,系数 ko 被压到 0.001。 最终返回值是 (allProfit / (allLoss + DBL_EPSILON)) * dealsNumb,但注意上面代码里 ko 算出来后根本没乘进返回式——这要么是无用变量,要么是作者漏了一步。你在 MT5 里跑前最好确认:是想用 ko 惩罚失衡样本,还是单纯靠 1.5 倍阈值拦截。外汇与贵金属杠杆高,这类统计偏差会让回测曲线虚胖,实盘可能倾向失效。
if (profit > class="num">0.0) allProfit += profit; else allLoss += -profit; if (posType == class="num">1) buys++; else sells++; allProfit += profit; posType = class="num">0; } if (posType == class="num">0 && signal != class="num">0) { posType = signal; posOpPrice = rates [h].open; } } dealsNumb = buys + sells; class="type">class="kw">double ko = class="num">1.0; if (sells == class="num">0 || buys == class="num">0) class="kw">return -DBL_MAX; if (sells / buys > class="num">1.5 || buys / sells > class="num">1.5) ko = class="num">0.001; class="kw">return (allProfit / (allLoss + DBL_EPSILON)) * dealsNumb; } class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
「EA 里的仓位盈亏与信号反手逻辑」
这段 MLP 思路的 EA 并未把目标函数归一化喂给网络做误差,而是直接在回测循环里用信号和开盘价算每根 K 线的浮动盈亏,顺带处理非交易时段的跳过的仓位。它本质是个信息参考型框架,实盘下单前你仍要自己补经纪商所需的开仓校验。 代码里佣金被硬编码成 0.00003 每笔,属于作者示例环境假设,换贵金属或交叉盘时这个点必须改。评论区有用户指出 TargetFunction 里对利润做了双重求和,作者自己也确认删掉末尾重复行才对,说明直接拖进 MT5 编译跑,数字可能偏大。 非交易时间由 truTradeTime 数组控制,若对应时段为假就走重置分支:把 posType 置 0、统计 buys/sells 后 continue。反手条件只看 posType 与 signal 异号,例如持多(1)遇到 signal -1 就按当前开盘价平仓并记盈亏,不区分点差和滑点。 外汇与贵金属波动剧烈,这类简化训练框架只解决「网络停滞」和可视化的入门问题;真要上净值型账户,得按自己经纪商规则改写头寸处理,否则概率上更可能踩坑而不是稳过。
if (!truTradeTime [h]) { if (posType != class="num">0) { class=class="str">"cmt">// 如果有空位 posClPrice = rates [h].open; class=class="str">"cmt">// 以栏位开盘价收盘 profit = (posClPrice - posOpPrice) * signal - class="num">0.00003; class=class="str">"cmt">// 委员会 if (profit > class="num">0.0) allProfit += profit; else allLoss += -profit; if (posType == class="num">1) buys++; else sells++; allProfit += profit; posType = class="num">0; class=class="str">"cmt">// 重置位置 } class="kw">continue; class=class="str">"cmt">// 跳过非交易时间 } if ((posType == class="num">1 && signal == -class="num">1) || (posType == -class="num">1 && signal == class="num">1)) { posClPrice = rates [h].open; class=class="str">"cmt">// 以当前条形图的开盘价收盘 profit = (posClPrice - posOpPrice) * signal - class="num">0.00003; class=class="str">"cmt">// 利润计算 class=class="str">"cmt">// 损益核算 if (profit > class="num">0.0) allProfit += profit; else allLoss += -profit;
◍ 记住这一条就够了
上面这段回测收口逻辑,核心就卡在买卖次数失衡的惩罚系数上:当 sells/buys 或 buys/sells 超过 1.5 倍时,ko 直接压到 0.001,等于把这类单边刷单的策略评分打废。 净收益计算用的是 (allProfit / (allLoss + DBL_EPSILON)) * dealsNumb,每次平仓扣 0.00003 点差成本,开 MT5 把这段塞进你的 OnTester 里跑一遍,看 EURUSD 的 H1 回测会不会因为某段只做多就直接被 ko 干掉。 外汇和贵金属杠杆高,这种统计过滤只是帮你在策略层面排雷,实盘仍可能连续止损,别当成免死金牌。
class=class="str">"cmt">// 交易统计 if (posType == class="num">1) buys++; else sells++; allProfit += profit; posType = class="num">0; class=class="str">"cmt">// 关闭位置 } class="type">class="kw">double ko = class="num">1.0; if (sells == class="num">0 || buys == class="num">0) class="kw">return -DBL_MAX; if (sells / buys > class="num">1.5 || buys / sells > class="num">1.5) ko = class="num">0.001; class="kw">return (allProfit / (allLoss + DBL_EPSILON)) * dealsNumb; allProfit += profit; if (!truTradeTime [h]) { if (posType != class="num">0) { class=class="str">"cmt">// 如果有空位 posClPrice = rates [h].open; class=class="str">"cmt">// 以栏位开盘价收盘 profit = (posClPrice - posOpPrice) * signal - class="num">0.00003; class=class="str">"cmt">// 委员会 if (profit > class="num">0.0) allProfit += profit; else allLoss += -profit; if (posType == class="num">1) buys++; else sells++; class=class="str">"cmt">//allProfit += profit; posType = class="num">0; class=class="str">"cmt">// 重置位置 } class="kw">continue; class=class="str">"cmt">// 跳过非交易时间 } if ((posType == class="num">1 && signal == -class="num">1) || (posType == -class="num">1 && signal == class="num">1)) { posClPrice = rates [h].open; class=class="str">"cmt">// 以当前条形图的开盘价收盘 profit = (posClPrice - posOpPrice) * signal - class="num">0.00003; class=class="str">"cmt">// 利润计算 class=class="str">"cmt">// 损益核算 if (profit > class="num">0.0) allProfit += profit; else allLoss += -profit; class=class="str">"cmt">// 交易统计 if (posType == class="num">1) buys++; else sells++; class=class="str">"cmt">//allProfit += profit; posType = class="num">0; class=class="str">"cmt">// 关闭位置 }