使用Python和MQL5进行特征工程(第四部分):基于UMAP回归的K线模式识别(基础篇)
◍ 用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 脚本里重跑,拿去喂给降维或聚类做形态实验。外汇与贵金属波动剧烈,这类特征仅描述历史结构,不预示后续方向。
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 倍的现象。
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 行删尾,确保你的训练集不碰测试期。
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