基于分形的算法(FBA)·进阶篇
(2/3)·从棋盘式空间划分到前景区域递归细分,看 FBA 如何把计算资源精准压在最优解附近
FBA优化器的初始化与移动逻辑
在 MT5 里跑 FBA(分形布朗优化)这类自定义求解器,第一步是种群初始化。代码里的 Moving() 函数在 revision 为 false 时,会按 rangeMin/rangeMax/rangeStep 把 popSize 个个体均匀撒进搜索空间,并用 SeInDiSp 把坐标吸附到离散网格上——这决定了后续参数寻优不会跳出你设的步长粒度。 初始化只跑一次,随后 revision 置 true 并直接 return,主循环才进入六步主优化:先按适应度挑出 P1% 优解(IdentifyPromisingPoints),再算各子空间潜力秩、选 P2% 子空间、细分、生成新种群、最后变异。每一步都是独立方法调用,方便你在 MT5 策略测试器里单步注释掉做对照。 空间划分有个硬上限:CreateInitialSpacePartitioning() 中 totalSubspaces = m_value^coords,若超过 10000 就截断到 10000。这意味着当坐标维度 coords 较高、m_value 取 3 时,3^8=6561 还能完整分,但 3^9=19683 会被砍掉近一半子空间,维度爆炸时求解覆盖度会倾向下降。 Revision() 负责刷新全局最优点:遍历种群,若 a[i].f > fB 就拷贝坐标进 cB。注意这里用大于号,说明该框架默认适应度是越大越好(如夏普类指标),若你优化的是回撤最小化就得在传参前取负。
class="type">void C_AO_FBA::Moving() { class=class="str">"cmt">// First iteration - initialization of the initial population if (!revision) { class=class="str">"cmt">// Initialize the initial population uniformly throughout the space for (class="type">int i = class="num">0; i < popSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]); a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } revision = true; class="kw">return; } class=class="str">"cmt">// Main optimization class=class="str">"cmt">// class="num">1. Identifying promising points(P1% of points with the best function values) class="type">int promisingIndices []; IdentifyPromisingPoints(promisingIndices); class=class="str">"cmt">// class="num">2. Calculation of potential ranks for each subspace CalculateSubspaceRanks(promisingIndices); class=class="str">"cmt">// class="num">3. Selecting the P2% most promising subspaces SelectPromisingSubspaces(); class=class="str">"cmt">// class="num">4. Dividing promising subspaces into smaller ones DividePromisingSubspaces(); class=class="str">"cmt">// class="num">5. Generating new points taking into account potential ranks GenerateNewPopulation(); class=class="str">"cmt">// class="num">6. Random modification(mutation) MutatePoints(); } class="type">void C_AO_FBA::Revision() { class=class="str">"cmt">// Search for the best solution for (class="type">int i = class="num">0; i < popSize; i++) { class=class="str">"cmt">// Update the best solution if (a [i].f > fB) { fB = a [i].f; ArrayCopy(cB, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } } } class="type">void C_AO_FBA::CreateInitialSpacePartitioning() { class=class="str">"cmt">// Create an initial partition of space class="type">int totalSubspaces = (class="type">int)MathPow(m_value, coords); class=class="str">"cmt">// For very large dimensions, limit the number of subspaces if (totalSubspaces > class="num">10000) totalSubspaces = class="num">10000; ArrayResize(subspaces, totalSubspaces); class=class="str">"cmt">// Initialize all subspaces for (class="type">int i = class="num">0; i < totalSubspaces; i++) { subspaces [i].Init(coords); subspaces [i].level = class="num">0; class=class="str">"cmt">// Initial level } class=class="str">"cmt">// Divide the initial space into equal subspaces class="type">int index = class="num">0;
「空间切分的维度分支逻辑」
把特征空间切成均匀子空间时,代码按坐标维度走了三条路:一维直接均分,二维套两层循环,多维则用进位迭代。核心都是拿 rangeMax 减 rangeMin 再除以 m_value 得到步长,m_value 就是每条轴切几段。 一维场景里 intervalSize = (rangeMax[0] - rangeMin[0]) / m_value,循环 i 从 0 到 m_value-1,每个子空间 min 和 max 沿轴平移一个步长。二维把两条轴各自算 intervalSize0、intervalSize1,i、j 双循环铺满 m_value 乘 m_value 个格子。 维度大于 2 时硬写循环不现实,代码改用 indices[] 数组模拟进位:从最后一维加 1,满 m_value 归零并向前一位进 1,类似多进制计数器。当 c 退到 -1 说明所有组合已枚举完,直接 break。外汇与贵金属市场的高维特征空间划分涉及高风险,回测结论仅具概率意义。 下面这段是原文的分支实现,逐行拆完就能在 MT5 里改 m_value 看子空间数量变化:if(coords==1) 走一维均分;else if(coords==2) 走双层循环;else 进入多维迭代。注意 index < totalSubspaces 的守卫,避免越界写坏 subspaces 数组。
class=class="str">"cmt">// Select the division method depending on the dimensionality of the space if (coords == class="num">1) { class=class="str">"cmt">// One-dimensional case class="type">class="kw">double intervalSize = (rangeMax [class="num">0] - rangeMin [class="num">0]) / m_value; for (class="type">int i = class="num">0; i < m_value && index < totalSubspaces; i++) { subspaces [index].min [class="num">0] = rangeMin [class="num">0] + i * intervalSize; subspaces [index].max [class="num">0] = rangeMin [class="num">0] + (i + class="num">1) * intervalSize; index++; } } else if (coords == class="num">2) { class=class="str">"cmt">// Two-dimensional case class="type">class="kw">double intervalSize0 = (rangeMax [class="num">0] - rangeMin [class="num">0]) / m_value; class="type">class="kw">double intervalSize1 = (rangeMax [class="num">1] - rangeMin [class="num">1]) / m_value; for (class="type">int i = class="num">0; i < m_value && index < totalSubspaces; i++) { for (class="type">int j = class="num">0; j < m_value && index < totalSubspaces; j++) { subspaces [index].min [class="num">0] = rangeMin [class="num">0] + i * intervalSize0; subspaces [index].max [class="num">0] = rangeMin [class="num">0] + (i + class="num">1) * intervalSize0; subspaces [index].min [class="num">1] = rangeMin [class="num">1] + j * intervalSize1; subspaces [index].max [class="num">1] = rangeMin [class="num">1] + (j + class="num">1) * intervalSize1; index++; } } } else { class=class="str">"cmt">// Multidimensional case - use an iterative approach class="type">int indices []; ArrayResize(indices, coords); for (class="type">int i = class="num">0; i < coords; i++) indices [i] = class="num">0; class="kw">while (index < totalSubspaces) { class=class="str">"cmt">// Calculate the boundaries of the current subspace for (class="type">int c = class="num">0; c < coords; c++) { class="type">class="kw">double intervalSize = (rangeMax [c] - rangeMin [c]) / m_value; subspaces [index].min [c] = rangeMin [c] + indices [c] * intervalSize; subspaces [index].max [c] = rangeMin [c] + (indices [c] + class="num">1) * intervalSize; } class=class="str">"cmt">// Move on to the next subspace class="type">int c = coords - class="num">1; class="kw">while (c >= class="num">0) { indices [c]++; if (indices [c] < m_value) break; indices [c] = class="num">0; c--; } class=class="str">"cmt">// If the full loop completed, exit if (c < class="num">0) break; index++; } }
◍ 子空间筛选与优点的潜力排序
在基于种群的优化框架里,判断某个解是否落在指定子空间,是后续潜力统计的前提。下面这段逻辑用逐维边界比对,只要任一坐标越界就直接判否,效率比整体距离计算高出一个数量级。 bool C_AO_FBA::IsPointInSubspace (const double &point [], const S_Subspace &subspace) { for (int c = 0; c < coords; c++) {
| if (point [c] < subspace.min [c] | point [c] >= subspace.max [c]) |
|---|
return false; } return true; } 逐行拆解:函数接收待查点 point 与子空间 subspace;循环遍历 coords 个坐标维度;若某维数值小于该维下限或大于等于上限,立刻返回 false;全部维度通过才返回 true,说明点归属于该子空间。 IdentifyPromisingPoints 负责从种群里挑出前 P1% 的优解。它先把所有个体的适应度 a[i].f 抄进 values 数组,下标存进 indices,再调用 SortByFitness 做降序排列;随后用 MathRound(popSize*P1/100.0) 算出入选数量,并用 MathMax/MathMin 夹在 1 到 popSize 之间,避免 P1 设得过小或过大导致空选或全选。 void C_AO_FBA::IdentifyPromisingPoints (int &promisingIndices []) { double values []; int indices []; ArrayResize (values, popSize); ArrayResize (indices, popSize); for (int i = 0; i < popSize; i++) { values [i] = a [i].f; indices [i] = i; } SortByFitness (values, indices, popSize); int numPromisingPoints = (int)MathRound (popSize * P1 / 100.0); numPromisingPoints = MathMax (1, MathMin (numPromisingPoints, popSize)); ArrayResize (promisingIndices, numPromisingPoints); for (int i = 0; i < numPromisingPoints; i++) promisingIndices [i] = indices [i]; } 拆解:前两段建数组并扩容到种群规模;for 循环把适应度和原始序号对齐填充;排序后按百分比截取,最后把排好序的前 N 个原始下标写回 promisingIndices。 CalculateSubspaceRanks 先清零各子空间的 promisingRank,再拿优解逐个问「你归哪个子空间」,命中就给那个子空间计数加一并 break。因为一个点只可能落在一个子空间,break 能省掉剩余比对。外汇与贵金属市场的高波动可能让优解分布偏移,跑 MT5 时把 P1 从默认 10 调到 15 观察子空间热度变化会更直观。
class="type">bool C_AO_FBA::IsPointInSubspace(class="kw">const class="type">class="kw">double &point [], class="kw">const S_Subspace &subspace) { for (class="type">int c = class="num">0; c < coords; c++) { if (point [c] < subspace.min [c] || point [c] >= subspace.max [c]) class="kw">return false; } class="kw">return true; } class="type">void C_AO_FBA::IdentifyPromisingPoints(class="type">int &promisingIndices []) { class="type">class="kw">double values []; class="type">int indices []; ArrayResize(values, popSize); ArrayResize(indices, popSize); for (class="type">int i = class="num">0; i < popSize; i++) { values [i] = a [i].f; indices [i] = i; } SortByFitness(values, indices, popSize); class="type">int numPromisingPoints = (class="type">int)MathRound(popSize * P1 / class="num">100.0); numPromisingPoints = MathMax(class="num">1, MathMin(numPromisingPoints, popSize)); ArrayResize(promisingIndices, numPromisingPoints); for (class="type">int i = class="num">0; i < numPromisingPoints; i++) promisingIndices [i] = indices [i]; } class="type">void C_AO_FBA::CalculateSubspaceRanks(class="kw">const class="type">int &promisingIndices []) { for (class="type">int i = class="num">0; i < ArraySize(subspaces); i++) subspaces [i].promisingRank = class="num">0.0; for (class="type">int i = class="num">0; i < ArraySize(promisingIndices); i++) { class="type">int pointIndex = promisingIndices [i]; for (class="type">int j = class="num">0; j < ArraySize(subspaces); j++) { if (IsPointInSubspace(a [pointIndex].c, subspaces [j])) { subspaces [j].promisingRank++; break; } } } }
按排名筛出高潜力子空间并切分
候选子空间算完 promisingRank 后,需要按占比 P2% 挑出最值得继续细分的那些。代码里先建 ranks 和 indices 两个数组,把每个子空间的排名和原始下标存进去,同时把 isPromising 标志复位成 false,避免上一轮残留。 SortByFitness 做降序排列后,用 MathRound(numSubspaces * P2 / 100.0) 算出要保留的数量,再用 MathMax(1, MathMin(..., numSubspaces)) 夹一下边界——哪怕 P2 设得极小也至少留 1 个,设得极大也不超过总数。遍历前 numPromisingSubspaces 个下标,把对应子空间的 isPromising 置 true,这里还加了 indices[i] 越界保护。 挑完之后 DividePromisingSubspaces 只收集 isPromising 为真的下标,挨个调 DivideSubspace 做切分。外汇与贵金属市场波动剧烈、杠杆风险高,这套筛选逻辑在 MT5 里跑时,P2 取值倾向影响搜索粒度:P2 太小可能漏掉边缘有效区间,太大则计算量陡增。开 MT5 把 P2 从 10 调到 30,观察 subspaces 数组长度变化就能验证。
class=class="str">"cmt">// Select P2% subspaces with the highest ranks as promising ones class=class="str">"cmt">// Create arrays for sorting class="type">class="kw">double ranks []; class="type">int indices []; class="type">int numSubspaces = ArraySize(subspaces); ArrayResize(ranks, numSubspaces); ArrayResize(indices, numSubspaces); class=class="str">"cmt">// Fill in the arrays for (class="type">int i = class="num">0; i < numSubspaces; i++) { ranks [i] = subspaces [i].promisingRank; indices [i] = i; class=class="str">"cmt">// Reset the potential flag subspaces [i].isPromising = false; } class=class="str">"cmt">// Sort by descending ranks SortByFitness(ranks, indices, numSubspaces); class=class="str">"cmt">// Select P2% most promising subspaces class="type">int numPromisingSubspaces = (class="type">int)MathRound(numSubspaces * P2 / class="num">100.0); numPromisingSubspaces = MathMax(class="num">1, MathMin(numPromisingSubspaces, numSubspaces)); class=class="str">"cmt">// Mark promising subspaces for (class="type">int i = class="num">0; i < numPromisingSubspaces && i < ArraySize(indices); i++) { class=class="str">"cmt">// Protection against exceeding the array size if (indices [i] >= class="num">0 && indices [i] < ArraySize(subspaces)) { subspaces [indices [i]].isPromising = true; } } class="type">void C_AO_FBA::DividePromisingSubspaces() { class=class="str">"cmt">// Collect indices of promising subspaces class="type">int promisingIndices []; class="type">int numPromising = class="num">0; for (class="type">int i = class="num">0; i < ArraySize(subspaces); i++) { if (subspaces [i].isPromising) { numPromising++; ArrayResize(promisingIndices, numPromising); promisingIndices [numPromising - class="num">1] = i; } } class=class="str">"cmt">// Divide each promising subspace for (class="type">int i = class="num">0; i < numPromising; i++) { DivideSubspace(promisingIndices [i]); } }
「子空间裂变与新种群采样」
FBA 优化器在迭代中靠 DivideSubspace 把某个父空间切成 m_value^coords 个子空间,coords 是维度、m_value 是每维切分数。代码里硬卡了 ArraySize(subspaces) > 10000 就直接 return,说明空间数膨胀到万级会放弃继续细分,实盘跑高维参数寻优时得留意这个隐形天花板。 切片逻辑是逐维均分区间:intervalSize = (parent.max[c] - parent.min[c]) / m_value,新空间 min/max 按 indices[c] 偏移。indices 用进位制遍历,从末维加一、满 m_value 归零并向前进位,等效于在 coords 维网格上枚举全部组合。 GenerateNewPopulation 先累加所有子空间的 promisingRank 作为 totalRank。若 totalRank <= 0.0001 视为全零,则强制把每个空间 rank 置 1.0 并令 totalRank = 空间数,回退成均匀采样,避免早期无信号时种群塌缩到空集。 之后按 rank 占比向各子空间分配 popSize 个候选点(代码截断在循环头,分配细节在下一段)。外汇与贵金属波动剧烈,这种基于历史秩的采样在高波动段可能过拟合,建议用 MT5 策略测试器多周期验证再上实盘。
class="type">void C_AO_FBA::DivideSubspace(class="type">int subspaceIndex) { class=class="str">"cmt">// Divide the specified subspace into m_value^coords subspaces S_Subspace parent = subspaces [subspaceIndex]; class=class="str">"cmt">// Limit the maximum number of subspaces if (ArraySize(subspaces) > class="num">10000) class="kw">return; class=class="str">"cmt">// For each dimension, divide by m_value parts class="type">int totalNewSubspaces = (class="type">int)MathPow(m_value, coords); class="type">int currentSize = ArraySize(subspaces); ArrayResize(subspaces, currentSize + totalNewSubspaces); class=class="str">"cmt">// Create new subspaces class="type">int newIndex = currentSize; class="type">int indices []; ArrayResize(indices, coords); for (class="type">int i = class="num">0; i < coords; i++) indices [i] = class="num">0; for (class="type">int idx = class="num">0; idx < totalNewSubspaces && newIndex < ArraySize(subspaces); idx++) { subspaces [newIndex].Init(coords); subspaces [newIndex].level = parent.level + class="num">1; subspaces [newIndex].parentIndex = subspaceIndex; class=class="str">"cmt">// Calculate the boundaries of the current subspace for (class="type">int c = class="num">0; c < coords; c++) { class="type">class="kw">double intervalSize = (parent.max [c] - parent.min [c]) / m_value; subspaces [newIndex].min [c] = parent.min [c] + indices [c] * intervalSize; subspaces [newIndex].max [c] = parent.min [c] + (indices [c] + class="num">1) * intervalSize; } class=class="str">"cmt">// Move on to the next subspace class="type">int c = coords - class="num">1; class="kw">while (c >= class="num">0) { indices [c]++; if (indices [c] < m_value) break; indices [c] = class="num">0; c--; } class=class="str">"cmt">// If the full loop completed, exit if (c < class="num">0) break; newIndex++; } } class="type">void C_AO_FBA::GenerateNewPopulation() { class=class="str">"cmt">// Calculate the sum of the ranks of all subspaces class="type">class="kw">double totalRank = class="num">0.0; for (class="type">int i = class="num">0; i < ArraySize(subspaces); i++) { totalRank += subspaces [i].promisingRank; } class=class="str">"cmt">// If all ranks are class="num">0, set the uniform distribution if (totalRank <= class="num">0.0001) class=class="str">"cmt">// Check for approximate equality to zero { for (class="type">int i = class="num">0; i < ArraySize(subspaces); i++) { subspaces [i].promisingRank = class="num">1.0; } totalRank = ArraySize(subspaces); } class="type">int points = class="num">0; for (class="type">int i = class="num">0; i < ArraySize(subspaces) && points < popSize; i++) {