在算法交易中 KOHONEN 神经网络的实际应用 第二部分优化和预测(基础篇)
「从修正类到实盘:Kohonen 图的两类用法」
在系列前一篇里,我们已把公开的 Kohonen 神经网络类做了修正,补齐了训练所需的核心算法。类能跑通只是第一步,真正对交易者有用的是把它接进 MT5 的实际问题里。 这一节先定下落地方向:用 Kohonen 自组织映射图(SOM)做两件事——一是在 EA 参数空间里挑出较优组合,二是把价格序列投到网络上做形态预测。两者都不依赖未来函数,输入只来自已发生的行情与回测结果。 需要提醒,外汇与贵金属杠杆高、滑点随机,网络给出的参数倾向或形态概率只作筛选参考,不能直接当成下单依据。开 MT5 把前篇修正过的神经类挂上,先准备一组 EA 历史回测参数表,后面两节会直接套用到这张图上。
◍ 用聚类高原替代极值选参
MT5 测试器跑完优化,常吐出几千行报告。按利润或夏普排序,顶部往往是一堆数值相同、参数迥异的设置——遗传算法本就‘陷’在盈利通道里,把亏损版本早筛掉了,导致参数平面分布畸变,Kohonen 图容易漏掉好参数旁边的‘危险谷’。EURUSD D1 上 2018 年 1–6 月遗传优化 WizardTest,剔掉交易量低于 5 且夏普异常的条目、只留利润≥10000 后,csv 还剩约 2000 行,大量行盈利指标一致而参数不同。 选参不该追目标函数最大值,而应找函数值区间内最长持续的‘高原’:高原水平≈平均盈利能力,长度≈稳健性。用 K-Means 或 Kohonen 聚类搜高原时,簇数无通用标准——太多则过拟合、太小则欠拟合,上限受图边长约束(前述公式 7 定边长 15,则簇数不超 15),建议多跑几次实验。 为让网络只按 EA 参数或只按经济指标聚,给 CSOMNode 加 dimensionMax 与 dimensionBitMask:前者截断末位组件,后者用位掩码随机屏蔽。训练前设好掩码,距离计算即排除指定维;默认 ApplyFeatureMaskAfterTraining=false,若置 true 则在 Train 后调 SetFeatureMask,支持‘无掩码训练、掩码找簇’。 实测:15×15 网跑 100 代,全特征聚出的簇 0 正向测试利润不比随机首行好;改用前 6 维(仅 EA 参数)掩码重聚,U 矩阵与簇结构明显变样。从约 2000 行里专家肉眼挑右上角稳定区,正向利润跑赢均值 1007+标准差 1444;自动簇 0 无掩码时明显逊于专家。外汇/贵金属实盘参数敏感,左一步右一步进损失谷的概率不低,掩码隔离经济指标是避坑硬操作。
用RGB映射替代神经元邻近度来挑参数
Kohonen 图的聚类质量直接决定 EA 最优参数搜索的准确度,但比对神经元空间邻近度这套做法有点重。换一条路:把特征权重直接画进 RGB 色彩空间,挑最浅的神经元,相当于用肉眼做三维特征排序。
RGB 模型正好塞得下 3 个特征平面。特征多于 3 个时就退化为灰度渐变,但最浅点依旧可定位。挑选特征时尽量取相互独立的量,前文已提过这一点。
实现上不必重写整套 SOM 逻辑,直接从 CSOMDisplay 派生 CSOMDisplayRGB,只覆写几个虚方法,让显示器输出 RGB 映射而非最后一个 DIM_OUTPUT 平面。SOM-Explorer-RGB 改造版只增加了一个输入参数开启 RGB 模式,图对象换成派生类,显示分支单独走。
复用已有的 FeatureDirection 参数就能指定进 RGB 空间的特征。Wizard2018plus 示例里写 ++,-,,,:利润、PF 进图(对应 +),回撤反向进图(对应 -),其余逗号跳过。附带文件 WizardTest-rgb.set 里网络文件 Wizard2018plus.som 沿用上一阶段产物。
左侧 RGB 空间里利润=红、PF=绿、回撤=蓝,最亮神经元标了「RGB」,日志打出其坐标与特征值作为候选。因利润与 PF 强相关,换掩码跳过利润、加交易数(部分券商按交易量返佣),右侧颜色映射变成 PF=红、回撤=绿、交易数=蓝。
只留 PF 和回撤就掉回灰度图;单留利润则黑白模式与第一平面完全对应,可用来校验渲染没出错。
把正向测试接进来分析也顺手:MetaTrader 测试器优化后带 forward 测试,把 WizardTest 的优化(20180101-20180701)与 forward(20180701-20181201)合并,只留过去/未来利润两列生成 Wizard2018-with-forward.csv。全特征教学后,未来利润区域明显比过去利润区小,零集群(红)含该位置;按 ++ 掩码取最浅点也落此 cluster,但中心与浅点都不含超级盈利设置——比早前结果差约两倍。
改用 FeatureMask=1100000 只留前两特征重连网络,左下角高值区在末图被高亮,取该神经元设置做 tester 报告,同样弱于先前。实盘实验没捞出最佳参数,但把 forward 结果喂进神经网分析的方向是对的,本例测试 EA 可能本就不适合稳定交易,细节可在评论区拆。
class="kw">static class="type">int dimensionMax; class="kw">static class="type">class="kw">ulong dimensionBitMask; class="kw">static class="type">void CSOMNode::SetFeatureMask(class="kw">const class="type">int dim = class="num">0, class="kw">const class="type">class="kw">ulong bitmask = class="num">0) { dimensionMax = dim; dimensionBitMask = bitmask; } class="type">class="kw">double CSOMNode::CalculateDistance(class="kw">const class="type">class="kw">double &vector[]) class="kw">const { class="type">class="kw">double distSqr = class="num">0; if(dimensionMax <= class="num">0 || dimensionMax > m_dimension) dimensionMax = m_dimension; for(class="type">int i = class="num">0; i < dimensionMax; i++) {
「用位掩码挑特征维度」
自组织映射(SOM)里不是所有输入维度都得参与距离计算。代码用 dimensionBitMask 做开关:某位为 1 才把该维的 (vector[i]-m_weights[i])² 累加进 distSqr,全 0 则退化为全维度欧氏距离。 SetFeatureMask 把外部传进来的 ulong 位掩码存进 m_featureMask,并遍历 0..m_dimension-1 统计置位个数到 m_featureMaskSize,同时 Print 出对应 m_titles[i] 方便在 MT5 日志里核对启用了哪些特征。 从字符串解析掩码时,FeatureMask 参数最长截 64 字符(MathMin(StringLen,64)),逐字符遇 '1' 就把 1 左移 i 位 OR 进 mask,再交给 KohonenMap.SetFeatureMask(0,mask)。 LoadSettings 读文本配置:以 ';' 开头的行跳过,按 '=' 拆名值,再按 '|' 拆首个浮点写进 v[index];只装载 KohonenMap.FindFeature 能查到的特征,最后打印实际命中数 count。外汇与贵金属行情高波动,这类特征筛选仅降低维度噪声,不预示方向。
if(dimensionBitMask == class="num">0 || ((dimensionBitMask & (class="num">1 << i)) != class="num">0)) { distSqr += (vector[i] - m_weights[i]) * (vector[i] - m_weights[i]); } } class="kw">return distSqr; } class="type">void CSOM::SetFeatureMask(class="kw">const class="type">int dim, class="kw">const class="type">class="kw">ulong bitmask) { m_featureMask = class="num">0; m_featureMaskSize = class="num">0; if(bitmask != class="num">0) { m_featureMask = bitmask; Print("启用了特性掩码:"); for(class="type">int i = class="num">0; i < m_dimension; i++) { if((bitmask & (class="num">1 << i)) != class="num">0) { m_featureMaskSize++; Print(m_titles[i]); } } } CSOMNode::SetFeatureMask(dim == class="num">0 ? m_dimension : dim, bitmask); } class="type">class="kw">ulong mask = class="num">0; if(FeatureMask != "") { class="type">int n = MathMin(StringLen(FeatureMask), class="num">64); for(class="type">int i = class="num">0; i < n; i++) { mask |= (StringGetCharacter(FeatureMask, i) == &class="macro">#x27;class="num">1&class="macro">#x27; ? class="num">1 : class="num">0) << i; } } KohonenMap.SetFeatureMask(class="num">0, mask); class="type">bool LoadSettings(class="kw">const class="type">class="kw">string filename, class="type">class="kw">double &v[]) { class="type">int h = FileOpen(filename, FILE_READ | FILE_TXT); if(h == INVALID_HANDLE) { Print("FileOpen 错误 ", filename, " : ",GetLastError()); class="kw">return class="kw">false; } class="type">int n = KohonenMap.GetFeatureCount(); ArrayResize(v, n); ArrayInitialize(v, EMPTY_VALUE); class="type">int count = class="num">0; class="kw">while(!FileIsEnding(h)) { class="type">class="kw">string line = FileReadString(h); if(StringFind(line, ";") == class="num">0) class="kw">continue; class="type">class="kw">string name2value[]; if(StringSplit(line, &class="macro">#x27;=&class="macro">#x27;, name2value) != class="num">2) class="kw">continue; class="type">int index = KohonenMap.FindFeature(name2value[class="num">0]); if(index != -class="num">1) { class="type">class="kw">string values[]; if(StringSplit(name2value[class="num">1], &class="macro">#x27;|&class="macro">#x27;, values) > class="num">0) { v[index] = StringToDouble(values[class="num">0]); count++; } } } Print("Settings loaded: ", filename, "; features found: ", count); class="type">class="kw">ulong mask = class="num">0; for(class="type">int i = class="num">0; i < n; i++) { if(v[i] != EMPTY_VALUE) {
◍ 把.set配置喂给自组织映射做模式匹配
这段逻辑处理的是以 .set 结尾的文件:先判断文件名末四位是否为「.set」,是则把文件里的参数数组读进 double 型 v[],再交给 LoadSettings 解析。若读取成功(count>0),就通过 KohonenMap.AddPattern(v,"SETTINGS") 把这组 EA 参数当作一个模式塞进自组织映射网络。 匹配阶段调用 GetBestMatchingFeatures(v,y) 找出最相近的节点,打印其坐标、命中次数 node.GetHitsCount()、均方误差 node.GetMSE() 和聚类编号。随后 CalculateOutput(v,true) 强制重算输出,hasOneTestPattern 置真,意味着后续不再随机生成样本而是拿这组真实配置跑网络。 特征掩码那几行也别忽略:循环里若第 i 维有效就 mask |= (1<<i) 置位,否则 v[i]=0;mask 非 0 时 KohonenMap.SetFeatureMask(0,mask) 只让有效维度参与距离计算,避免空特征污染节点权重。 下面这段 input 是 EA 外壳参数,MagicNumber 写死 17897,开仓/平仓信号阈值都是 10(取值区间 0~100),止损止盈各 50 点。外汇与贵金属杠杆高、滑点跳空频繁,这类默认 50 点止损在黄金分钟线可能一两根影线就被扫,实盘前务必在 MT5 策略测试器里按品种波动重调。
mask |= (class="num">1 << i); } else { v[i] = class="num">0; } } if(mask != class="num">0) { KohonenMap.SetFeatureMask(class="num">0, mask); } FileClose(h); class="kw">return count > class="num">0; } class=class="str">"cmt">// 以特定模式方式处理 .set 文件 if(StringFind(DataFileName, ".set") == StringLen(DataFileName) - class="num">4) { class="type">class="kw">double v[]; if(LoadSettings(DataFileName, v)) { KohonenMap.AddPattern(v, "SETTINGS"); ArrayPrint(v); class="type">class="kw">double y[]; CSOMNode *node = KohonenMap.GetBestMatchingFeatures(v, y); Print("Matched Node Output(", node.GetX(), ",", node.GetY(), "); Hits:", node.GetHitsCount(), "; Error:", node.GetMSE(), "; Cluster N", node.GetCluster(), ":"); ArrayPrint(y); KohonenMap.CalculateOutput(v, true); hasOneTestPattern = true; } } class="kw">input class="type">class="kw">string Expert_Title ="WizardTest"; class=class="str">"cmt">// 文档名称 class="type">class="kw">ulong Expert_MagicNumber =class="num">17897; class=class="str">"cmt">// class="type">bool Expert_EveryTick =class="kw">false; class=class="str">"cmt">// class=class="str">"cmt">//--- 主信号的输入参数 class="kw">input class="type">int Signal_ThresholdOpen =class="num">10; class=class="str">"cmt">// open [class="num">0...class="num">100] 的信号阈值 class="kw">input class="type">int Signal_ThresholdClose =class="num">10; class=class="str">"cmt">// close [class="num">0...class="num">100] 的信号阈值 class="kw">input class="type">class="kw">double Signal_PriceLevel =class="num">0.0; class=class="str">"cmt">// 执行交易的价格水平 class="kw">input class="type">class="kw">double Signal_StopLevel =class="num">50.0; class=class="str">"cmt">// 止损水平 (点数) class="kw">input class="type">class="kw">double Signal_TakeLevel =class="num">50.0; class=class="str">"cmt">// 获利水平 (点数)