自适应社会行为优化(ASBO):Schwefel函数与Box-Muller方法·进阶篇
◍ 把同货币对多窗口塞进一个主控
做复盘或信号系统时,最常撞墙的一件事是:同一个货币对开了好几张图,每张都跑着功能不同的 EA,互相之间没法通信,日志也散得一塌糊涂。这一节先解决集成思路——用一个主控程序把同品种的多窗口收纳进来,再统一把运行信息打到日志、把成交信号直接标在图上。 顺带要落地的,是一个控制指标的初始化细节。它本身不复杂,但模块加载顺序和全局变量作用域容易踩坑,稍后代码里能直接看到 PopulationSize、Generations 这些输入项如何在 OnInit 里决定种群规模。 下面这段是演化优化模块的骨架:默认种群 50、迭代 100 代、初始步长 1.0,OnTick 里用 86400 秒(刚好一天)做触发阈值,每天跑一次 Optimize,平时走 TradingLogic。外汇和贵金属波动剧烈,这类自动优化仅作技术验证,实盘前务必在 MT5 策略测试器里跑历史回测。 初始化时每个个体基因取 0~100 的随机值,适应度函数示例用三基因离 50 的平方和取负,意味着越靠近中值适应度越高——你可以把 CalculateFitness 换成自己的目标函数(如夏普率或回撤)来验证。
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 在历史面拟合好不等于实盘能扛滑点,参数漂移可能引发连续回撤,请先在策略测试器用真实点差跑通再上仓。
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() 重采样。 外汇与贵金属波动具有高风险,这类合成噪声只用于策略内部的参数扰动,不能直接当行情方向信号。
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。
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 ¶m, 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 ¶m, 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。
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 ¢er []) { 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 ¢er[]) { 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 是否过拟合。
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; } }