基于通用 MLP 逼近器的EA·综合运用
🧠

基于通用 MLP 逼近器的EA·综合运用

(3/3)·不碰权重不写优化器,用现成MLP把指标转成信号并塞进EA的完整走通记录

案例拆解新手友好 第 3/3 篇
不少交易者一听神经网络就默认要自己调参、跑优化器,结果卡在环境配置上从不动手。其实MLP当纯函数变换器用时,根本不需要训练权重也能接进EA。这篇就拆一个跳过SGD/ADAM、直接集成的可行做法。

「把参数边界喂给优化器」

这段逻辑在做一件事:先给每个待优化维度设定搜索边界与步长,再选一种优化算法把种群跑完。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 线记账。

MQL5 / C++
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,阈值拉大信号变少、回测交易次数倾向下降。

MQL5 / C++
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 倍阈值拦截。外汇与贵金属杠杆高,这类统计偏差会让回测曲线虚胖,实盘可能倾向失效。

MQL5 / C++
  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 就按当前开盘价平仓并记盈亏,不区分点差和滑点。 外汇与贵金属波动剧烈,这类简化训练框架只解决「网络停滞」和可视化的入门问题;真要上净值型账户,得按自己经纪商规则改写头寸处理,否则概率上更可能踩坑而不是稳过。

MQL5 / C++
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 干掉。 外汇和贵金属杠杆高,这种统计过滤只是帮你在策略层面排雷,实盘仍可能连续止损,别当成免死金牌。

MQL5 / C++
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">// 关闭位置
}
把信号变换交给小布盯盘看
这些把指标塞进MLP再吐信号的诊断逻辑,小布盯盘的AIGC已内置,打开对应品种页即可看到盘整与趋势的概率切分,你只管决定跟不跟。

常见问题

固定架构与激活函数后,它只是确定性非线性变换,输出区间由权重初值决定;用历史样本回测分布、设阈值过滤即可,外汇贵金属仍属高风险,信号仅作参考。
不同指标量纲差太大,直接喂进去会让某一维主导变换;先做滚动标准化或缩放到相近区间,再接网络层更稳。
可以,内置的AIGC模块已封装好通用MLP变换与品种状态识别,不必自己写MQL5集成代码,切换品种就能看信号倾向。
因为标签可靠性难预先验证,网络往往拟合了人为假设而非真实结构;本篇改用无优化器的变换思路来绕开该问题。
任何多维指标向量都行,例如波动率加动量加价差,输出可映射为策略选择概率,具体组合看品种特性。