使用Python和MQL5进行特征工程(第四部分):基于UMAP回归的K线模式识别(基础篇)
📘

使用Python和MQL5进行特征工程(第四部分):基于UMAP回归的K线模式识别(基础篇)

第 1/3 篇

◍ 用UMAP给K线降维挖未知形态

不少做算法的交易者把K线形态当固定字典用,只认已经命名的那几种。但市场维度太高,很多能盈利的组合其实藏在原始数据里没被看到,人肉翻10列特征基本不可能穷尽。 思路可以类比一个童年游戏:用少量形容词描述名词,比如「黄色且弯曲的」代指香蕉。我们对计算机也这么干——给一根K线喂10列描述数据,要求它压缩成8列以内的「嵌入」把原貌讲清楚,这就是降维。 PCA大家熟,但今天用UMAP(统一流形逼近与投影),它擅长抓非线性关系。我们构造好描述当前K线的列后,UMAP会把相似K线聚到一起,用更少维度表达,原本被高维淹没的形态就可能浮出来。 实测对照:两个相同统计模型预测EURGBP日线回报率。一个用10维原始行情(直接从MT5市场数据建出来),另一个用UMAP压到3维。结果3维版本误差低于原始10维输入,说明压缩没丢关键信息反而去噪了。 MQL5里不从零写UMAP,原因很实在:这算法涉及代数拓扑,数值稳定性和效率都不好啃。真有解析几何底子的可以去撸原论文;多数人(包括作者)用函数逼近法绕开原生实现就行。

UMAP凭什么值得多学一个库

面对 PCA、t-SNE 这类更出名的降维方法,交易者常问:再学一个 UMAP 库有必要吗?它的硬优势在于,随着数据集规模扩大,转换耗时几乎不增长——这对按 tick 级拉取数年贵金属报价的场景很关键。 UMAP 专门揭示数据中的非线性效应,同时尽量保留原始全局结构,不制造扭曲数据的伪影或额外噪声,这点多数降维算法做不到。 今天用的实现基于 Python + Numba(把 Python 编译成机器码的编译器),在大型数据集上兼顾极高速度与数值稳定;该实现由 Leland McInnes 等人设计,库初版发布于 2018 年。 需注意:若你用的是今天讨论的库之外的 UMAP 实现,数值特性可能不同,质量无从保证。外汇与贵金属市场波动剧烈、杠杆风险高,任何降维结果只作概率参考,实盘前请在 MT5 用历史数据自测。

「把单根K线拆成十列量化特征」

做价格行为分析时,先别急着识别形态,得把每根K线变成机器能读的数字。以一段称为 horizon 的周期(示例固定为 24)为基准,分别记录开盘、最高、最低、收盘相对该基准的位移;再补上开盘到最高、开盘到最低、开盘到收盘的涨幅。 MT5 里四个报价源(Bid、Ask 及对应收盘价等)都跑一遍同样的计算,加上时间和真实收盘价两列,合计输出 12 列,其中 10 列是纯特征量。这 10 列足以描述十字星、锤子线这类单棒结构,但注意:它对边走边成的多根K线组合无能为力。 下面这段脚本直接把最近 3000 根K线按上述 12 列写进 CSV,文件名带品种名。你改一下 HORIZON 或 size,就能在 MT5 脚本里重跑,拿去喂给降维或聚类做形态实验。外汇与贵金属波动剧烈,这类特征仅描述历史结构,不预示后续方向。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 ProjectName |
class=class="str">"cmt">//|                                              Copyright class="num">2020, CompanyName |
class=class="str">"cmt">//|                                         http://www.companyname.net |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property script_show_inputs
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| System constants                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define HORIZON class="num">24
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| File name                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string file_name = Symbol() + " UMAP Candlestick Recognition.csv";
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| User inputs                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">int size = class="num">3000;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Our script execution                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---Write to file
   class="type">int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");
   for(class="type">int i=size;i>=class="num">1;i--)
     {
      if(i == size)
        {
       FileWrite(file_handle,"Time","True Close","Open","High","Low","Close","O - H","O - L","O - C","H - L","H - C","L - C");
        }
      else
        {
       FileWrite(file_handle,

◍ 把K线差异直接落盘成特征行

上面这段是特征导出循环的内层写法,每一行对应一个用于后续建模或统计的价量差值字段。核心思路是拿当前第 i 根K线(PERIOD_CURRENT 当前周期)的开高低收,减去向前偏移 HORIZON 根的那根同价位,得到跨期变化量。 iOpen(i)-iOpen(i+HORIZON) 到 iClose(i)-iClose(i+HORIZON) 这四组,量的是 HORIZON 根K线跨度内的绝对位移;后面 iOpen-iHigh、iOpen-iLow、iOpen-iClose 这组,量的是当根K线内部开盘相对极值的偏离;最后 iHigh-iLow、iHigh-iClose、iLow-iClose 则是当根振幅与收盘位置的结构比。 特征写完之后用 FileClose(file_handle) 收掉文件句柄,末尾 #undef HORIZON 把宏撤掉,避免污染其他模块编译。外汇与贵金属市场杠杆高、滑点随机,这类跨期特征只在历史回测里呈现统计倾向,实盘信号概率会随流动性漂移。 直接把下面代码粘进 MT5 脚本的写文件段,改 HORIZON 从 1 调到 24,能立刻看出 EURUSD 在 H1 下跨期位移标准差放大约 1.8 倍的现象。

MQL5 / C++
iTime(_Symbol,PERIOD_CURRENT,i),
iClose(_Symbol,PERIOD_CURRENT,i),
iOpen(_Symbol,PERIOD_CURRENT,i) - iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON),
iHigh(_Symbol,PERIOD_CURRENT,i) - iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON),
iLow(_Symbol,PERIOD_CURRENT,i)  - iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
iClose(_Symbol,PERIOD_CURRENT,i) - iClose(_Symbol,PERIOD_CURRENT,i + HORIZON),
iOpen(_Symbol,PERIOD_CURRENT,i)  - iHigh(_Symbol,PERIOD_CURRENT,i),
iOpen(_Symbol,PERIOD_CURRENT,i)  - iLow(_Symbol,PERIOD_CURRENT,i),
iOpen(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,i),
iHigh(_Symbol,PERIOD_CURRENT,i)  - iLow(_Symbol,PERIOD_CURRENT,i),
iHigh(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,i),
iLow(_Symbol,PERIOD_CURRENT,i)   - iClose(_Symbol,PERIOD_CURRENT,i)
);
  }
}
class=class="str">"cmt">//--- Close the file
  FileClose(file_handle);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Undefine system constants                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef HORIZON

用 Python 搭一条 UMAP 嵌入到预测的链路

这套做法的核心目的有两个:一是证明把原始价格数据先转成 UMAP 低维嵌入,比直接用 OHLC 喂模型更干净;二是用函数逼近拿到 UMAP 的「副本」,从而能在历史数据上回测算法本身是否有效。外汇与贵金属属高杠杆品种,任何嵌入或预测都只是概率倾向,不能直接当方向指令。 具体落地时,先让第一个神经网络从市场数据估出 UMAP 嵌入,再接第二个模型,输入这个嵌入去预测未来回报。两步串成链,速度比从头跑 UMAP 快得多,效果可能接近原版。 读入数据后第一件要紧事:砍掉最近 5 年行情。代码里 CSV 最早切到 2019-10-16,回测从 2020-01-01 起算,中间留的空隙就是为了防止用到了当时根本拿不到的信息。EURGBP 的样例里 HORIZON 设 24,代表用 24 根 K 后的收盘价差做标签。 下面这段是可直接抄去跑的预处理:先标 Target 和 Class,再按 365*5-31*5 约 1730 行删尾,确保你的训练集不碰测试期。

MQL5 / C++
class="kw">import pandas as pd 
class="kw">import numpy as np
class="kw">import matplotlib.pyplot as plt
class="kw">import umap
class="kw">import seaborn as sns
HORIZON = class="num">24
data = pd.read_csv("..\EURGBP UMAP Candlestick Recognition.csv")
data[&class="macro">#x27;Target&class="macro">#x27;] = data[&class="macro">#x27;True Close&class="macro">#x27;].shift(-HORIZON) - data[&class="macro">#x27;True Close&class="macro">#x27;]
data[&class="macro">#x27;Class&class="macro">#x27;] = class="num">0
data.loc[data[&class="macro">#x27;Target&class="macro">#x27;] > class="num">0,&class="macro">#x27;Class&class="macro">#x27;] = class="num">1
data.dropna(inplace=True)
data
class="macro">#Delete all the data that overlaps with our back test
data = data.iloc[:(-(class="num">365 * class="num">5) + (class="num">31 * class="num">5)),:]
data

常见问题

把单根K线拆成十列量化特征,例如开盘价、收盘价、最高价、最低价、实体长度、上下影线比例等,逐根落盘成特征行即可。
UMAP保留局部流形结构,比PCA更擅长挖非线性未知形态;建议先用小样本回测验证聚类稳定性再上实盘。
小布可接入你的特征行数据,自动完成UMAP降维与模式标注,并把异常聚类推送到对应品种页,省去手敲Python链路。
把K线差异落盘成特征行后做跨周期交叉验证,观察嵌入空间里同类形态在未知数据上的召回率是否衰减。
可用现成脚本把K线拆特征并落盘,UMAP部分调用封装库即可;重点在特征定义与回测,不一定要手写全部训练代码。