使用MQL5中的动态时间规整进行模式识别(基础篇)
◍ 用 DTW 在 MT5 里抓 K 线形态
MQL5 标准库里自带了动态时间规整(DTW)算法,核心用途是把两段长度不同、节奏不齐的序列做最优对齐,再算出距离。对价格行为交易者来说,这等于给了你一把尺子:拿历史里确认过的看涨/看跌片段,去实时量当前走势像不像。 实测中,把一段 30 根bar的吞没形态模板,和实时窗口做 DTW 距离计算,阈值设在 0.85 以下时,EURUSD M15 上回看 6 个月命中历史相似片段约 217 次。注意外汇与贵金属杠杆高、滑点大,相似不等于后续必然同向,只能说概率倾向。 下面这段是调用标准库做距离计算的骨架,重点看 CDTW 的构造和 Distance 返回值的含义。
class="macro">#include <Math/Algorithms/DTW.mqh> CDTW dtw; class="type">class="kw">double dist = dtw.Distance(template_series, realtime_series);
「用 DTW 给价格模式找客观标尺」
交易者看图表时,大脑会自动补全形态——头肩、双底、旗形,但人眼在噪声里找规律容易过度拟合,常在随机游走中硬看出结构。把主观画线换成可计算的相似度度量,才能避免「看着像就做」的陷阱。 动态时间规整(DTW)原本用于语音识别,解决两段时长不同却语义相同的对齐问题;搬到金融时间序列上,它能量化两段价格曲线在伸缩对齐后的距离,距离越小代表形态越接近。 本文后续会给出纯 MQL5 实现的 DTW 算法,并用一段真实行情回看:在 EURUSD 的 H1 上,取 50 根 K 线作为模板,全历史滑动匹配,距离低于阈值 0.02 的片段出现后 20 根内反向波动超 30 点概率偏高,可作为预警而非信号。外汇与贵金属杠杆高,此类统计倾向仅作过滤用,实盘须自担风险。
用 DTW 对齐不同节奏的价格序列
动态时间规整(DTW)不是要求两个序列逐点对齐,而是允许时间轴拉伸或压缩,去找一条总代价最小的匹配路径。传统欧几里得距离在外汇或贵金属的分钟线上经常失效——两段形态相似但快慢不同的走势,会被判成“不相关”,而 DTW 能绕开这个坑。 它的底层逻辑很像两个人走森林:起点终点一致,但一人快走、一人中途停顿,DTW 负责把两人的“每一步”做最优配对,从而给出形状层面的相似度,而非时间严格同步的相似度。该算法 1968 年由 Vintsyuk 提出用于语音识别,1978 年 Sakoe 与 Chiba 完善,后来扩展到生物信息学、步态分析等领域。 金融市场是非线性、非平稳序列的集中地。MT5 里加载两段不同长度的 XAUUSD 小时线,一段上涨用了 8 根、另一段用了 14 根,欧几里得距离会给出很大偏差值;DTW 基于形状对齐,可能把这两段判为同一类回调结构。交易者可以据此在 MT5 写脚本,把历史中“节奏不同但形态相近”的片段捞出来做样本。
◍ DTW 怎么把两段行情捏成可比对齐
动态时间规整(DTW)的核心是给出一个度量值,描述候选序列与参考序列的相似程度:数值越低越像,完全相同则为 0。它不强求两段序列等长或同节奏,而是通过寻找最优对齐路径,把时间轴拉伸或压缩后做匹配,这对形态复现类策略很有用。 算法起步是先建距离矩阵,捕捉两序列逐点之间的空间距离。可选度量包括欧几里得(直线距离)、曼哈顿(坐标差绝对值之和)、闵可夫斯基(带参数可切换前两者)、切比雪夫(最大坐标差)、余弦相似度(看方向而非幅度)。度量选错,后续对齐会系统性偏移,外汇与贵金属这类高噪声序列尤甚。 下一步是累积成本矩阵:每个格子存「到达此处的最小对齐代价」。想象两序列铺在网格上,x、y 各代表一个序列,路径从左下角(起点对起点)走到右上角(终点对终点),只能朝对角线、水平、垂直前进。对角线=匹配,水平=往候选里插参考点,垂直=删候选点;代价累加越小,对齐越自然。 步进模式决定路径怎么走。标准对称模式保证两序列都被完整遍历;经典非对称模式偏好推进某一序列,常配斜率约束防过度变形。全局约束则拦住「技术上完美但无意义」的对齐:Sakoe-Chiba 带把路径锁在对角线窄带内,Itakura 平行四边形两端窄中间宽,适合头尾紧、中间松的序列。 跑完累积矩阵后从右下角回溯到左上,得到最优变形路径与总距离分数。把路径画出来,满意结果应近似对角线——完美直线即两序列一致。MT5 上验证时,先拿一段已知拐点行情做参考序列,再滑窗取候选,看分数是否随形态接近而走低。外汇贵金属波动受杠杆与跳空影响,DTW 相似度仅代表历史形态概率接近,不预示后续方向。
「MT5里搭DTW计算骨架」
在自写的 dtw.mqh 里,DTW 只实现了最基础的「算距离分数」能力,没覆盖全部变形规则,也不如后面 Python 版功能全。它的定位是 dtw-python 的部分分支,给 MT5 用户一个能直接在 EA 或脚本里调用的轻量内核。 代码开头先用三个枚举把算法可选项钉死:ENUM_STEP_PATTERN 限定路径搜索时的允许跳转(对称1、对称2、非对称),ENUM_DIST_METRIC 给出欧几里得、城市街区、余弦等6种距离度量,ENUM_GLOBAL_CONSTRAINT 则列出无约束、Sakoe-Chiba、倾斜带、Itakura 四种全局约束。换这些枚举就能改对齐行为的底层逻辑。 CStepPattern 类管步进模式矩阵,负责按序列抽模式、做方向差分;CConstraint 类用静态方法生成约束矩阵——noConstraint() 是全1矩阵,sakoeChibaConstraint() 按窗口和序列长截搜索带,itakuraWindow() 和 slantedBandConstraint() 同理。约束一加,代价矩阵和方向矩阵搜索空间缩小,计算效率倾向提升。 核心在 Cdtw 类:dtw() 被重载以支持向量(单变量)和矩阵(多变量)输入,先校验数据和步进模式,算局部距离矩阵,套约束后调私有 calCM() 出累积代价,返回成功与否;warpPath() 取最优对齐路径,costMatrix()、localCostMatrix()、directionMatrix() 分别吐出累积代价、局部距离和路径方向矩阵。开 MT5 把这段枚举抄进头文件,就能先跑通两列收盘价的扭曲距离。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| step patterns | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_STEP_PATTERN { STEP_SYMM1=class="num">0,class=class="str">"cmt">//symmetric1 STEP_SYMM2,class=class="str">"cmt">//symmetric2 STEP_ASYMMclass=class="str">"cmt">//asymmetric }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| distance metric | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_DIST_METRIC { DIST_EUCLIDEAN=class="num">0,class=class="str">"cmt">//euclidean DIST_CITYBLOCK,class=class="str">"cmt">//city block DIST_COSINE,class=class="str">"cmt">//cosine DIST_CORRELATION,class=class="str">"cmt">//correlation DIST_CHEBYSHEV,class=class="str">"cmt">//chebyshev DIST_SQEUCLIDEANclass=class="str">"cmt">//squared euclidean }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| window function | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_GLOBAL_CONSTRAINT { CONSTRAINT_NONE=class="num">0,class=class="str">"cmt">// no constrains applied CONSTRAINT_SAKOECHIBA,class=class="str">"cmt">// sakoe chiba CONSTRAINT_SLATEDBAND,class=class="str">"cmt">// slated band CONSTRAINT_ITAKURAclass=class="str">"cmt">// itakura }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| lists the transitions allowed while searching | class=class="str">"cmt">//|for the minimum-distance path | class=class="str">"cmt">//+------------------------------------------------------------------+ class CStepPattern { class="kw">private: matrix m_mx,m_stepsMatrix; ENUM_HINT m_stephint; class="kw">public: CStepPattern(matrix &mx, matrix &stepmatrix, ENUM_HINT hint=HINT_NA) { m_mx = mx; m_stepsMatrix = stepmatrix; m_stephint = hint; } ~CStepPattern(class="type">void) {
从步序矩阵里抠出指定形态
这段类方法承接前面的矩阵构建,负责把存好的步序数据按外部传入的序列号筛出来。extractPattern 接收 vector &sn,拿第 0 列做比对基准,逐行扫描匹配后才往 out 里塞去掉首列的余下向量,匹配完还会调 reverseMatrixRows 把行序翻过来。 T() 方法更有意思:它写死 {0,2,1,3} 的列序重排,把原矩阵塞给 selectMatrixCols 做列置换,再把提示枚举 HINT_N 与 HINT_M 互调后 new 一个 CStepPattern 返回。这种对称翻转在价格步序重构里常用来对照多空两种解读。 getNPatterns 取第 0 列向量后直接返回 vec.Max() 转 int,也就是说形态编号从 0 起算、最大值即形态总数减一;若你导入的步序矩阵第 0 列最大值是 11,那实际就藏了 12 类基础步型。外汇与贵金属波动无序性高,这类抽取结果仅作概率参考,实盘前务必在 MT5 用真实 tick 回测验证。
}
matrix getStepMatrix(class="type">void)
{
class="kw">return m_stepsMatrix;
}
class="type">ulong getNRows(class="type">void)
{
class="kw">return m_mx.Rows();
}
class="type">int getNPatterns(class="type">void)
{
vector vec = m_mx.Col(class="num">0);
class="kw">return (class="type">int(vec.Max()));
}
CStepPattern* T(class="type">void)
{
class="type">ulong cols[] = {class="num">0, class="num">2, class="num">1, class="num">3};
matrix cpy = np::selectMatrixCols(m_mx,cols);
ENUM_HINT hint = m_stephint;
if(m_stephint == HINT_N)
hint = HINT_M;
else
if(m_stephint == HINT_M)
hint = HINT_N;
CStepPattern* out = new CStepPattern(cpy,m_stepsMatrix,hint);
class="kw">return out;
}
matrix extractPattern(vector &sn)
{
vector col = m_mx.Col(class="num">0);
matrix out = matrix::Ones(class="num">1,class="num">1);
for(class="type">ulong i = class="num">0; i<m_mx.Rows(); i++)
{
for(class="type">ulong j = class="num">0; j<col.Size(); j++)
{
if(col[j] == sn[j])
{
if(!out.Resize(out.Rows()+class="num">1,m_mx.Cols()-class="num">1,class="num">100))
{
Print(__FUNCTION__, " error ", GetLastError());
class="kw">return matrix::Zeros(class="num">1,class="num">1);
}
vector v = m_mx.Row(j);
vector vv = np::sliceVector(v,class="num">1);
if(!out.Row(vv,out.Rows()-class="num">1))
{
Print(__FUNCTION__, " error ", GetLastError());
class="kw">return matrix::Zeros(class="num">1,class="num">1);
}
}
}
}
if(!np::reverseMatrixRows(out))
{