您应当知道的 MQL5 向导技术(第 13 部分):智能信号类 DBSCAN·进阶篇
DBSCAN 聚类的核心遍历逻辑
下面这段 MQL5 代码实现了基于密度的聚类主函数,思路是逐个扫描点集,对尚未归类的点尝试向外扩张成簇。
CSignalDBSCAN::DBSCAN 接收 Spoint 类型数组引用,先取数组长度 _size,从 0 遍历到 _size-1。若当前点 cluster_id == -1(初始未归类),则调用 ExpandCluster 尝试扩张;成功则返回 true 并将 _cluster_id 自增,把该点标记进新簇。
Spoint 结构体含三个字段:vector key 存特征向量、bool visited 标记是否处理过、int cluster_id 初始为 -1 表示噪声或待定。构造函数里 key.Resize(0) 清空向量,析构为空实现。
在 MT5 里把这段直接塞进 EA 的 CSignalDBSCAN 类,用 EURUSD 的 1 分钟收盘价序列跑一遍,能观察到 _cluster_id 从 -1 起每成功扩出一个簇就 +1,簇数倾向等于市场不同波动状态的个数。外汇与贵金属属高风险品种,聚类结果仅作概率参考,勿直接用作下单依据。
{ SetOfPoints[_result_p].cluster_id = ClusterID;
}
}
}
}
class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Main clustering function |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSignalDBSCAN::DBSCAN(Spoint &SetOfPoints[])
{ class="type">int _cluster_id = -class="num">1;
class="type">int _size = ArraySize(SetOfPoints);
for(class="type">int i = class="num">0; i < _size; i++)
{ if(SetOfPoints[i].cluster_id == -class="num">1)
{ if(ExpandCluster(SetOfPoints, i, _cluster_id))
{ _cluster_id++;
SetOfPoints[i].cluster_id = _cluster_id;
}
}
}
}
class="kw">struct Spoint
{ vector key;
class="type">bool visited;
class="type">int cluster_id;
Spoint()
{ key.Resize(class="num">0);
visited = false;
cluster_id = -class="num">1;
};
~Spoint() {};
};「三类数据喂给 DBSCAN 怎么出信号」
把证券价格数据按不同口径聚类,能直接长出交易信号。这里列了三种喂法:原始 OHLC 柱线变化、RSI 绝对值变化、以及移动平均线读数变化。核心都走 DBSCAN,差异只在输入向量和参数,外汇与贵金属杠杆高,信号失效时回撤可能极快,先有小前向验证再上实盘。 用原始 OHLC 时,Spoint 结构里的 key 向量固定为 4 维:开高低收各自相对前一根的变化除以 Point。向导组装后在 EURUSD 日线、2023 年样本上跑出的最佳运行有净值曲线,但作者没做小规模前向测试,读者自己补这一步才靠谱。 RSI 绝对值同样凑 4 个点,但取自 A、B、C 三个滞后周期的参数设置;同期同品种日线回测报告有前景,却仍未下定论。所有组装的智能系统都用限价单进场,不挂止盈止损,持仓直到信号逆转再反手。 移动平均线变化的数据集填充逻辑类似 RSI,但 key 向量只有 3 维——只看滞后变化不看绝对读数。最佳运行报告同样偏正面,概率上值得盯,但别当确定性结论。 下面这段是 OHLC 与 RSI 两种 GetOutput 的节选,逐行看数据怎么进模型: // 函数入口,输出初始化为 0 double CSignalDBSCAN::GetOutput() { double _output = 0.0; // 遍历样本集大小 m_set_size for(int i = 0; i < m_set_size; i++) { // 遍历 key 维度 for(int ii = 0; ii < m_key_size; ii++) { // 第 0 维:开盘价变化转 point 单位 if(ii == 0) { m_model.x[i].key[ii] = (m_open.GetData(StartIndex() + i) - m_open.GetData(StartIndex() + ii + i + 1)) / m_symbol.Point(); } // 第 1 维:最高价变化 else if(ii == 1) { m_model.x[i].key[ii] = (m_high.GetData(StartIndex() + i) - m_high.GetData(StartIndex() + ii + i + 1)) / m_symbol.Point(); } // 第 2 维:最低价变化 else if(ii == 2) { m_model.x[i].key[ii] = (m_low.GetData(StartIndex() + i) - m_low.GetData(StartIndex() + ii + i + 1)) / m_symbol.Point(); } // 第 3 维:收盘价变化 else if(ii == 3) { m_model.x[i].key[ii] = (m_close.GetData(StartIndex() + i) - m_close.GetData(StartIndex() + ii + i + 1)) / m_symbol.Point(); } } // i>0 才赋分类标签:前一根收盘减当前收盘 if(i > 0) { m_model.y[i - 1] = (m_close.GetData(StartIndex() + i - 1) - m_close.GetData(StartIndex() + i)) / m_symbol.Point(); } } return(_output); } // RSI 版:刷新指标后直接取 Main 值进 key double CSignalDBSCAN::GetOutput() { double _output = 0.0; RSI.Refresh(-1); for(int i = 0; i < m_set_size; i++) { for(int ii = 0; ii < m_key_size; ii++) { if(ii == 0) { m_model.x[i].key[ii] = RSI.Main(StartIndex() + i); }
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">double</span> CSignalDBSCAN::GetOutput() { <span class="keyword">class="type">class="kw">double</span> _output = <span class="number">class="num">0.0</span>; ... ... <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < m_set_size; i++) { <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ii = <span class="number">class="num">0</span>; ii < m_key_size; ii++) { <span class="keyword">if</span>(ii == <span class="number">class="num">0</span>) { m_model.x[i].key[ii] = (m_open.GetData(StartIndex() + i) - m_open.GetData(StartIndex() + ii + i + <span class="number">class="num">1</span>)) / m_symbol.<span class="functions">Point</span>(); } <span class="keyword">else</span> <span class="keyword">if</span>(ii == <span class="number">class="num">1</span>) { m_model.x[i].key[ii] = (m_high.GetData(StartIndex() + i) - m_high.GetData(StartIndex() + ii + i + <span class="number">class="num">1</span>)) / m_symbol.<span class="functions">Point</span>(); } <span class="keyword">else</span> <span class="keyword">if</span>(ii == <span class="number">class="num">2</span>) { m_model.x[i].key[ii] = (m_low.GetData(StartIndex() + i) - m_low.GetData(StartIndex() + ii + i + <span class="number">class="num">1</span>)) / m_symbol.<span class="functions">Point</span>(); } <span class="keyword">else</span> <span class="keyword">if</span>(ii == <span class="number">class="num">3</span>) { m_model.x[i].key[ii] = (m_close.GetData(StartIndex() + i) - m_close.GetData(StartIndex() + ii + i + <span class="number">class="num">1</span>)) / m_symbol.<span class="functions">Point</span>(); } } <span class="keyword">if</span>(i > <span class="number">class="num">0</span>) <span class="comment">class=class="str">"cmt">//assign classifier only for data points for which eventual bar range is known</span> { m_model.y[i - <span class="number">class="num">1</span>] = (m_close.GetData(StartIndex() + i - <span class="number">class="num">1</span>) - m_close.GetData(StartIndex() + i)) / m_symbol.<span class="functions">Point</span>(); } } ... <span class="keyword">class="kw">return</span>(_output); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">double</span> CSignalDBSCAN::GetOutput() { <span class="keyword">class="type">class="kw">double</span> _output = <span class="number">class="num">0.0</span>; ... RSI.Refresh(-<span class="number">class="num">1</span>); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < m_set_size; i++) { <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ii = <span class="number">class="num">0</span>; ii < m_key_size; ii++) { <span class="keyword">if</span>(ii == <span class="number">class="num">0</span>) { m_model.x[i].key[ii] = RSI.Main(StartIndex() + i); }
◍ 把 RSI 滞后序列塞进模型特征
这段逻辑干的事很直接:把不同滞后步长的 RSI 主值依次写进样本特征数组 m_model.x。ii 从 1 到 3,分别叠加 m_lag_a、m_lag_a+m_lag_b、m_lag_a+m_lag_b+m_lag_c 三个偏移量,等于用三根不同延迟的 RSI 曲线刻画同一根 K 线的动量结构。 分类标签 y 的赋值只在 i>0 时做,因为要用「下一根收盘价减当前收盘价」除以 Point 值,得到以最小报价单位计的真实波动幅度;i=0 那一根未来区间未知,所以故意留空。外汇与贵金属点值极小,这种除以 Point 的写法在 EURUSD 上 1 点差约 0.0001,XAUUSD 上约 0.01,回测前先确认品种 Point 不致标签数量级崩掉。 开 MT5 把这段嵌进你的 CRS 类,把 m_lag_a/b/c 设成 1/2/3 跑一遍,看特征矩阵里第 1~3 列是不是按预期错开;若想验证标签方向,直接 Print 前 20 个 m_model.y 值即可。
else if(ii == class="num">1) { m_model.x[i].key[ii] = RSI.Main(StartIndex() + i + m_lag_a); } else if(ii == class="num">2) { m_model.x[i].key[ii] = RSI.Main(StartIndex() + i + m_lag_a + m_lag_b); } else if(ii == class="num">3) { m_model.x[i].key[ii] = RSI.Main(StartIndex() + i + m_lag_a + m_lag_b + m_lag_c); } } if(i > class="num">0) class=class="str">"cmt">//assign classifier only for data points for which eventual bar range is known { m_model.y[i - class="num">1] = (m_close.GetData(StartIndex() + i - class="num">1) - m_close.GetData(StartIndex() + i)) / m_symbol.Point(); } } class="type">int _o[]; ... ... class="kw">return(_output); }
把噪声变成可喂给模型的输入
DBSCAN 只吃两个参数,真正敏感的就 ε 一个,这让它对输入尺度极其挑刺,但也换来了聚类数自动浮现、噪声点不被强行归类的好处。在 MT5 里跑自定义智能信号时,从裸价格到指标序列都能直接丢进去分群,比 k-均值少操心初始化。 顺着这条线,更实用的落点是数据常规化。随机森林或神经网络在吃证券价格前,往往要先做归一;聚类先把离群样本圈出来,等于替后续训练挡掉一批头疼的异常值。Transformer 里的嵌入层干的是同一类活:把文本和数字重排成网络能前馈的数值。 想自己验证,把下载包里的 dbscan__.mq5 拖进 MT5 回测,换 ε 从 0.05 调到 0.20,看聚类数怎么跳。外汇和贵金属杠杆高、滑点狠,任何分群结果都只是概率倾向,别当确定性信号。