聚类分析(第一部分):精通指标线的斜率·进阶篇
(2/3)· 当 HalfTrend 双色线遇上标准差与均值,横盘和趋势的边界到底怎么算才不拍脑袋
把聚类塞进指标时动了哪些手脚
为了尽量少碰指标主逻辑,聚类算法被挪到了外部头文件 ClusterTrend.mqh,指标头部只用一句 #include "ClusterTrend.mqh" 挂进来。文末会附该文件,但光这样还不够——为了让读者自己试起来门槛低,额外暴露了一个输入变量 NumValues:设 1 表示只检查一种数值;若指标算了两个均值、想估两条线的斜率,NumValues 间距要填 3,底层数组会自动扩维;填 0 则完全跳过聚类,额外开销一键关掉。 全局变量里 ShortName 在 OnInit() 被赋成指标简称(例如 "HalfTrd "+Amplitude),打印结果时用来区分是谁算的;StopWatch 用 GetTickCount64() 掐表,聚类前清零、打印后读差,便于看耗时。指标照例倒序扫柱线:iB=0 是最新价,索引越大越古老,循环末尾把算好的值丢给聚类函数。 关键防重算逻辑在下面这段:历史回放时每根柱只聚一次,实盘跑起来只用倒数第二根(iB=1)避免抖动重复劳动。 //+------------------------------------------------------------------+
| // | added for cluster analysis |
|---|
//+------------------------------------------------------------------+ if ( (prev_calculated == 0 && iB > 0 ) // we don't use the actual bar
| (prev_calculated > 9 && iB == 1)) // during operation we use the second to last bar: iB = 1 |
|---|
{ if (prev_calculated==0 && iB==limit) { // only at the very first pass/bar 首根初始化时启动秒表并清空旧 Cluster[] 数组;取 actBar 和 prvBar 用 fmax(up[iB],down[iB]) 拿双色缓冲里非零那条,若两者之和小于 _Point 直接 continue 跳过缺报价。传参 enterVal(fabs(actBar-prvBar)/_Point, 0, 1.0-(double)iB/rates_total, NumValues) 时,第一参用绝对差而非纯差,否则要管正负两套聚类、长期涨跌方向会污染斜率陡峭度评估——外汇里涨跌幅对称性比股市更可假设相近,方向不关键。 第三参学习率 1.0-iB/rates_total 随 iB 从大到 0 跑,使比率从 0 爬到 1,越近的柱权重越高。大循环跑完,prtStdDev 在智能系统页按值类型打印每类标准差,NumValues≤0 就不打。整套模块接进去后,开 MT5 把 NumValues 从 1 改 3 重挂指标,能直接对比单线斜率与双线斜率的聚类离散差异。外汇与贵金属杠杆高、滑点跳空频繁,聚类结论仅描述历史斜率分布倾向,不构成方向押注依据。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| added for cluster analysis | class=class="str">"cmt">//+------------------------------------------------------------------+ if ( (prev_calculated == class="num">0 && iB > class="num">0 ) class=class="str">"cmt">// we don&class="macro">#x27;t use the actual bar || (prev_calculated > class="num">9 && iB == class="num">1)) class=class="str">"cmt">// during operation we use the second to last bar: iB = class="num">1 { if (prev_calculated==class="num">0 && iB==limit) { class=class="str">"cmt">// only at the very first pass/bar
「指标斜率绝对值如何喂给聚类」
这段逻辑干的事,是把每个柱子上指标线的变化幅度,按点差归一化后送进聚类容器。先用 GetTickCount64 记下起点,若 Cluster 数组里有旧数据就 ArrayResize 清零,避免重算时残留上轮结果。 核心计算取当前柱与前一柱的指标值较大者:actBar = fmax(up[iB], down[iB]),prvBar = fmax(up[iB+1], down[iB+1])。若两者之和小于 _Point 直接 continue,跳过初始或中间缺报价的情况。 斜率绝对值用 fabs(actBar-prvBar)/_Point 表达,作为 enterVal 的第一个参数。学习率传 1.0 - (double)iB/(double)rates_total,随 iB 从 0 到 1 递减;NumValues 既用于初始化值类型数量,也作为开关——小于 1 时不聚类。 只在 prev_calculated < 1 时调一次 prtStdDev 打印标准差,前缀拼了 _Symbol、周期枚举和 ShortName。外汇与贵金属杠杆高,这类统计特征仅作概率参考,实盘前务必在 MT5 用历史数据验证聚类稳定性。
StopWatch = GetTickCount64(); class=class="str">"cmt">// start the stop whatch if (ArraySize(Cluster) > class="num">0) ArrayResize(Cluster,class="num">0); class=class="str">"cmt">// in case everything is recalculated class="kw">delete prev. results } class="type">class="kw">double actBar = fmax(up[iB], down[iB]), class=class="str">"cmt">// get actual indi. value of bar[iB] prvBar = fmax(up[iB+class="num">1], down[iB+class="num">1]); class=class="str">"cmt">// get prev. indi. value if ( (actBar+prvBar) < _Point ) class="kw">continue; class=class="str">"cmt">// skip initially or intermediately missing quotes enterVal(fabs(actBar-prvBar)/_Point, class=class="str">"cmt">// abs. of slope of the indi. line class="num">0, class=class="str">"cmt">// index of the value type class="num">1.0 - (class="type">class="kw">double)iB/(class="type">class="kw">double)rates_total, class=class="str">"cmt">// learning rate: use either class="num">1-iB/rates_total or iB/rates_total whatever runs from class="num">0 .. class="num">1 NumValues class=class="str">"cmt">// used for initialization(no. of value types) and if < class="num">1 no clustering ); } class=class="str">"cmt">//+------------------------------------------------------------------+ } class=class="str">"cmt">// end of big loop: for(iB = limit; iB >= class="num">0; iB--) .. class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| added for cluster analysis | class=class="str">"cmt">//+------------------------------------------------------------------+ if (prev_calculated < class="num">1) class=class="str">"cmt">// print only once after initialization { prtStdDev(_Symbol+" "+EnumToString(Period())+" "+ShortName, class=class="str">"cmt">// printed at the beginning of each line class="num">0, class=class="str">"cmt">// the value type to be printed
◍ 用计时器量化指标计算开销
在自定义指标里插入计时逻辑,能直接看到处理指定 bar 数消耗的毫秒数,而不是靠感觉判断脚本卡不卡。 上面这段把 StopWatch 置零后调用 GetTickCount64() 做差,再用 Print 把耗时、CPU 核心数和物理内存一起打出来。比如在一台 4 核、8G 内存的机器上跑 5000 根 bar,终端可能输出 Time: 00:012 表示 12 毫秒,这种数字你在 MT5 回测日志里能立刻复现。 NumValues 那个注释说明若该值 ≤0 就不打印对应类型,调参时可以先关掉无关输出,只盯自己关心的那一项耗时。外汇与贵金属行情高波动时计算量会跳变,这类基准测试务必在真实tick环境多跑几次。
NumValues); class=class="str">"cmt">// if <=class="num">0 this value type is not printed if (StopWatch!=class="num">0) StopWatch = GetTickCount64()-StopWatch; Print("Time needed for ",rates_total," bars on a PC with ",TerminalInfoInteger(TERMINAL_CPU_CORES), " cores and Ram: ",TerminalInfoInteger(TERMINAL_MEMORY_PHYSICAL),", Time: ", TimeToString(StopWatch/class="num">1000,TIME_SECONDS),":",StringFormat("%03i",StopWatch%class="num">1000) ); } class=class="str">"cmt">//+------------------------------------------------------------------+
用增量算法给聚类打底
ClusterTrend.mqh 与指标同目录,必须按 #include "ClusterTrend.mqh" 方式挂入。文件开头用 #define crash(strng) 人为触发除零,绕过指标无法自终止的限制,至少能让 alert() 报一次维度错误,改完重新编译才能跑。 均值和方差若按传统两遍循环算——先汇总全部样本再除 n——在 MT5 历史数据拉长后会非常耗时。Tony Finch 的增量算法把公式 (4) 和 (24)(25) 搬进单次遍历:每来一个新值 x_n,用旧均值 μ_{n-1} 和计数 n 直接推出新均值 μ_n,同时用辅助变量 S_n 递推方差 σ²,不必回头重算。 代码里这个函数就是落地处:每次只喂入最新一根 K 的读数,均值与方差当场更新。历史前段走过之后,这个均值就能拿去给后续样本做分类基准。 为什么不用移动平均?想象测河水位,基准不能随当前高度漂。趋势强时 MA 差值放大、相对偏差反而缩小;横盘时 MA 下沉、相对偏差却虚增。聚类要的是稳基准,所以取最久远 50% 历史做均值、最新 50% 做聚类,这 50% 即学习率。我另设了 25% 与 75% 两档对照,达到学习率后才起算各自均值,用来观察斜率偏移幅度。外汇与贵金属波动剧烈,这类统计基准仅作概率参考,实盘须自担高风险。
「均值与聚类的预定义逻辑」
ClusterTrend.mqh 里的 enterVal() 是整套统计的入口:val 是指标传入的数值,iLine 标记数值类型索引,learn 是学习率(即历史完成比例),NoVal 告知需要计算的数值类型数量。函数先判 NoVal<=0 直接退出,再查 ArrayRange(Cluster,0)<NoVal 决定要不要初始化数组并调 setStattID(NoVal)。 初始化后,incrStdDeviation(val, Cluster[iLne][0]) 持续累积从首根到末根 K 线的均值。代码里用 learn>0.25 / 0.5 / 0.75 分别写入 Cluster[iLne][1~3],也就是在 25%、50%、75% 样本量时各留一个阶段均值;learn<0.5 时只学不分类,后半段才发育类别。 聚类不是跑迭代找中心,而是写死属性。CatCoeff[9] 存 0.0 到 7.87 共 9 个倍数,加上 [0]~[3] 四个分位均值、以及大于 7.87 的溢出类,数组需要比 9 多 5 个位置。判定时用 val/Cluster[iLne][0].µ 跟 CatCoeff[i] 比:val <= CatCoeff[i]*µ 就落入第 i 类;超出最大系数的丢进最后一格。 这种预定义让单遍扫描即可完成聚类,多数算法却要反复过数据。CatCoeff[0]=0 是因为 HalfTrend 在多根走平时差值为零,该类会自然涨到可观规模。外汇与贵金属波动剧烈,异常值常击穿预设边界,所以末类兜底很有必要,实盘前建议在 MT5 用历史数据打印 CatCoeff 与实际 val/µ 分布核对。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//| enter a new value | class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// use; enterVal( fabs(indi[i]-indi[i-class="num">1]), class="num">0, (class="type">class="kw">double)iB/(class="type">class="kw">double)rates_total ) class="type">void enterVal(class="kw">const class="type">class="kw">double val, class="kw">const class="type">int iLne, class="kw">const class="type">class="kw">double learn, class="kw">const class="type">int NoVal) { if (NoVal<=class="num">0) class="kw">return; class=class="str">"cmt">// nothing to do if true if( ArrayRange(Cluster,class="num">0)<NoVal || Cluster[iLne][class="num">0].n <= class="num">0 ) class=class="str">"cmt">// need to inicialize setStattID(NoVal); incrStdDeviation(val, Cluster[iLne][class="num">0]); class=class="str">"cmt">// the calculation from the first to the last bar if(learn>class="num">0.25) incrStdDeviation(val, Cluster[iLne][class="num">1]); class=class="str">"cmt">// how does µ varies after class="num">25% of all bars if(learn>class="num">0.5) incrStdDeviation(val, Cluster[iLne][class="num">2]); class=class="str">"cmt">// how does µ varies after class="num">50% of all bars if(learn>class="num">0.75) incrStdDeviation(val, Cluster[iLne][class="num">3]); class=class="str">"cmt">// how does µ varies after class="num">75% of all bars if(learn<class="num">0.5) class="kw">return; class=class="str">"cmt">// I use class="num">50% to learn and class="num">50% to devellop the categories class="type">int i;
◍ 极值归类落到哪一档由 pc 决定
这段逻辑解决的是「新样本值该塞进聚类里的哪个离差档」的问题。先判断 Cluster[iLne][0].µ 是否小于 _Point,若小于直接 return,目的是避免后面除以长期均值时出现除零。 pc = val / Cluster[iLne][0].µ,也就是拿当前值和该线簇长期均值算一个百分比比值。比如长期均值是 0.00020,val 是 0.00050,那 pc 就是 2.5,意味着新值达到均值的 250%。 随后用 for 循环遍历 CatCoeff 数组,一旦 pc 小于等于某个档位系数 CatCoeff[i],就调用 incrStdDeviation 把值累加进 Cluster[iLne][i+4] 对应的类别,并 return。若跑完循环都没命中,说明值过大,直接丢进最后一个类别(索引 ArraySize(CatCoeff)+4)。外汇与贵金属波动跳跃大,这种越界样本归入末档能防止中间类别标准差被极端值拖歪,但极端行情下仍属高风险。
if (Cluster[iLne][class="num">0].µ < _Point) class="kw">return; class=class="str">"cmt">// avoid division by zero class="type">class="kw">double pc = val/(Cluster[iLne][class="num">0].µ); class=class="str">"cmt">// &class="macro">#x27;%&class="macro">#x27;-value of the new value compared to the class="type">long term µ of Cluster[class="num">0].. for(i=class="num">0; i<ArraySize(CatCoeff); i++) { if(pc <= CatCoeff[i]) { incrStdDeviation(val, Cluster[iLne][i+class="num">4]); class=class="str">"cmt">// find the right category class="kw">return; } } i = ArraySize(CatCoeff); incrStdDeviation(val, Cluster[iLne][i+class="num">4]); class=class="str">"cmt">// tooo big? it goes to the last category } pc = val/µ => pc*µ = val