自适应社会行为优化(ASBO):Schwefel函数与Box-Muller方法·进阶篇
📘

自适应社会行为优化(ASBO):Schwefel函数与Box-Muller方法·进阶篇

第 2/2 篇

◍ 把同货币对多窗口塞进一个主控

做复盘或信号系统时,最常撞墙的一件事是:同一个货币对开了好几张图,每张都跑着功能不同的 EA,互相之间没法通信,日志也散得一塌糊涂。这一节先解决集成思路——用一个主控程序把同品种的多窗口收纳进来,再统一把运行信息打到日志、把成交信号直接标在图上。 顺带要落地的,是一个控制指标的初始化细节。它本身不复杂,但模块加载顺序和全局变量作用域容易踩坑,稍后代码里能直接看到 PopulationSize、Generations 这些输入项如何在 OnInit 里决定种群规模。 下面这段是演化优化模块的骨架:默认种群 50、迭代 100 代、初始步长 1.0,OnTick 里用 86400 秒(刚好一天)做触发阈值,每天跑一次 Optimize,平时走 TradingLogic。外汇和贵金属波动剧烈,这类自动优化仅作技术验证,实盘前务必在 MT5 策略测试器里跑历史回测。 初始化时每个个体基因取 0~100 的随机值,适应度函数示例用三基因离 50 的平方和取负,意味着越靠近中值适应度越高——你可以把 CalculateFitness 换成自己的目标函数(如夏普率或回撤)来验证。

MQL5 / C++
class=class="str">"cmt">// Inputs
class="kw">input class="type">int    PopulationSize = class="num">50;   class=class="str">"cmt">// Population size
class="kw">input class="type">int    Generations = class="num">100;     class=class="str">"cmt">// Number of generations
class="kw">input class="type">class="kw">double InitialStepSize = class="num">1.0; class=class="str">"cmt">// Initial step size
class=class="str">"cmt">// Structure for storing an individual
class="kw">struct Individual
{
    class="type">class="kw">double genes [class="num">3];   class=class="str">"cmt">// Strategy parameters
    class="type">class="kw">double stepSizes [class="num">3];   class=class="str">"cmt">// Step sizes for each parameter
    class="type">class="kw">double fitness;   class=class="str">"cmt">// Fitness
};
class=class="str">"cmt">// Global variables
Individual population [];
class="type">int generation = class="num">0;
class=class="str">"cmt">// Initialization function
class="type">int OnInit()
{
   ArrayResize(population, PopulationSize);
   InitializePopulation();
   class="kw">return (INIT_SUCCEEDED);
}
class=class="str">"cmt">// Main loop function
.class="type">class="kw">datetime lastOptimizationTime = class="num">0;
class="type">void OnTick()
{
   class="type">class="kw">datetime currentTime = TimeCurrent();
   class=class="str">"cmt">// Check if a day has passed since the last optimization
   if (currentTime - lastOptimizationTime >= class="num">86400) class=class="str">"cmt">// class="num">86400 seconds in a day
   {
       Optimize();
       lastOptimizationTime = currentTime;
   }
   class=class="str">"cmt">// Here is the code for trading with the current optimal parameters
   TradingLogic();
}
class="type">void Optimize()
{
   class=class="str">"cmt">// Optimization code(current OnTick content)
}
class="type">void TradingLogic()
{
   class=class="str">"cmt">// Implementing trading logic using optimized parameters
}
class=class="str">"cmt">// Initialize the population
class="type">void InitializePopulation()
{
   for (class="type">int i = class="num">0; i < PopulationSize; i++)
   {
       for (class="type">int j = class="num">0; j < class="num">3; j++)
       {
           population [i].genes [j] = MathRand() / class="num">32767.0 * class="num">100;   class=class="str">"cmt">// Random values from class="num">0 to class="num">100
           population [i].stepSizes [j] = InitialStepSize;
       }
   }
}
class=class="str">"cmt">// Population fitness assessment
class="type">void EvaluatePopulation()
{
   for (class="type">int i = class="num">0; i < PopulationSize; i++)
   {
       population [i].fitness = CalculateFitness(population [i]);
   }
}
class=class="str">"cmt">// Calculate the fitness of an individual(this is where you need to implement your objective function)
class="type">class="kw">double CalculateFitness(Individual &ind)
{
   class=class="str">"cmt">// Example of a simple objective function
   class="kw">return -(MathPow(ind.genes [class="num">0] - class="num">50, class="num">2) + MathPow(ind.genes [class="num">1] - class="num">50, class="num">2) + MathPow(ind.genes [class="num">2] - class="num">50, class="num">2));
}
class=class="str">"cmt">// Sort the population in descending order of fitness
class="type">void SortPopulation()
{
   ArraySort(population, WHOLE_ARRAY, class="num">0, MODE_DESCEND);
}
class=class="str">"cmt">// Population mutation according to Schwefel&class="macro">#x27;s concept
class="type">void Mutate()
{
   for (class="type">int i = class="num">1; i < PopulationSize; i++)   class=class="str">"cmt">// Start from class="num">1 to keep the best solution
   {

把进化参数塞进 MT5 的每日重优化循环

这段实现把一个三基因个体的进化框架直接挂到 EA 生命周期上:种群规模 50、迭代 100 代、初始步长 1.0,三个基因的取值域被硬性夹在 0~100。适应度函数用负平方和把最优解锚在 (50,50,50),偏离越远适应度越低,属于典型的凸碗形测试面。 初始化时每个基因由 MathRand()/32767.0*100 均匀撒点,步长统一给 InitialStepSize;变异段对步长做 exp(0.2*rand-0.1) 的乘性扰动,再叠加正态随机走,最后用 MathMax/MathMin 收回边界。开 MT5 把这段代码贴进 EA,改 PopulationSize 到 200 可能让日线级重优化更稳,但会吃更多回测时间。 OnTick 里用 currentTime-lastOptimizationTime>=86400 做每日触发器,过了 24 小时才跑一次 Optimize(),平时只走 TradingLogic()。外汇与贵金属杠杆高,这种自优化 EA 在历史面拟合好不等于实盘能扛滑点,参数漂移可能引发连续回撤,请先在策略测试器用真实点差跑通再上仓。

MQL5 / C++
for (class="type">int j = class="num">0; j < class="num">3; j++)
  {
    class=class="str">"cmt">// Step size mutation
    population [i].stepSizes [j] *= MathExp(class="num">0.2 * MathRandom() - class="num">0.1);
    class=class="str">"cmt">// Gene mutation
    population [i].genes [j] += population [i].stepSizes [j] * NormalRandom();
    class=class="str">"cmt">// Limiting gene values
    population [i].genes [j] = MathMax(class="num">0, MathMin(class="num">100, population [i].genes [j]));
  }
  }
}
class=class="str">"cmt">// Auxiliary function for displaying information about an individual
class="type">void PrintIndividual(Individual &ind)
{
  Print("Genes: ", ind.genes [class="num">0], ", ", ind.genes [class="num">1], ", ", ind.genes [class="num">2]);
  Print("Step sizes: ", ind.stepSizes [class="num">0], ", ", ind.stepSizes [class="num">1], ", ", ind.stepSizes [class="num">2]);
  Print("Fitness: ", ind.fitness);
}
class="kw">input class="type">int PopulationSize = class="num">50;   class=class="str">"cmt">// Population size
class="kw">input class="type">int Generations = class="num">100;     class=class="str">"cmt">// Number of generations
class="kw">input class="type">class="kw">double InitialStepSize = class="num">1.0; class=class="str">"cmt">// Initial step size
class="kw">struct Individual
{
  class="type">class="kw">double genes[class="num">3];   class=class="str">"cmt">// Strategy parameters
  class="type">class="kw">double stepSizes[class="num">3];   class=class="str">"cmt">// Step sizes for each parameter
  class="type">class="kw">double fitness;   class=class="str">"cmt">// Fitness
};
class="type">int OnInit()
{
  ArrayResize(population, PopulationSize);
  InitializePopulation();
  class="kw">return (INIT_SUCCEEDED);
}
class="type">void InitializePopulation()
{
  for (class="type">int i = class="num">0; i < PopulationSize; i++)
  {
    for (class="type">int j = class="num">0; j < class="num">3; j++)
    {
      population [i].genes [j] = MathRand() / class="num">32767.0 * class="num">100;
      population [i].stepSizes [j] = InitialStepSize;
    }
  }
}
class="type">class="kw">datetime lastOptimizationTime = class="num">0;
class="type">void OnTick()
{
  class="type">class="kw">datetime currentTime = TimeCurrent();
  class=class="str">"cmt">// Check if a day has passed since the last optimization
  if (currentTime - lastOptimizationTime >= class="num">86400) class=class="str">"cmt">// class="num">86400 seconds in a day
  {
    Optimize();
    lastOptimizationTime = currentTime;
  }
  class=class="str">"cmt">// Here is the code for trading with the current optimal parameters
  TradingLogic();
}
class="type">void Optimize()
{
  class=class="str">"cmt">// Optimization code(current OnTick content)
}
class="type">void TradingLogic()
{
  class=class="str">"cmt">// Implementing trading logic using optimized parameters
}
class="type">void EvaluatePopulation()
{
  for (class="type">int i = class="num">0; i < PopulationSize; i++)
  {
    population [i].fitness = CalculateFitness(population [i]);
  }
}
class="type">class="kw">double CalculateFitness(Individual &ind)
{
  class="kw">return -(MathPow(ind.genes [class="num">0] - class="num">50, class="num">2) + MathPow(ind.genes [class="num">1] - class="num">50, class="num">2) + MathPow(ind.genes [class="num">2] - class="num">50, class="num">2));
}
class="type">void SortPopulation()
{

「用 Box-Muller 在 MT5 里造正态噪声」

遗传算法里的变异步长离不开正态分布随机数。下面这段把 Box-Muller 变换直接塞进 MQL5,用两个均匀随机数 u1、u2 拧出一个标准正态值,供种群基因扰动使用。 NormalRandom() 里 u1、u2 取自 MathRand()/32767.0,范围约 [0,1];当 u1 极靠近 0 时 MathLog 会吐负无穷,实盘跑前最好加个下限保护。 OnStart 默认抽 10000 个样本,把 [-4,4] 切成 20 个宽 0.4 的桶装直方图,再算均值、方差、标准差。理论上均值应贴近 0、标准差贴近 1;你在 MT5 按 F5 跑完看 Print,若均值偏离 0 超过 0.05 或标准差掉到 0.95 以下,说明随机种子周期太短,倾向换 MathRandom() 重采样。 外汇与贵金属波动具有高风险,这类合成噪声只用于策略内部的参数扰动,不能直接当行情方向信号。

MQL5 / C++
class="type">class="kw">double NormalRandom()
{
  class="type">class="kw">double u1 = (class="type">class="kw">double)MathRand() / class="num">32767.0;
  class="type">class="kw">double u2 = (class="type">class="kw">double)MathRand() / class="num">32767.0;
  class="kw">return MathSqrt(-class="num">2 * MathLog(u1)) * MathCos(class="num">2 * M_PI * u2);
}
class="type">void OnStart()
{
  class="type">class="kw">double sum = class="num">0;
  class="type">class="kw">double sumSquared = class="num">0;
  class="type">class="kw">double min = DBL_MAX;
  class="type">class="kw">double max = DBL_MIN;
  class="type">int histogram [];
  ArrayResize(histogram, class="num">20);
  ArrayInitialize(histogram, class="num">0);
  for (class="type">int i = class="num">0; i < NumSamples; i++)
  {
    class="type">class="kw">double value = NormalRandom();
    sum += value;
    sumSquared += value * value;
    if (value < min) min = value;
    if (value > max) max = value;
    class="type">int index = (class="type">int)((value + class="num">4) / class="num">0.4);
    if (index >= class="num">0 && index < class="num">20) histogram [index]++;
  }
  class="type">class="kw">double mean = sum / NumSamples;
  class="type">class="kw">double variance = (sumSquared - sum * sum / NumSamples) / (NumSamples - class="num">1);
  class="type">class="kw">double stdDev = MathSqrt(variance);
  Print("Statistics for ", NumSamples, " generated numbers:");
  Print("Average value: ", mean);
  Print("Standard deviation: ", stdDev);
  Print("Minimum value: ", min);
  Print("Maximum value: ", max);
}

◍ 自适应变异与参数约束的实装细节

把进化算法的参数更新搬进 MT5,核心就是几段不长的函数。下面这段直方图打印逻辑,能在优化器跑完后代你直观看分布:循环 20 个桶,每个桶宽 0.4,从 -4.0 到 4.0,星号数量按 histogram[i]*50/NumSamples 缩放,NumSamples 是样本总数。 [CODE] 中的 AdaptiveMutation 用对数正态扰动同时更新 Cg、Cs、Cn 三个权重:每个乘子都是 MathExp(tau_prime*NormalRandom() + tau*NormalRandom()),tau 与 tau_prime 控制全局与个体步长。外汇与贵金属行情高波动,这种无界乘子容易让参数爆掉,所以必须接 LimitParameters 把三者夹在 [0,2] 区间。

归一化也不能省。NormalizeParameters 在 sum>0 时除以总和,否则退回 1/3 均等分配,保证权重语义不漂。ScaleParameter 用 sign*MathLog(1+MathAbs(param)) 压缩量级,AdaptMutationStep 在param>threshold 时把步长乘 0.9——这两点直接决定你在 EURUSD 上重优化频率。

越界监控交给 MonitorAndAdapt:超 maxAllowedValue 就累加计数,超阈值调 AdjustMutationStrategy;ResetParameters 则按 resetInterval 代把 Cg/Cs/Cn 复位到初值。LimitGrowth 用 2/(1+exp(-param))-1 把任意值压进 [-1,1],比硬截断更平滑。开 MT5 把这几段贴进 EA,先跑 2023 年 XAUUSD H1 看越界计数是否频繁再调 threshold。

MQL5 / C++
class=class="str">"cmt">// Display the histogram
Print("Distribution histogram:");
for (class="type">int i = class="num">0; i < class="num">20; i++)
{
  class="type">class="kw">string bar = "";
  for (class="type">int j = class="num">0; j < histogram [i] * class="num">50 / NumSamples; j++) bar += "*";
  PrintFormat("[%.1f, %.1f): %s", -class="num">4 + i * class="num">0.4, -class="num">3.6 + i * class="num">0.4, bar);
}
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void AdaptiveMutation(class="type">class="kw">double &Cg, class="type">class="kw">double &Cs, class="type">class="kw">double &Cn)
{
  Cg *= MathExp(tau_prime * NormalRandom() + tau * NormalRandom());
  Cs *= MathExp(tau_prime * NormalRandom() + tau * NormalRandom());
  Cn *= MathExp(tau_prime * NormalRandom() + tau * NormalRandom());
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void LimitParameters(class="type">class="kw">double &param, class="type">class="kw">double minValue, class="type">class="kw">double maxValue)
{
  param = MathMax(minValue, MathMin(param, maxValue));
}
class=class="str">"cmt">// Usage:
LimitParameters(Cg, class="num">0.0, class="num">2.0);
LimitParameters(Cs, class="num">0.0, class="num">2.0);
LimitParameters(Cn, class="num">0.0, class="num">2.0);
class="type">void NormalizeParameters(class="type">class="kw">double &Cg, class="type">class="kw">double &Cs, class="type">class="kw">double &Cn)
{
  class="type">class="kw">double sum = Cg + Cs + Cn;
  if (sum > class="num">0)
  {
    Cg /= sum;
    Cs /= sum;
    Cn /= sum;
  }
  else
  {
    class=class="str">"cmt">// If sum is class="num">0, set equal values
    Cg = Cs = Cn = class="num">1.0 / class="num">3.0;
  }
}
class="type">class="kw">double ScaleParameter(class="type">class="kw">double param)
{
  if (param == class="num">0) class="kw">return class="num">0;
  class="type">class="kw">double sign = (param > class="num">0) ? class="num">1 : -class="num">1;
  class="kw">return sign * MathLog(class="num">1 + MathAbs(param));
}
class="type">void AdaptMutationStep(class="type">class="kw">double &stepSize, class="type">class="kw">double paramValue)
{
  if(MathAbs(paramValue) > threshold)
  {
    stepSize *= class="num">0.9; class=class="str">"cmt">// Reduce the step size
  }
}
class="type">void ResetParameters(class="type">int generationCount)
{
  if(generationCount % resetInterval == class="num">0)
  {
    Cg = initialCg;
    Cs = initialCs;
    Cn = initialCn;
  }
}
class="type">class="kw">double LimitGrowth(class="type">class="kw">double param)
{
  class="kw">return class="num">2.0 / (class="num">1.0 + MathExp(-param)) - class="num">1.0; class=class="str">"cmt">// Limit values in [-class="num">1, class="num">1]
}
class="type">void MonitorAndAdapt(class="type">class="kw">double &param, class="type">int &outOfBoundsCount)
{
  if(MathAbs(param) > maxAllowedValue)
  {
    outOfBoundsCount++;
    if(outOfBoundsCount > threshold)
    {
      class=class="str">"cmt">// Adaptation of the mutation strategy
      AdjustMutationStrategy();
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ASBO::AdaptiveMutation(S_ASBO_Agent &ag)
{
  ag.Cg *= MathExp(tau_prime * u.GaussDistribution(class="num">0, -class="num">1, class="num">1, class="num">1) + tau * u.GaussDistribution(class="num">0, -class="num">1, class="num">1, class="num">8));

邻域中心怎么按适应度差挑最近的三个

ASBO 算法里每个 agent 更新坐标前,要先找「离自己适应度最近的邻居中心」。上面两段代码干了同一件事:排除自身后,按fitness_i - fitness_self从小到大排,取前 3 个邻居的坐标做平均,写进 center[]。

第一段用指针引用 a[] 和 ag,第二段改成传 agent 数组 ag[] 加下标 ind,逻辑一致但接口不同——如果你在 MT5 里接自己的种群结构,注意别两个版本混用导致指针越界。 冒泡排序在这里是 O(n²),popSize 若设到 200 以上,单代邻域计算可能吃掉几毫秒;小布建议种群小于 80 时直接用,超了倾向换快速选择只捞前 3 名。 坐标维度 coords 和外层种群规模 popSize 必须匹配 ArrayResize 的大小,否则 center[j] 累加会踩到未初始化内存,回测时表现为偶发 NaN。

MQL5 / C++
ag.Cs *= MathExp(tau_prime * u.GaussDistribution(class="num">0, -class="num">1, class="num">1, class="num">1) + tau * u.GaussDistribution(class="num">0, -class="num">1, class="num">1, class="num">8));
ag.Cn *= MathExp(tau_prime * u.GaussDistribution(class="num">0, -class="num">1, class="num">1, class="num">1) + tau * u.GaussDistribution(class="num">0, -class="num">1, class="num">1, class="num">8));
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ASBO::FindNeighborCenter(const S_ASBO_Agent &ag, class="type">class="kw">double &center [])
{
class=class="str">"cmt">// Create arrays for indices and fitness differences
  class="type">int indices [];
  class="type">class="kw">double differences [];
  ArrayResize(indices, popSize - class="num">1);
  ArrayResize(differences, popSize - class="num">1);
class=class="str">"cmt">// Fill the arrays
  class="type">int count = class="num">0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (&a [i] != &ag)  class=class="str">"cmt">// Exclude the current agent
    {
      indices [count] = i;
      differences [count] = MathAbs(a [i].fitness - ag.fitness);
      count++;
    }
  }
class=class="str">"cmt">// Sort arrays by fitness difference(bubble sort)
  for (class="type">int i = class="num">0; i < count - class="num">1; i++)
  {
    for (class="type">int j = class="num">0; j < count - i - class="num">1; j++)
    {
      if (differences [j] > differences [j + class="num">1])
      {
        class=class="str">"cmt">// Exchange of differences
        class="type">class="kw">double tempDiff = differences [j];
        differences [j] = differences [j + class="num">1];
        differences [j + class="num">1] = tempDiff;
        class=class="str">"cmt">// Exchange of indices
        class="type">int tempIndex = indices [j];
        indices [j] = indices [j + class="num">1];
        indices [j + class="num">1] = tempIndex;
      }
    }
  }
class=class="str">"cmt">// Initialize the center
  ArrayInitialize(center, class="num">0.0);
class=class="str">"cmt">// Calculate the center based on the three nearest neighbors
  for (class="type">int j = class="num">0; j < coords; j++)
  {
    for (class="type">int k = class="num">0; k < class="num">3; k++)
    {
      class="type">int nearestIndex = indices [k];
      center [j] += a [nearestIndex].c [j];
    }
    center [j] /= class="num">3;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ASBO::FindNeighborCenter(class="type">int ind, S_ASBO_Agent &ag[], class="type">class="kw">double &center[])
{
  class="type">int indices[];
  class="type">class="kw">double differences[];
  ArrayResize(indices, popSize - class="num">1);
  ArrayResize(differences, popSize - class="num">1);
  class="type">int count = class="num">0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (i != ind)
    {
      indices[count] = i;
      differences[count] = MathAbs(ag[ind].f - ag[i].f);
      count++;
    }
  }
class=class="str">"cmt">// Sort by fitness difference ascending

「邻居均值落点怎么算」

排序完成后,先把 center 数组清零,避免残留上一次迭代的值污染聚类中心。 用 MathMin(3, count) 取最近邻数量,当样本少于 3 个时代码自动降级到可用数量,不会越界——这是多 agent 聚类里常被忽略的防护。 随后对每个坐标维度 j,累加前 neighborsCount 个最近样本的对应坐标再除以数量,得到该簇的中心向量。外汇与贵金属行情下用这类逻辑做状态聚类,信号漂移风险高,建议先在 MT5 策略测试器用历史 Tick 验证邻居数取 3 是否过拟合。

MQL5 / C++
  for (class="type">int i = class="num">0; i < count - class="num">1; i++)
  {
    for (class="type">int j = class="num">0; j < count - i - class="num">1; j++)
    {
      if (differences[j] > differences[j + class="num">1])
      {
        class="type">class="kw">double tempDiff = differences[j];
        differences[j] = differences[j + class="num">1];
        differences[j + class="num">1] = tempDiff;
        class="type">int tempIndex = indices[j];
        indices[j] = indices[j + class="num">1];
        indices[j + class="num">1] = tempIndex;
      }
    }
  }
  ArrayInitialize(center, class="num">0.0);
  class="type">int neighborsCount = MathMin(class="num">3, count);  class=class="str">"cmt">// Protection against the case when there are less than class="num">3 agents
  for (class="type">int j = class="num">0; j < coords; j++)
  {
    for (class="type">int k = class="num">0; k < neighborsCount; k++)
    {
      class="type">int nearestIndex = indices[k];
      center[j] += ag[nearestIndex].c[j];
    }
    center[j] /= neighborsCount;
  }
}

常见问题

在主控里用统一结构体存各周期的状态与信号,按权重汇总决策,避免每个窗口各跑一套重复计算。
放在每日新K线形成后调用重优化函数,用前一日数据刷新种群,防止盘中频繁重算拖慢执行。
小布可定时扫描你的品种页,把多窗口冲突与参数退化提示直接推给你,你只需点开确认是否重优化。
对生成值做标准差倍数截断(如±3σ),再映射到参数上下界,既保留随机性又不会越约束。
按适应度绝对差升序排,取前三作为邻居,再用它们的均值算落点,让劣解也能被邻域拉回。