您应当知道的 MQL5 向导技术(第 20 部分):符号回归·进阶篇
📘

您应当知道的 MQL5 向导技术(第 20 部分):符号回归·进阶篇

第 2/2 篇

单点交叉的随机切点实现

遗传算法里单点交叉的核心是:先随机挑一个切点,再把两个父代染色体在切点前后互换片段。下面这段逻辑用 rand() 取模来生成切点,保证切点落在 1 到 _length-1 之间,避免切在 0 号位导致完全不交换。 _cross 初始化为 0;当基因长度 _length 大于 1 时,_cross = rand() % (_length - 1) + 1,意味着若有 10 个基因位,切点只会在 1~9 之间出现。 随后两个 for 循环完成拼接:0 到 _cross-1 直接拷贝父代原段,_cross 到结尾则互换来源。ChildA 前半取自 ParentA、后半取自 ParentB,ChildB 正好相反。 在 MT5 策略测试器里把这段代码塞进 EA 的种群繁殖函数,跑 30 代以上,能直观看到切点随机性对后代多样性的影响;外汇与贵金属品种波动大,遗传优化仅降低过拟合概率,实盘仍属高风险。

MQL5 / C++
  class="type">int _cross = class="num">0;
  if(_length > class="num">1)
  {  _cross = rand() % (_length - class="num">1) + class="num">1;
  }
  for(class="type">int c = class="num">0; c < _cross; c++)
  {  ChildA[c] = ParentA[c];
      ChildB[c] = ParentB[c];
  }
  for(class="type">int l = _cross; l < _length; l++)
  {  ChildA[l] = ParentB[l];
      ChildB[l] = ParentA[l];
  }

「把遗传树塞进信号类里」

做自定义信号类,第一步还是给模型备好数据集。这里用的就是前面适应度函数填好的 'XY' 输入矩阵,由 GetBestTree 函数统一调度。矩阵把单维 x 与单维 y 配对——自变量和因变量;多维情形也能把 Y 向量摊成矩阵或更高维结构供树表达式使用。 我们这次只用单维,数据行全是连续收盘价。最近一行里取倒数第二根收盘作 x,当前收盘作 y,填充 XY 矩阵的代码在下面拆解。 适应度评估别默认拍脑袋。虽然选了回归路线,但回归衡量值有好几种,所以把‘用哪种回归指标’做成输入参数去优化更稳;默认先跑常见的均方根误差(RMSE)。 GetBestTree 跑完遗传算法,回吐若干领头树表达式。拿当前收盘当 x 喂进 GetFitness,能吐出下一根收盘的预测 y,同时附带该表达式的适应度——因为输入 Y 向量本就装着目标预测值。 预测价只是方向性提示,绝对值常偏离近期收盘区间,直接喂给 EA 会出问题。必须归一化:标准信号类的多空条件整数输出锁死在 0–100,下面代码就是把预测价转成这个区间的信号。多空函数互为镜像,组装进智能系统参考前文即可。 [CODE] 段里的 GetBestTree 逐行看:void CSignalSR::GetBestTree(matrix &XY, vector &Y, string &BestTree[]) 是函数头,接收数据矩阵、目标向量和最佳树数组;double _best_fit = DBL_MAX 先给最优适应度赋极大值做初值。for(int e = 1 + m_epochs; e >= 1; e--) 从最大树深往下迭代;Spopulation _p 建种群,ArrayResize(_p.population, m_population) 按种群规模扩容器。_e_size = 2 * e 按深度估树大小,内层循环 GetExpressionTree(e, _tree) 随机生树并拷进种群个体。 后面 for(int g=0; g<m_generations; g++) 跑代数:_fitness.Init 按种群数初始化,逐个体 GetFitness 算分;_fitness.Percentile(m_fitness) 取分位阈值筛优胜者进 _s 种群,奇数样本砍一保证配对,_samples==0 就 break 提前停。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">// Get Best Fit</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CSignalSR::GetBestTree(<span class="keyword">matrix</span> &amp;XY, <span class="keyword">vector</span> &amp;Y, <span class="keyword">class="type">class="kw">string</span> &amp;BestTree[])
{&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> _best_fit = <span class="macro">DBL_MAX</span>;
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> e = <span class="number">class="num">1</span> + m_epochs; e &gt;= <span class="number">class="num">1</span>; e--)
&nbsp;&nbsp; {&nbsp;&nbsp;Spopulation _p;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span>(_p.population, m_population);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> _e_size = <span class="number">class="num">2</span> * e;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> p = <span class="number">class="num">0</span>; p &lt; m_population; p++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> _tree[];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; GetExpressionTree(e, _tree);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _e_size = <span class="keyword">class="type">int</span>(_tree.Size());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ArrayResize</span>(_p.population[p].tree, _e_size);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ee = <span class="number">class="num">0</span>; ee &lt; _e_size; ee++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;_p.population[p].tree[ee] = _tree[ee];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> g = <span class="number">class="num">0</span>; g &lt; m_generations; g++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;<span class="keyword">vector</span> _fitness;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _fitness.Init(<span class="keyword">class="type">int</span>(_p.population.Size()));
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> p = <span class="number">class="num">0</span>; p &lt; <span class="keyword">class="type">int</span>(_p.population.Size()); p++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;_fitness[p] = GetFitness(XY, Y, _p.population[p].tree);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> _fit = _fitness.Percentile(m_fitness);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Spopulation _s;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> _samples = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> p = <span class="number">class="num">0</span>; p &lt; <span class="keyword">class="type">int</span>(_p.population.Size()); p++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;<span class="keyword">if</span>(_fitness[p] &lt;= _fit)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;_samples++;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ArrayResize</span>(_s.population, _samples);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ArrayResize</span>(_s.population[_samples - <span class="number">class="num">1</span>].tree, _e_size);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ee = <span class="number">class="num">0</span>; ee &lt; _e_size; ee++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;_s.population[_samples - <span class="number">class="num">1</span>].tree[ee] = _p.population[p].tree[ee];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(_samples % <span class="number">class="num">2</span> == <span class="number">class="num">1</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;_samples--;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span>(_s.population, _samples);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(_samples == <span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;<span class="keyword">class="kw">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

◍ 遗传算子落地与做多信号触发

遗传算法迭代里,交叉和变异是子代种群成型的两步关键操作。下面这段代码先把子代数组按样本数扩容,然后两两抽取父代做交叉;每次循环以 rand()%100 < 5 判定,约 5% 概率对单个子代树做变异,相当于每代约有 1/10 的子代个体会被扰动。 Spopulation _g; ArrayResize(_g.population, _samples); for(int s = 0; s < _samples - 1; s += 2) { int _a = rand() % _samples; int _b = rand() % _samples; SetCrossover(_s.population[_a].tree, _s.population[_b].tree, _g.population[s].tree, _g.population[s + 1].tree); if (rand() % 100 < 5) // 5% chance { SetMutation(_g.population[s].tree); } if (rand() % 100 < 5) { SetMutation(_g.population[s + 1].tree); } } // 上面逐行拆解:第1行声明子代种群;第2行按样本数_resize;for 以步长2遍历,保证成对处理;_a/_b 是父代随机下标;SetCrossover 把两棵父树交叉写入 s 和 s+1 两棵子树;两个 if 各按 5% 阈值调用 SetMutation,彼此独立。 旧种群随后被整批替换:ArrayResize(_p.population, _samples) 后,把 _g 里每棵树的 _e_size 个节点抄回 _p。紧接着扫一遍新种群算适应度,_fit 更小时更新 _best_fit 并 ArrayCopy 出 BestTree,这就是每代最优个体的落点。 int CSignalSR::LongCondition(void) { int result = 0; m_close.Refresh(-1); matrix _xy; _xy.Init(m_data_set, 2); for(int i = 0; i < m_data_set; i++) { _xy[i][0] = m_close.GetData(StartIndex()+i+1); _xy[i][1] = m_close.GetData(StartIndex()+i); } ... return(result); } // 这段把最新收盘价和前一根收盘价塞进 2 列矩阵,作为遗传模型「投票」做多的输入特征;具体 return 的 result 由 GetBestTree 给出的预测方向决定,价格后续倾向于上涨时 result 为正。 外汇与贵金属波动剧烈、杠杆风险高,这套信号仅提供概率倾向,实盘前请在 MT5 用历史数据跑通 LongCondition 与 GetBestTree 的联调。

MQL5 / C++
Spopulation _g;
ArrayResize(_g.population, _samples);
for(class="type">int s = class="num">0; s < _samples - class="num">1; s += class="num">2)
{  class="type">int _a  = rand() % _samples;
    class="type">int _b  = rand() % _samples;
    SetCrossover(_s.population[_a].tree, _s.population[_b].tree, _g.population[s].tree, _g.population[s + class="num">1].tree);
    if (rand() % class="num">100 < class="num">5)   class=class="str">"cmt">// class="num">5% chance
    {   SetMutation(_g.population[s].tree);
    }
    if (rand() % class="num">100 < class="num">5)
    {   SetMutation(_g.population[s + class="num">1].tree);
    }
}
class=class="str">"cmt">// Replace old population
ArrayResize(_p.population, _samples);
for(class="type">int s = class="num">0; s < _samples; s ++)
{  for(class="type">int ee = class="num">0; ee < _e_size; ee++)
   {  _p.population[s].tree[ee] = _g.population[s].tree[ee];
   }
}
class=class="str">"cmt">// Print best individual
for(class="type">int s = class="num">0; s < _samples; s ++)
{  _fit = GetFitness(XY, Y, _p.population[s].tree);
   if (_fit < _best_fit)
   {  _best_fit = _fit;
      ArrayCopy(BestTree,_p.population[s].tree);
   }
}
}
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Voting" that price will grow.                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalSR::LongCondition(class="type">void)
{  class="type">int result = class="num">0;
   m_close.Refresh(-class="num">1);
   matrix _xy;
   _xy.Init(m_data_set, class="num">2);
   for(class="type">int i = class="num">0; i < m_data_set; i++)
   {  _xy[i][class="num">0] = m_close.GetData(StartIndex()+i+class="num">1);
      _xy[i][class="num">1] = m_close.GetData(StartIndex()+i);
   }
...
   class="kw">return(result);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Voting" that price will grow.                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalSR::LongCondition(class="type">void)
{
   ...
   vector _y;
   class="type">class="kw">string _best_fit[];
   GetBestTree(_xy, _y, _best_fit);
   ...
   class="kw">return(result);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

支撑阻力信号的做多票数怎么算

CSignalSR 的 LongCondition 并不靠单一阈值硬切,而是把当前水平 _y[0] 与收盘价的距离先归一化再乘 100,得到一张‘看涨票值’。 代码里 _cond = (_y[0]-m_close.GetData(StartIndex())) / fmax(fabs(_y[0]), m_close.GetData(StartIndex())),这一步把价格偏离压到约 [-100, 100] 的区间;乘 100.0 后,正数代表价格位于参考水平上方、倾向触发多头投票。 只有 _cond > 0.0 时才把票值取绝对值转成 int 返回,否则 result 保持 0。也就是说,价格低于参考线时该类信号不投做多票,实战中可据此在 MT5 策略测试器里观察票值为 0 的频次来判断震荡市过滤效果。 外汇与贵金属杠杆高、滑点大,这套票数逻辑只是信号权重的一环,实盘前务必用历史数据验证其漏报与误报比例。

MQL5 / C++
class="type">int CSignalSR::LongCondition(class="type">void)
{  class="type">int result = class="num">0;

      ...
   class="type">class="kw">double _cond = (_y[class="num">0]-m_close.GetData(StartIndex())) / fmax(fabs(_y[class="num">0]), m_close.GetData(StartIndex()));
   _cond *= class="num">100.0;
   class=class="str">"cmt">//printf(__FUNCSIG__ + " cond: %.2f", _cond);
   class=class="str">"cmt">//class="kw">return(result);
   if(_cond > class="num">0.0)
   {  result = class="type">int(fabs(_cond));
   }
   class="kw">return(result);
}

「随机基因也能跑出正期望」

把组装好的智能系统按几组“最佳设置”丢进策略测试器,同一套参数每次跑出来的净值曲线都不太一样。树型表达式由随机选择生成,交叉与突变也是随机的,所以单次测试运行几乎不可能被原样复现。 有意思的是,只要某次运行是盈利的,换一组随机种子用相同设置再跑,绩效统计会漂移,但整体仍倾向盈利。这说明盈利性更多来自设置结构,而非某次幸运的随机树。 我们的回测锁定 2022 年、H4 周期、欧元日元(EURJPY)。测试刻意不设止损止盈价格目标,以便干净地观察智能系统自身的进出场逻辑强弱。外汇与贵金属属高风险品种,回测盈利不代表实盘概率一致,开 MT5 用相同年份品种自测最实在。

◍ 把工具请下神坛

符号回归在这套智能信号实例里,只处理一维输入与一维输出,数据集刻意压得很小,跑通逻辑优先于堆参数。遗传优化本身带随机性,同一套配置多次回测很难复现完全一致的结果,这是算法性质决定的,不是调参能抹掉的。 因此基于它的信号不适合单独扛仓位,放在大周期上和其它独立信号做交叉确认更稳妥;外汇与贵金属杠杆高、滑点跳空频繁,任何单模型确认都只是概率倾斜而非胜率担保。 附带的 sr.mq5 与 SignalWZ_20.mqh 可直接丢进 MT5 跑,但别神话这套写法——它只是把多空条件权衡自动化了,真要上线还得你自己压回测样本与周期。

常见问题

用随机索引在内部节点里取切点,避开叶子节点,交叉后先做结构合法性校验再入种群,能降低无效树比例。
树评估输出值过阈值且当前无持仓时返回做多票,具体阈值按品种波动调,回测里常用标准化后的正区间。
小布可接管重复的基因交叉与回测扫描,你只设品种和约束,它把达标树和做多票数整理好给你看。
把触碰次数和距现价远近各归一化再乘权重求和,票数为正且超中位线才计入做多,避免噪声区刷票。
样本外正期望仅代表概率倾向,外汇贵金属高风险,须多周期验证并控仓,不可视作稳定盈利依据。