您应当知道的 MQL5 向导技术(第 13 部分):智能信号类 DBSCAN(基础篇)
用 DBSCAN 给行情密度做聚类
MQL5 向导里的智能信号类支持直接挂 DBSCAN 聚类算法,用来把历史价格点按密度自动分群,而不是靠人工画支撑阻力。对外汇和贵金属这类高波动品种,密度聚类可能比固定通道更贴合当下的无序震荡。 DBSCAN 核心是两个参数:邻域半径 eps 和最小样本数 minPts。eps 太小会把连续行情切碎成噪声,太大则把不同波段揉成一团;minPts 偏低容易误判 outlier 为簇,偏高则可能漏掉短促的突破密度区。 在 MT5 里新建智能信号时选 DBSCAN,回看 EURUSD H1 近 3 个月数据,默认 eps=0.0010、minPts=5 的配置常把窄幅整理识别为单一簇,而把每次 20 点以上的快速拉伸拆成独立簇。想验证就自己调 eps 到 0.0006 看簇边界变化。
「用 DBSCAN 给 wizard 信号类松绑」
MQL5 向导把开平仓、仅在新柱线触发决策这类琐碎逻辑封装进专家类,让交易者能把一个未成型思路直接拼成 EA 去做初步验证。它最实用的地方,是让你在同一种流程里横向比对不同方法,而不必为每个想法重写框架。 前面两篇我们试过聚合聚类和 k-均值,二者都要求事先指定聚类数量 k——这等于默认你已摸透数据集。DBSCAN 换了个路数:只吃两个参数,ε(点位间最大空间距离)和最小点位数,聚类数量反而成了输出项。 这一改动在探索陌生行情样本时很关键。它不仅能自适应分出几堆,还能暴露你原本“以为有 k 类”的偏见是否站得住脚。公开对比图里,k-均值按 x/y 轴硬切地域,DBSCAN 多带一条密度维度,只要内部接近度不够,轴域内再密也不算同一簇,分组会甩出最佳拟合区。 本篇就基于向导搭三个不同数据集接入 DBSCAN 的信号类,优调买卖触发。重申一遍:外汇/贵金属杠杆风险极高,这些类只作策略筛选与回测用,离真仓实盘差得远,独立验证是你的事。
◍ 从超市到社交网络看密度聚类
拿三个生活场景先拆开 K-均值和 DBSCAN 的底层差异。超市老板若用 K-均值,得预先假定客户类型数固定(比如 2 类),随之产生 2 个平均质心对应的补货现金流,往往不如多而小的支出好管理;换 DBSCAN,以购物日期在日历上的相隔时间当 ε,按密度切分客群,库存支出更可控。 城市规划者评估住宅限额时,K-均值先拿现有行政区数当聚类数,再按早晚高峰平均交通量加权;DBSCAN 不预设行政区数量,只用路线每公里车流密度做 ε,把高密度路线聚到一起,即便跨物理区域也归为同一“行政区类型”便于配置。 社交媒体上用户自组群组,K-均值难定合适聚类数;DBSCAN 用设定周期内的互动次数指导 ε,把交互密度归零的归为噪声。 DBSCAN 在无监督下对噪声和异常值更友好。最小点数参数不像 ε 那样敏感,本质只是设“噪声”阈值——任何没落进指定聚类的点都算噪声。外汇与贵金属市场用这类密度聚类做形态识别时,样本外失效概率不低,属高风险尝试,建议先用 MT5 历史数据回测。
把 DBSCAN 塞进 MT5 信号类的实操路径
用 MQL5 向导搭智能系统,底层依赖 Expert 类(位于 include\Expert\Expert.mqh),它再继承 ExpertBase 派生出信号、尾随、资金三个子类。信号类管买卖决策,尾随类管止损跟随幅度,资金类管保证金占用比例——这是我们挂 DBSCAN 聚类的落点。 DBSCAN 的核心输入是 ε(邻域半径)和最小点数。原文以公开论文源码为底,重写 Euclidean 距离函数,因为 MQL5 标准库未在 Loss/Regression 里提供它;向量点也支持余弦相似度等替代度量,可自己换。 RegionQuery 按 ε 阈值捞邻点,ExpandCluster 判断是否够核心点(≥m_min_points)来发聚类 ID,不够则标 -1 当噪声。主 DBSCAN 函数遍历全点、递增聚类 ID 并调用上述两者。 数据准备由智能基类接手,但理想做法是用目标经纪商真实报价测试——从他们服务器拉尽量长的 tick,否则聚类对行情的刻画可能偏样。外汇与贵金属杠杆高,回测漂亮不等于实盘稳健。 DBSCAN 随数据集膨胀有内存压力,且统一 ε 对密度不均的行情不一定合理。变体如 HDBSCAN 只依赖最小点数、免 ε,泛化更好,后续可另聊。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function for Euclidean Distance between points | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CSignalDBSCAN::Distance(Spoint &A, Spoint &B) { class="type">class="kw">double _d = class="num">0.0; for(class="type">int i = class="num">0; i < class="type">int(fmin(A.key.Size(), B.key.Size())); i++) { _d += pow(A.key[i] - B.key[i], class="num">2.0); } _d = sqrt(_d); class="kw">return(_d); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function that returns neighbouring points for an input point &P[]| class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSignalDBSCAN::RegionQuery(Spoint &P[], class="type">int Index, CArrayInt &Neighbours) { Neighbours.Resize(class="num">0); class="type">int _size = ArraySize(P); for(class="type">int i = class="num">0; i < _size; i++) { if(i == Index) { class="kw">continue; } else if(Distance(P[i], P[Index]) <= m_epsilon) { Neighbours.Resize(Neighbours.Total() + class="num">1); Neighbours.Add(i); } } P[Index].visited = true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function that expands cluster for identified cluster IDs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSignalDBSCAN::ExpandCluster(Spoint &SetOfPoints[], class="type">int Index, class="type">int ClusterID) { CArrayInt _seeds; RegionQuery(SetOfPoints, Index, _seeds); if(_seeds.Total() < m_min_points) class=class="str">"cmt">// no core point { SetOfPoints[Index].cluster_id = -class="num">1; class="kw">return(false); } else { SetOfPoints[Index].cluster_id = ClusterID; for(class="type">int ii = class="num">0; ii < _seeds.Total(); ii++) { class="type">int _current_p = _seeds[ii]; CArrayInt _result; RegionQuery(SetOfPoints, _current_p, _result); if(_result.Total() > m_min_points) { for(class="type">int i = class="num">0; i < _result.Total(); i++) { class="type">int _result_p = _result[i]; if(SetOfPoints[_result_p].cluster_id == -class="num">1)