彗星尾算法(CTA)·进阶篇
◍ 彗星尾迹算法的初始化与粒子散布
在 MT5 里写自定义优化器,C_AO_CTA 类的 Init 先把搜索区间、步长和 epoch 数接进来,再交给 StandardInit 做基础校验,失败直接返 false。 epochNow 归零后,按 cometsNumb 给彗星数组扩容,每颗彗星的坐标维度用 ArrayResize(comets[i].c, coords) 拉开,适应度 f 预设为 -DBL_MAX,也就是负极大,等待首轮评估覆盖。 尾迹长度 tailLength 按各维度区间跨度乘 tailLengthKo 算,比如某参数 rangeMax-rangeMin=2.0、tailLengthKo=0.1,那 tailLength 就是 0.2;partNumber = popSize / cometsNumb 决定每颗彗星带多少粒子。 Moving 里若 revision 为 false,则先给每颗彗星在 [rangeMin, rangeMax] 内随机定位并吸附到步长网格,再让每颗粒子以彗星中心做高斯散布,半宽取 tailLength*0.5 且夹紧在边界内。 这套初始化决定了后续种群是否覆盖全空间,外汇与贵金属参数优化属高风险实验,回测拟合优不代表实盘概率占优,开 MT5 把 tailLengthKo 从 0.1 调到 0.3 能直观看粒子云变胖。
class="type">bool C_AO_CTA::Init(class="kw">const class="type">class="kw">double &rangeMinP [], class=class="str">"cmt">//minimum search range class="kw">const class="type">class="kw">double &rangeMaxP [], class=class="str">"cmt">//maximum search range class="kw">const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search class="kw">const class="type">int epochsP = class="num">0) class=class="str">"cmt">//number of epochs { if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return class="kw">false; class=class="str">"cmt">//---------------------------------------------------------------------------- epochs = epochsP; epochNow = class="num">0; ArrayResize(comets, cometsNumb); for (class="type">int i = class="num">0; i < cometsNumb; i++) { ArrayResize(comets [i].c, coords); comets [i].f = -DBL_MAX; } ArrayResize(tailLength, coords); ArrayResize(maxSpaceDistance, coords); for (class="type">int i = class="num">0; i < coords; i++) { maxSpaceDistance [i] = rangeMax [i] - rangeMin [i]; tailLength [i] = maxSpaceDistance [i] * tailLengthKo; } partNumber = popSize / cometsNumb; class="kw">return true; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_CTA::Moving() { epochNow++; class="type">int cnt = class="num">0; class="type">class="kw">double min = class="num">0.0; class="type">class="kw">double max = class="num">0.0; class=class="str">"cmt">//---------------------------------------------------------------------------- if (!revision) { for (class="type">int i = class="num">0; i < cometsNumb; i++) { for (class="type">int c = class="num">0; c < coords; c++) { comets [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]); comets [i].c [c] = u.SeInDiSp(comets [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } for (class="type">int i = class="num">0; i < cometsNumb; i++) { for (class="type">int p = class="num">0; p < partNumber; p++) { for (class="type">int c = class="num">0; c < coords; c++) { min = comets [i].c [c] - tailLength [c] * class="num">0.5; if (min < rangeMin [c]) min = rangeMin [c]; max = comets [i].c [c] + tailLength [c] * class="num">0.5; if (max > rangeMax [c]) max = rangeMax [c]; a [cnt].c [c] = u.GaussDistribution(comets [i].c [c], min, max, class="num">1); a [cnt].c [c] = u.SeInDiSp(a [cnt].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } cnt++; } } revision = true; class="kw">return; } class=class="str">"cmt">//---------------------------------------------------------------------------- cnt = class="num">0; class="type">class="kw">double coefTail = class="num">0.0;
粒子变异里的 0.6 概率分水岭
这段嵌套循环在做的,是给每一颗「彗星」粒子在每个坐标维度上生成下一代候选位置。外层遍历 cometsNumb 颗粒子,中层 partNumber 次采样,内层 coords 个维度,三重循环跑完才会把 cnt 推进一步。 核心分支在 u.RNDprobab() < 0.6:60% 概率走高斯扰动路线,40% 概率走跨粒子线性插值路线。前者用当前粒子与质心 cB 的距离占比 coefTail 来压缩扰动幅度,距离越远,tail 系数越靠近 minShiftCoef,位移越收敛;后者随机挑两颗非自身的粒子做 r1、r2 差分,再乘 0.1 和随机概率注入扰动。 高斯路径里有个细节:当 cB[c]*Dir 与粒子坐标同侧或异侧时,min/max 的尾巴分配公式互换,保证扰动区间始终贴着粒子原位置向质心反方向或顺方向拉伸。相等时直接给 ±0.1*tailLength 的窄窗。最后无论哪条路,都用 SeInDiSp 把结果吸附到 [rangeMin, rangeMax] 的离散网格上。 开 MT5 把 0.6 改成 0.3 或 0.8,能看到候选粒子群从「聚拢质心」切到「散开探索」的明显差异;外汇与贵金属行情下用这类群体算法做特征采样,需清楚杠杆与滑点会放大样本外偏差,属高风险操作。
class="type">class="kw">double coefSize = class="num">0.0; for (class="type">int i = class="num">0; i < cometsNumb; i++) { for (class="type">int p = class="num">0; p < partNumber; p++) { for (class="type">int c = class="num">0; c < coords; c++) { if (u.RNDprobab() < class="num">0.6) { coefTail = fabs(comets [i].c [c] - cB [c]) / maxSpaceDistance [c]; coefSize = coefTail; class=class="str">"cmt">//(class="num">1-x)*class="num">0.9+x*class="num">0.5 coefTail = (class="num">1 - coefTail) * maxShiftCoef + coefTail * minShiftCoef; class=class="str">"cmt">//(class="num">1-x)*class="num">0.1+x*class="num">0.9 coefSize = (class="num">1 - coefSize) * maxSizeCoef + coefSize * minSizeCoef; if (cB [c] * Dir > comets [i].c [c] * Dir) { min = comets [i].c [c] - tailLength [c] * coefTail * coefSize; max = comets [i].c [c] + tailLength [c] * (class="num">1.0 - coefTail) * coefSize; } if (cB [c] * Dir < comets [i].c [c] * Dir) { min = comets [i].c [c] - tailLength [c] * (class="num">1.0 - coefTail) * coefSize; max = comets [i].c [c] + tailLength [c] * (coefTail)*coefSize; } if (cB [c] == comets [i].c [c]) { min = comets [i].c [c] - tailLength [c] * class="num">0.1; max = comets [i].c [c] + tailLength [c] * class="num">0.1; } if (min < rangeMin [c]) min = rangeMin [c]; if (max > rangeMax [c]) max = rangeMax [c]; a [cnt].c [c] = u.GaussDistribution(comets [i].c [c], min, max, Power); a [cnt].c [c] = u.SeInDiSp(a [cnt].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } else { class="type">int r = class="num">0; class="type">int r1 = class="num">0; class="type">int r2 = class="num">0; do { r = u.RNDminusOne(cometsNumb); r1 = r; } class="kw">while (r1 == i); do { r = u.RNDminusOne(cometsNumb); r2 = r; } class="kw">while (r2 == i || r2 == r1); a [cnt].c [c] = comets [r1].c [c] + class="num">0.1 * (comets [r2].c [c] - comets [i].c [c]) * u.RNDprobab(); a [cnt].c [c] = u.SeInDiSp(a [cnt].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } cnt++; } }
「精英保留与彗星核更新的实现细节」
这段 Revision 函数是粒子群类里负责「留下最好解」的核心。它先扫一遍种群 a[],把适应度 a[i].f 超过历史最优 fB 的个体下标记下来,一旦找到就刷新 fB 并把该个体的参数数组 c 整组拷进 cB,相当于精英存档。 紧接着是彗星核的重置逻辑。代码用 cnt 顺序遍历当前代粒子,对每颗彗星 comets[i] 都比一遍分配给它的 partNumber 个候选,若某粒子适应度更高就覆盖彗星自身的 f 与参数。注意这里 cometsNumb 颗彗星各自独立挑核,cnt 自增贯穿所有粒子,所以 partNumber × cometsNumb 应等于 popSize,否则会漏算或越界。 在 MT5 里把这段直接贴进你的 C_AO_CTA 类,把 popSize、cometsNumb、partNumber 打桩输出,能验证总数对齐。外汇与贵金属市场波动剧烈,这类优化仅用于策略参数搜索,实盘前须充分回测,模型过拟合概率不低。
class="type">void C_AO_CTA::Revision() { class="type">int ind = -class="num">1; for (class="type">int i = class="num">0; i < popSize; i++) { if (a [i].f > fB) { fB = a [i].f; ind = i; } } if (ind != -class="num">1) ArrayCopy(cB, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY); class=class="str">"cmt">//set a new kernel------------------------------------------------------------ class="type">int cnt = class="num">0; for (class="type">int i = class="num">0; i < cometsNumb; i++) { ind = -class="num">1; for (class="type">int p = class="num">0; p < partNumber; p++) { if (a [cnt].f > comets [i].f) { comets [i].f = a [cnt].f; ind = cnt; } cnt++; } if (ind != -class="num">1) ArrayCopy(comets [i].c, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY); } }
◍ 彗星尾算法跑分台的真实排位
把彗星尾算法(CTA)塞进标准优化测试台,每项函数跑 10000 次取平均。5 个采样点的 Hilly 函数得分 0.95346 / 0.86319 / 0.27770,Forest 函数 0.99794 / 0.85740 / 0.33949,Megacity 离散函数 0.88769 / 0.56431 / 0.10512,三项加总 5.846,约为理论满分的 64.96%。 横向拉一张 38 种算法的得分表,CTA 排第 3,前面只有二进制遗传算法(6.921,76.90%)和 (P+O) 进化策略(6.496,72.18%)。前两名在 500 采样点的高维段仍守住 0.50+ 和 0.56+,CTA 在 500 点 Megacity 只剩 0.10512,高维退化明显。 外汇与贵金属参数寻优属高风险场景,这套分数只说明 CTA 在中低维连续空间可能比多数群智能算法更稳,真上 MT5 做 EA 参数拟合,建议先用 5~25 采样点验证,别直接丢 500 维。
记住这一条就够了
CTA 把彗星运动拆成了可计算的优化器,但尾部方向、尾部大小、方向偏移都是外部参数,调参成本不低。实测里,尾部指向恒星(Dir_P = -1)在高维问题更管用,远离恒星的方向在低维收敛更好,动态切换能兼顾探索与精细化。 处理离散函数时结果分布较小,对交易系统寻优的稳定性和可重复性偏友好;但在平滑高维函数上它可能掉链子,别拿它硬刚高维连续空间。 真要上手,抓一个落点就行:在 MT5 里把尾部方向做成随迭代次数动态的系数,低维用远离恒星、高维切到指向恒星,其余参数先跑默认再看直方图评级。外汇与贵金属优化本身高风险,任何算法输出都只是概率优势,不是保本凭证。